Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
36baeeb081 | ||
|
|
7698ea040d | ||
|
|
84d911ae9e |
No files matched your search
+1
-1
@@ -1,5 +1,5 @@
|
||||
cmake_minimum_required(VERSION 3.24)
|
||||
project(KSwitcher VERSION 0.6.89 LANGUAGES CXX RC)
|
||||
project(KSwitcher VERSION 0.6.90 LANGUAGES CXX RC)
|
||||
if(NOT WIN32)
|
||||
message(FATAL_ERROR "KSwitcher uses native Windows APIs")
|
||||
endif()
|
||||
|
||||
+13
@@ -13,6 +13,15 @@ Far Manager є зразком для оформлення файлового м
|
||||
Це діє і для вкладених діалогів. Під час файлової операції Cancel спершу
|
||||
просить підтвердити переривання.
|
||||
|
||||
Спільне повідомлення Files підлаштовується під заголовок і кількість рядків.
|
||||
Висота діалогу залежить від видимого вмісту. Приховані та порожні секції
|
||||
не займають місця. Перед завершенням змін перевір коротке повідомлення й
|
||||
повідомлення з кількома рядками, включно з відступами до рамки та кнопок.
|
||||
Смуга прокрутки з’являється, лише коли текст не вміщується в доступну висоту.
|
||||
Оформлення, кнопки й необов’язковий прапорець задаються параметрами шаблону.
|
||||
Підтвердження гібернації та віддаленого оновлення мають червоне оформлення
|
||||
Warning і початковий фокус на No.
|
||||
|
||||
## Скорочення
|
||||
|
||||
- Enter і подвійний клік відкривають папку в поточній панелі, а локальний
|
||||
@@ -98,6 +107,10 @@ Far Manager є зразком для оформлення файлового м
|
||||
а Enter відкриває поточний елемент.
|
||||
- F9 перемикає головне меню Files. Після закриття фокус повертається в командний рядок. Коли меню відкрите, Alt з літерою верхнього рівня відкриває відповідний пункт.
|
||||
- Коли головне меню відкрите, L, F, C, O та R відкривають Left, Files, Commands, Options та Right. У підменю підсвічена літера викликає відповідну команду. Літери унікальні в межах поточного рівня. Усталені мнемоніки мають пріоритет, зокрема X для Exit.
|
||||
- У Commands лінії відокремлюють навігацію панелі, перегляд і редагування
|
||||
та керування віддаленим ПК. Остання група з’являється разом зі своїм
|
||||
роздільником лише в активній LAN-панелі. Роздільники пропускаються під
|
||||
час навігації й не реагують на клік.
|
||||
- Глобальне скорочення **Open KSwitcher Files** можна змінити або очистити в налаштуваннях. Для профілю без цього поля пропонується Ctrl+Alt+F, якщо Windows реєструє комбінацію і вона вільна серед прив’язок KSwitcher.
|
||||
- Глобальне скорочення повертає до відкритого вікна Files у поточній сесії Windows або запускає Files, якщо такого вікна немає. Пункт **Files** у треї відкриває нове вікно.
|
||||
|
||||
|
||||
Binary file not shown.
@@ -1,5 +1,11 @@
|
||||
# Latest Windows release
|
||||
|
||||
## v0.6.90 — 2026-11-16
|
||||
|
||||
Files groups Commands into navigation, viewing and editing, and remote PC actions. The remote group appears in an active LAN pane. Shared notices fit their contents and show a scrollbar only when needed. Remote hibernation and update confirmations use a compact red warning with No focused by default.
|
||||
|
||||
**Українською.** Меню Commands у Files групує навігацію, перегляд і редагування та дії для віддаленого ПК. Віддалена група доступна в активній LAN-панелі. Спільні повідомлення підлаштовуються під вміст і показують прокрутку лише за потреби. Гібернація та віддалене оновлення використовують компактне червоне підтвердження з фокусом на No.
|
||||
|
||||
## v0.6.89 — 2026-11-16
|
||||
|
||||
In Files, Commands now offers **Hibernate remote PC** and **Update remote KSwitcher** for the active LAN pane. The target PC has separate, default-off permissions for each action. Unattended updates verify the stable package, close the target installation's Files, Config and screenshot windows, and restart KSwitcher. Forced closure discards unsaved changes. The target journal records closed processes and the update result.
|
||||
|
||||
@@ -1 +1 @@
|
||||
06d74702f466c8f793fafbbf54942fd1688e36f975b4d709652bbdb023e2fc2f KSwitcher-win-x64.zip
|
||||
67db5adc069f847aa1d9987e4043dfd488b03ae15444a99b007774ac01556461 KSwitcher-win-x64.zip
|
||||
+60
-60
@@ -1,136 +1,136 @@
|
||||
{
|
||||
"schema": 1,
|
||||
"version": "0.6.89",
|
||||
"version": "0.6.90",
|
||||
"files": [
|
||||
{
|
||||
"path": "KSwitcher.exe",
|
||||
"sha256": "411340c538f2b76424b7e4d228c217ab1b56e059ee640c109888f59115d9d5ec",
|
||||
"size": 2465792
|
||||
"size": 2465792,
|
||||
"sha256": "7f6d0d1508a324ccc793455f6667084dcd1003cbc472dca9f6262c014068f99b",
|
||||
"path": "KSwitcher.exe"
|
||||
},
|
||||
{
|
||||
"path": "KSwitcher.Config.exe",
|
||||
"sha256": "f48c68669147e32090823465639d8301798f2e4934e160becd1b5fd42d6708bb",
|
||||
"size": 2445824
|
||||
"size": 2445824,
|
||||
"sha256": "2ae249c373b6f8a8f232daffae84738751d9aad7d1f39bd7335fb379a415e50c",
|
||||
"path": "KSwitcher.Config.exe"
|
||||
},
|
||||
{
|
||||
"path": "KSwitcher.Screenshot.exe",
|
||||
"sha256": "311ebf1a3d135fe6ce8bfd0978a9552fbb64c7d7f9a791f4aa4c5bd2c439f06f",
|
||||
"size": 3052544
|
||||
"size": 3052544,
|
||||
"sha256": "bd1fca6c5dfa35ac2ae4afab5becb67ed7701d011ce6e671a05340f0634150e2",
|
||||
"path": "KSwitcher.Screenshot.exe"
|
||||
},
|
||||
{
|
||||
"path": "KSwitcher.Files.exe",
|
||||
"sha256": "73acd9be067451c3db83706be0164959f2ac130259c41c48841d16d1aa2792d5",
|
||||
"size": 3671552
|
||||
"size": 3675136,
|
||||
"sha256": "bbcddc17a1cd69824a8cbdff7301854739c295b6b4fb27227acb00d375a9ab3a",
|
||||
"path": "KSwitcher.Files.exe"
|
||||
},
|
||||
{
|
||||
"path": "WebView2Loader.dll",
|
||||
"size": 194912,
|
||||
"sha256": "d3fce90bdeecb08596e377dea6a316354dd8949e32ff09b89d51be00c14e21b6",
|
||||
"size": 194912
|
||||
"path": "WebView2Loader.dll"
|
||||
},
|
||||
{
|
||||
"path": "terminal/addon-fit-LICENSE.txt",
|
||||
"size": 1103,
|
||||
"sha256": "e256f01188af527e4d06d21d06fbf785ae9c50d4b328bf03cbe0ba7f0aa4228f",
|
||||
"size": 1103
|
||||
"path": "terminal/addon-fit-LICENSE.txt"
|
||||
},
|
||||
{
|
||||
"path": "terminal/addon-fit.js",
|
||||
"size": 1521,
|
||||
"sha256": "ba3ea256ce0620a0992a197d6c9baea64823fc93d8da07a9e366ca9943c18527",
|
||||
"size": 1521
|
||||
"path": "terminal/addon-fit.js"
|
||||
},
|
||||
{
|
||||
"path": "terminal/index.html",
|
||||
"size": 527,
|
||||
"sha256": "084d97a727b778fb10d31adcd557ee7840a38377ed60e0ef0f3ece93b349b1ef",
|
||||
"size": 527
|
||||
"path": "terminal/index.html"
|
||||
},
|
||||
{
|
||||
"path": "terminal/terminal.js",
|
||||
"size": 4268,
|
||||
"sha256": "fb302b2902fea6a815a8266811f350d1e4c108ca88fafd49e6eddcbfff90a5e5",
|
||||
"size": 4268
|
||||
"path": "terminal/terminal.js"
|
||||
},
|
||||
{
|
||||
"path": "terminal/xterm-LICENSE.txt",
|
||||
"size": 1261,
|
||||
"sha256": "b569f629d00f2626a8100df2a1798210535621e42164dfd426a6fe5aac7b0ccd",
|
||||
"size": 1261
|
||||
"path": "terminal/xterm-LICENSE.txt"
|
||||
},
|
||||
{
|
||||
"path": "terminal/xterm.css",
|
||||
"size": 7112,
|
||||
"sha256": "854a7c0fb70e8b1a083c16797ab827299fb18744f5ad34f227b48337e33293c6",
|
||||
"size": 7112
|
||||
"path": "terminal/xterm.css"
|
||||
},
|
||||
{
|
||||
"path": "terminal/xterm.js",
|
||||
"size": 488663,
|
||||
"sha256": "14903579ff54664cd72f8e8699e6961a6272c21863ec1c3b118cdc8af5d4a972",
|
||||
"size": 488663
|
||||
"path": "terminal/xterm.js"
|
||||
},
|
||||
{
|
||||
"path": "LICENSE",
|
||||
"size": 4401,
|
||||
"sha256": "d3c81140cce01c26bf6cbf52aa40e0825e1fa35b9a91d1387170a65d2597082c",
|
||||
"size": 4401
|
||||
"path": "LICENSE"
|
||||
},
|
||||
{
|
||||
"path": "json-LICENSE.txt",
|
||||
"size": 1075,
|
||||
"sha256": "c0d068392ea65358b798b8c165103560f06e9e3b38c4ab4e2d8810a7b931af86",
|
||||
"size": 1075
|
||||
"path": "json-LICENSE.txt"
|
||||
},
|
||||
{
|
||||
"path": "files-text-license.txt",
|
||||
"size": 883,
|
||||
"sha256": "185c5a37521316e940ead89cbf0b1ba5862d810c7c966e6c6a14a4a515b23625",
|
||||
"size": 883
|
||||
"path": "files-text-license.txt"
|
||||
},
|
||||
{
|
||||
"path": "README.md",
|
||||
"size": 94223,
|
||||
"sha256": "7b34f2af7341f57d7bac1b5b7e55a40595a6bc20f461f98c0c6320336d0deba7",
|
||||
"size": 94223
|
||||
"path": "README.md"
|
||||
},
|
||||
{
|
||||
"path": "LAN-PREVIEW.md",
|
||||
"size": 11418,
|
||||
"sha256": "2c13d3ed4761e679c352fb1ade24a1ec3060ad2862376ede801cdee5fcc3bada",
|
||||
"size": 11418
|
||||
"path": "LAN-PREVIEW.md"
|
||||
},
|
||||
{
|
||||
"path": "dictionaries/en.txt",
|
||||
"size": 682867,
|
||||
"sha256": "5f267c141c3d6837870e1c098f86b89a113ba300343119432644ead88f720c21",
|
||||
"size": 682867
|
||||
"path": "dictionaries/en.txt"
|
||||
},
|
||||
{
|
||||
"path": "dictionaries/LICENSE-CC-BY-SA-4.0.txt",
|
||||
"size": 20138,
|
||||
"sha256": "28a9529c7d0bb4dc51f4bf5c116a3d16ef247a052f7591466768ddf563fd1cf5",
|
||||
"size": 20138
|
||||
"path": "dictionaries/LICENSE-CC-BY-SA-4.0.txt"
|
||||
},
|
||||
{
|
||||
"path": "dictionaries/README.md",
|
||||
"size": 2265,
|
||||
"sha256": "3017bf44d3be9d9dfab21bd6778aa52b7947e61acf9ce2bc86b41d6e949ac311",
|
||||
"size": 2265
|
||||
"path": "dictionaries/README.md"
|
||||
},
|
||||
{
|
||||
"path": "dictionaries/ru.txt",
|
||||
"size": 1060364,
|
||||
"sha256": "9ef922c7699569304a428f6aba0fd13a21341c0d0cb9561f37ef83c41577a28b",
|
||||
"size": 1060364
|
||||
"path": "dictionaries/ru.txt"
|
||||
},
|
||||
{
|
||||
"path": "dictionaries/sources.json",
|
||||
"size": 1280,
|
||||
"sha256": "98e5b3fe7304e37705c9e5ff48b0b399ccb16e806736d21f09921b99e5f2a408",
|
||||
"size": 1280
|
||||
"path": "dictionaries/sources.json"
|
||||
},
|
||||
{
|
||||
"path": "dictionaries/uk.txt",
|
||||
"size": 940799,
|
||||
"sha256": "d30e990d6ec2983c04f3644a3c61c6c668e8c542323f5931916fdd55b916eebf",
|
||||
"size": 940799
|
||||
"path": "dictionaries/uk.txt"
|
||||
},
|
||||
{
|
||||
"path": "tools/README.txt",
|
||||
"size": 993,
|
||||
"sha256": "a34b5161a8315af037e1ec71d816bfc688f7c364141d4ee2347cf0515ef0c82a",
|
||||
"size": 993
|
||||
"path": "tools/README.txt"
|
||||
},
|
||||
{
|
||||
"path": "tools/StayHere.exe",
|
||||
"sha256": "2b214b0ed2b6f754611f3355ccdcc6797c6bc5c2aac70bd51e060ba25eb4c3a5",
|
||||
"size": 133120
|
||||
"size": 133120,
|
||||
"sha256": "9e914d996864d7f982ba50e481381d673e4e84ee7ad5d1f477f0898aa1816256",
|
||||
"path": "tools/StayHere.exe"
|
||||
},
|
||||
{
|
||||
"path": "tools/StayHere-README.md",
|
||||
"size": 2796,
|
||||
"sha256": "ba214e2aca7405e698b3ab21ceb1f41ce088edd996b3e30e4a9d58ff496f9413",
|
||||
"size": 2796
|
||||
"path": "tools/StayHere-README.md"
|
||||
}
|
||||
]
|
||||
],
|
||||
"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,89,0
|
||||
PRODUCTVERSION 0,6,89,0
|
||||
FILEVERSION 0,6,90,0
|
||||
PRODUCTVERSION 0,6,90,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.89"
|
||||
VALUE "FileVersion", "0.6.90"
|
||||
END
|
||||
END
|
||||
BLOCK "VarFileInfo"
|
||||
|
||||
+5
-13
@@ -47,24 +47,16 @@ BEGIN
|
||||
CONTROL "&Cancel", IDCANCEL, "Button", BS_OWNERDRAW | WS_TABSTOP, 153, 42, 60, 17
|
||||
END
|
||||
|
||||
607 DIALOGEX 0, 0, 300, 106
|
||||
STYLE DS_SETFONT | DS_CENTER | WS_POPUP
|
||||
FONT 10, "Consolas"
|
||||
BEGIN
|
||||
CONTROL "", 100, "Edit", ES_MULTILINE | ES_READONLY | ES_AUTOVSCROLL | WS_VSCROLL | WS_TABSTOP, 18, 22, 264, 52
|
||||
CONTROL "&OK", IDOK, "Button", BS_OWNERDRAW | WS_TABSTOP, 120, 80, 60, 17
|
||||
CONTROL "&Yes", IDYES, "Button", BS_OWNERDRAW | WS_TABSTOP | NOT WS_VISIBLE, 85, 80, 60, 17
|
||||
CONTROL "&No", IDNO, "Button", BS_OWNERDRAW | WS_TABSTOP | NOT WS_VISIBLE, 155, 80, 60, 17
|
||||
AUTOCHECKBOX "&Remember for this session", 101, 18, 63, 264, 13, NOT WS_VISIBLE
|
||||
END
|
||||
|
||||
609 DIALOGEX 0, 0, 185, 57
|
||||
607 DIALOGEX 0, 0, 185, 57
|
||||
STYLE DS_SETFONT | DS_CENTER | WS_POPUP | WS_CLIPCHILDREN
|
||||
FONT 10, "Consolas"
|
||||
BEGIN
|
||||
LTEXT "", 100, 14, 16, 157, 13, SS_NOPREFIX
|
||||
CONTROL "", 100, "Edit", ES_MULTILINE | ES_READONLY | ES_AUTOVSCROLL | WS_VSCROLL, 14, 16, 157, 13
|
||||
AUTOCHECKBOX "&Remember for this session", 101, 14, 30, 157, 13, NOT WS_VISIBLE
|
||||
CONTROL "", 102, "Static", NOT WS_VISIBLE, 14, 35, 1, 1
|
||||
CONTROL "&OK", IDOK, "Button", BS_OWNERDRAW | WS_TABSTOP, 67, 38, 51, 15
|
||||
CONTROL "&Yes", IDYES, "Button", BS_OWNERDRAW | WS_TABSTOP | NOT WS_VISIBLE, 36, 38, 51, 15
|
||||
CONTROL "&No", IDNO, "Button", BS_OWNERDRAW | WS_TABSTOP | NOT WS_VISIBLE, 98, 38, 51, 15
|
||||
END
|
||||
|
||||
608 DIALOGEX 0, 0, 220, 67
|
||||
|
||||
+158
-55
@@ -316,6 +316,7 @@ HWND picker{}, pickerFrame{}, menuBar{};
|
||||
HWND menuItems[5]{};
|
||||
constexpr std::wstring_view menuAccessKeys=L"LFCOR";
|
||||
std::vector<size_t> commandAccessPositions;
|
||||
std::vector<int> commandActions;
|
||||
int menuIndex{};
|
||||
bool commandPicker{};
|
||||
constexpr int defaultSizeWidth=6, defaultFontPoints=110;
|
||||
@@ -388,8 +389,14 @@ void savePlacement(HWND window)
|
||||
&placement,sizeof(placement));
|
||||
}
|
||||
void chooseDrive(int index, bool fromMenu = false);
|
||||
INT_PTR showFilesNotice(HWND owner,const wchar_t *text,const wchar_t *title,bool confirm=false,
|
||||
bool *remember=nullptr,bool warning=false);
|
||||
enum class FilesNoticeTone { dialog, neutral, warning };
|
||||
struct FilesNoticeOptions
|
||||
{
|
||||
bool confirm{};
|
||||
bool *remember{};
|
||||
FilesNoticeTone tone{FilesNoticeTone::dialog};
|
||||
};
|
||||
INT_PTR showFilesNotice(HWND owner,const wchar_t *text,const wchar_t *title,FilesNoticeOptions options={});
|
||||
void openMenu(HWND window, int index);
|
||||
void showFileDetails(const Panel &panel);
|
||||
void editOptions(HWND owner);
|
||||
@@ -1279,7 +1286,7 @@ void openOn(Panel &panel,const fs::path &directory,bool offerElevation,std::opti
|
||||
const auto question=L"Windows denied access to this folder.\r\n"+path+
|
||||
L"\r\n\r\nTry opening as administrator? Windows may still ask for UAC.";
|
||||
if(rememberElevationApproval || showFilesNotice(owner,question.c_str(),
|
||||
L"KSwitcher Files",true,&remember)==IDYES)
|
||||
L"KSwitcher Files",{.confirm=true,.remember=&remember})==IDYES)
|
||||
{
|
||||
const auto result=requestElevatedFolder(owner,directory,folders,files,metadata);
|
||||
SetForegroundWindow(owner);
|
||||
@@ -1431,7 +1438,8 @@ void runActivePeerAction(HWND owner,bool update)
|
||||
const auto question=update
|
||||
? L"Update KSwitcher on "+peer.name+L"? Open KSwitcher windows there will close if an update is available."
|
||||
: L"Hibernate "+peer.name+L"?";
|
||||
if(showFilesNotice(owner,question.c_str(),update?L"Update remote KSwitcher":L"Hibernate remote PC",true)!=IDYES)
|
||||
if(showFilesNotice(owner,question.c_str(),update?L"Update remote KSwitcher":L"Hibernate remote PC",
|
||||
{.confirm=true,.tone=FilesNoticeTone::warning})!=IDYES)
|
||||
return;
|
||||
|
||||
std::packaged_task<std::wstring()> task([peer,update]
|
||||
@@ -1481,6 +1489,11 @@ void runActivePeerAction(HWND owner,bool update)
|
||||
void selectDrive()
|
||||
{
|
||||
auto row=SendMessageW(picker,LB_GETCURSEL,0,0);
|
||||
if(commandPicker)
|
||||
{
|
||||
if(row<0 || static_cast<size_t>(row)>=commandActions.size() || commandActions[row]<0) return;
|
||||
row=commandActions[row];
|
||||
}
|
||||
if(!commandPicker && (row<0 || static_cast<size_t>(row)==drivePaths.size())) return;
|
||||
const auto remote=remoteDriveRows.find(row);
|
||||
if(commandPicker || row<static_cast<LRESULT>(drivePaths.size()) || remote!=remoteDriveRows.end())
|
||||
@@ -1516,6 +1529,26 @@ void selectDrive()
|
||||
|
||||
LRESULT CALLBACK pickerKeys(HWND window, UINT msg, WPARAM wp, LPARAM lp, UINT_PTR, DWORD_PTR)
|
||||
{
|
||||
if(commandPicker)
|
||||
{
|
||||
const auto separator=[](LRESULT row)
|
||||
{
|
||||
return row>=0 && static_cast<size_t>(row)<commandActions.size() && commandActions[row]<0;
|
||||
};
|
||||
if(msg==WM_KEYDOWN && (wp==VK_UP || wp==VK_DOWN || wp==VK_PRIOR || wp==VK_NEXT))
|
||||
{
|
||||
const auto result=DefSubclassProc(window,msg,wp,lp);
|
||||
const auto row=SendMessageW(window,LB_GETCURSEL,0,0);
|
||||
if(separator(row))
|
||||
SendMessageW(window,LB_SETCURSEL,row+((wp==VK_UP || wp==VK_PRIOR)?-1:1),0);
|
||||
return result;
|
||||
}
|
||||
if(msg==WM_LBUTTONDOWN || msg==WM_LBUTTONDBLCLK || (msg==WM_MOUSEMOVE && (wp&MK_LBUTTON)))
|
||||
{
|
||||
const auto hit=SendMessageW(window,LB_ITEMFROMPOINT,0,lp);
|
||||
if(!HIWORD(hit) && separator(LOWORD(hit))) return 0;
|
||||
}
|
||||
}
|
||||
if(!commandPicker && msg==WM_CHAR)
|
||||
{
|
||||
const auto key=towupper(static_cast<wchar_t>(wp));
|
||||
@@ -1881,7 +1914,8 @@ void openSelectedText(const Panel &panel, bool edit, bool external)
|
||||
farDropDownPalette,FilesModalDialog::run,FilesModalDialog::finish,FilesModalDialog::dismissOwned,
|
||||
[](HWND owner)
|
||||
{
|
||||
return showFilesNotice(owner,L"This file is already open. Switch to its window?",L"Files",true)==IDYES;
|
||||
return showFilesNotice(owner,L"This file is already open. Switch to its window?",L"Files",
|
||||
{.confirm=true})==IDYES;
|
||||
}},panel.peer?&*panel.peer:nullptr);
|
||||
}
|
||||
|
||||
@@ -2514,7 +2548,7 @@ INT_PTR CALLBACK copyDialog(HWND dialog, UINT message, WPARAM wp, LPARAM lp)
|
||||
if(LOWORD(wp)!=IDOK) return FALSE;
|
||||
const auto showError=[&](const wchar_t *message)
|
||||
{
|
||||
showFilesNotice(dialog,message,L"Error",false,nullptr,true);
|
||||
showFilesNotice(dialog,message,L"Error",{.tone=FilesNoticeTone::warning});
|
||||
SetFocus(GetDlgItem(dialog,pathField));
|
||||
};
|
||||
try
|
||||
@@ -2904,11 +2938,55 @@ struct FilesNotice
|
||||
{
|
||||
const wchar_t *text;
|
||||
std::wstring title;
|
||||
bool confirm;
|
||||
bool *remember;
|
||||
bool warning;
|
||||
FilesNoticeOptions options;
|
||||
};
|
||||
|
||||
void layoutFilesNotice(HWND dialog)
|
||||
{
|
||||
const auto ¬ice=*reinterpret_cast<const FilesNotice *>(GetWindowLongPtrW(dialog,DWLP_USER));
|
||||
const auto frame=dialogFrameMetrics(GetDpiForWindow(dialog));
|
||||
const auto inset=frame.content();
|
||||
const auto top=dialogListInset(frame,textHeight);
|
||||
const auto field=GetDlgItem(dialog,100);
|
||||
RECT client{};
|
||||
GetClientRect(dialog,&client);
|
||||
const auto contentWidth=std::max(1L,client.right-2*inset);
|
||||
TEXTMETRICW metrics{};
|
||||
if(const auto dc=GetDC(field))
|
||||
{
|
||||
const auto previous=SelectObject(dc,mono);
|
||||
GetTextMetricsW(dc,&metrics);
|
||||
SelectObject(dc,previous);
|
||||
ReleaseDC(field,dc);
|
||||
}
|
||||
const auto lineHeight=std::max(1L,metrics.tmHeight);
|
||||
const auto place=[&](int id,int y,int height)
|
||||
{
|
||||
SetWindowPos(GetDlgItem(dialog,id),nullptr,inset,y,contentWidth,height,SWP_NOZORDER|SWP_NOACTIVATE);
|
||||
};
|
||||
constexpr LRESULT maxLines=8;
|
||||
ShowScrollBar(field,SB_VERT,FALSE);
|
||||
place(100,top,maxLines*lineHeight);
|
||||
const auto lines=SendMessageW(field,EM_GETLINECOUNT,0,0);
|
||||
const bool overflow=lines>maxLines;
|
||||
const auto height=static_cast<int>(std::clamp<LRESULT>(lines,1,maxLines))*lineHeight;
|
||||
place(100,top,height);
|
||||
ShowScrollBar(field,SB_VERT,overflow);
|
||||
const auto style=GetWindowLongPtrW(field,GWL_STYLE);
|
||||
SetWindowLongPtrW(field,GWL_STYLE,overflow?style|WS_TABSTOP:style&~WS_TABSTOP);
|
||||
if(!overflow) SendMessageW(field,EM_LINESCROLL,0,-SendMessageW(field,EM_GETFIRSTVISIBLELINE,0,0));
|
||||
auto bottom=top+height;
|
||||
if(notice.options.remember)
|
||||
{
|
||||
bottom+=dialogRowGap();
|
||||
place(101,bottom,textHeight);
|
||||
bottom+=textHeight;
|
||||
}
|
||||
place(102,bottom+dialogRowGap()+4,1);
|
||||
if(notice.options.confirm) layoutDialogButtons(dialog,102,{IDYES,IDNO});
|
||||
else layoutDialogButtons(dialog,102,{IDOK});
|
||||
}
|
||||
|
||||
INT_PTR CALLBACK filesNoticeDialog(HWND window,UINT message,WPARAM wp,LPARAM lp)
|
||||
{
|
||||
if(message==WM_INITDIALOG)
|
||||
@@ -2916,70 +2994,73 @@ INT_PTR CALLBACK filesNoticeDialog(HWND window,UINT message,WPARAM wp,LPARAM lp)
|
||||
SetWindowLongPtrW(window,DWLP_USER,lp);
|
||||
const auto ¬ice=*reinterpret_cast<const FilesNotice *>(lp);
|
||||
prepareTextDialog(window);
|
||||
SetDlgItemTextW(window,100,notice.text);
|
||||
if(notice.warning)
|
||||
const auto field=GetDlgItem(window,100);
|
||||
SendMessageW(field,EM_SETMARGINS,EC_LEFTMARGIN|EC_RIGHTMARGIN,0);
|
||||
SetWindowTextW(field,notice.text);
|
||||
if(notice.options.remember)
|
||||
{
|
||||
const auto bounds=dialogControlBounds(window,100);
|
||||
const auto divider=dialogControlBounds(window,102);
|
||||
RECT text{0,0,bounds.right-bounds.left,0};
|
||||
if(const auto dc=GetDC(window))
|
||||
{
|
||||
const auto previous=SelectObject(dc,mono);
|
||||
DrawTextW(dc,notice.text,-1,&text,DT_CALCRECT|DT_WORDBREAK|DT_NOPREFIX);
|
||||
SelectObject(dc,previous);
|
||||
ReleaseDC(window,dc);
|
||||
}
|
||||
const auto height=std::max<LONG>(text.bottom,textHeight);
|
||||
SetWindowPos(GetDlgItem(window,100),nullptr,bounds.left,bounds.top,bounds.right-bounds.left,height,
|
||||
SWP_NOZORDER|SWP_NOACTIVATE);
|
||||
SetWindowPos(GetDlgItem(window,102),nullptr,divider.left,bounds.top+height+dialogRowGap()+4,0,0,
|
||||
SWP_NOSIZE|SWP_NOZORDER|SWP_NOACTIVATE);
|
||||
layoutDialogButtons(window,102,{IDOK});
|
||||
const auto checkbox=GetDlgItem(window,101);
|
||||
installFarCheckbox(checkbox,notice.options.tone==FilesNoticeTone::warning?FarCheckboxSurface::warning:
|
||||
notice.options.tone==FilesNoticeTone::neutral?FarCheckboxSurface::gray:FarCheckboxSurface::dialog);
|
||||
ShowWindow(checkbox,SW_SHOW);
|
||||
}
|
||||
if(notice.remember)
|
||||
{
|
||||
const auto field=GetDlgItem(window,100);
|
||||
RECT bounds{};
|
||||
GetWindowRect(field,&bounds);
|
||||
SetWindowPos(field,nullptr,0,0,bounds.right-bounds.left,
|
||||
(bounds.bottom-bounds.top)*3/4,SWP_NOMOVE|SWP_NOZORDER|SWP_NOACTIVATE);
|
||||
ShowWindow(GetDlgItem(window,101),SW_SHOW);
|
||||
}
|
||||
if(notice.confirm)
|
||||
if(notice.options.confirm)
|
||||
{
|
||||
ShowWindow(GetDlgItem(window,IDOK),SW_HIDE);
|
||||
ShowWindow(GetDlgItem(window,IDYES),SW_SHOW);
|
||||
ShowWindow(GetDlgItem(window,IDNO),SW_SHOW);
|
||||
groupDialogButtons(window,{IDYES,IDNO});
|
||||
}
|
||||
else if(!notice.warning) groupDialogButtons(window,{IDOK});
|
||||
const auto defaultButton=notice.confirm?IDNO:IDOK;
|
||||
RECT caption{},text{};
|
||||
if(const auto dc=GetDC(window))
|
||||
{
|
||||
const auto previous=SelectObject(dc,mono);
|
||||
DrawTextW(dc,notice.title.c_str(),-1,&caption,DT_CALCRECT|DT_SINGLELINE|DT_NOPREFIX);
|
||||
DrawTextW(dc,notice.text,-1,&text,DT_CALCRECT|DT_NOPREFIX|DT_EXPANDTABS);
|
||||
SelectObject(dc,previous);
|
||||
ReleaseDC(window,dc);
|
||||
}
|
||||
const auto inset=dialogFrameMetrics(GetDpiForWindow(window)).content();
|
||||
const auto minimum=std::max({24L*cellWidth,caption.right+6*cellWidth,
|
||||
notice.options.remember?30L*cellWidth:0L})+2*inset;
|
||||
const auto width=std::clamp(text.right+cellWidth+2*inset,minimum,std::max(minimum,60L*cellWidth+2*inset));
|
||||
SetWindowPos(window,nullptr,0,0,width,1,SWP_NOMOVE|SWP_NOZORDER|SWP_NOACTIVATE);
|
||||
layoutFilesNotice(window);
|
||||
FilesModalDialog::fitWidthToOwner(window,minimum,layoutFilesNotice);
|
||||
const auto defaultButton=notice.options.confirm?IDNO:IDOK;
|
||||
SendMessageW(window,DM_SETDEFID,defaultButton,0);
|
||||
SetFocus(GetDlgItem(window,defaultButton));
|
||||
return FALSE;
|
||||
}
|
||||
const auto *notice=reinterpret_cast<const FilesNotice *>(GetWindowLongPtrW(window,DWLP_USER));
|
||||
if(!notice) return FALSE;
|
||||
if(const auto painted=paintCopyDialog(window,message,wp,lp,notice->title.c_str(),
|
||||
notice->warning,notice->warning?std::initializer_list<int>{102}:std::initializer_list<int>{},
|
||||
!notice->warning)) return painted;
|
||||
const bool warning=notice->options.tone==FilesNoticeTone::warning;
|
||||
const bool palette=notice->options.tone==FilesNoticeTone::dialog;
|
||||
if(message==WM_CTLCOLORSTATIC && reinterpret_cast<HWND>(lp)==GetDlgItem(window,100) && !palette)
|
||||
{
|
||||
const auto dc=reinterpret_cast<HDC>(wp);
|
||||
SetTextColor(dc,warning?RGB(255,255,255):RGB(0,0,0));
|
||||
SetBkColor(dc,warning?RGB(128,0,0):RGB(192,192,192));
|
||||
return reinterpret_cast<INT_PTR>(warning?warningBrush:GetStockObject(LTGRAY_BRUSH));
|
||||
}
|
||||
if(const auto painted=paintCopyDialog(window,message,wp,lp,notice->title.c_str(),warning,{102},palette))
|
||||
return painted;
|
||||
const auto command=LOWORD(wp);
|
||||
if(message==WM_COMMAND && (command==IDOK || command==IDCANCEL || command==IDYES || command==IDNO))
|
||||
{
|
||||
if(command==IDYES && notice->remember)
|
||||
*notice->remember=IsDlgButtonChecked(window,101)==BST_CHECKED;
|
||||
if(command==IDYES && notice->options.remember)
|
||||
*notice->options.remember=IsDlgButtonChecked(window,101)==BST_CHECKED;
|
||||
FilesModalDialog::finish(window,command);
|
||||
return TRUE;
|
||||
}
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
INT_PTR showFilesNotice(HWND owner,const wchar_t *text,const wchar_t *title,bool confirm,bool *remember,bool warning)
|
||||
INT_PTR showFilesNotice(HWND owner,const wchar_t *text,const wchar_t *title,FilesNoticeOptions options)
|
||||
{
|
||||
const bool createBrush=warning && !warningBrush;
|
||||
const bool createBrush=options.tone==FilesNoticeTone::warning && !warningBrush;
|
||||
if(createBrush) warningBrush=CreateSolidBrush(RGB(128,0,0));
|
||||
FilesNotice notice{text,std::wstring(L" ")+title+L" ",confirm,remember,warning};
|
||||
const auto result=FilesModalDialog::run(GetModuleHandleW(nullptr),MAKEINTRESOURCEW(warning?609:607),owner,filesNoticeDialog,
|
||||
FilesNotice notice{text,std::wstring(L" ")+title+L" ",options};
|
||||
const auto result=FilesModalDialog::run(GetModuleHandleW(nullptr),MAKEINTRESOURCEW(607),owner,filesNoticeDialog,
|
||||
reinterpret_cast<LPARAM>(¬ice));
|
||||
if(createBrush)
|
||||
{
|
||||
@@ -4092,7 +4173,7 @@ INT_PTR CALLBACK makeFolderDialog(HWND dialog,UINT message,WPARAM wp,LPARAM lp)
|
||||
if(command!=IDOK) return FALSE;
|
||||
const auto error=[&](const wchar_t *text,HWND focus)
|
||||
{
|
||||
showFilesNotice(dialog,text,L"Error",false,nullptr,true);
|
||||
showFilesNotice(dialog,text,L"Error",{.tone=FilesNoticeTone::warning});
|
||||
SetFocus(focus);
|
||||
};
|
||||
const auto entered=dialogFieldText(input);
|
||||
@@ -4307,15 +4388,23 @@ void openMenu(HWND window, int index)
|
||||
rows.push_back(L"Update remote KSwitcher");
|
||||
}
|
||||
}
|
||||
commandActions.clear();
|
||||
if(index==2)
|
||||
{
|
||||
commandActions={0,3,-1,1,2,4,5};
|
||||
if(panels[active].peer) commandActions.insert(commandActions.end(),{-1,6,7});
|
||||
}
|
||||
else
|
||||
for(size_t i=0;i<rows.size();++i) commandActions.push_back(static_cast<int>(i));
|
||||
std::set<wchar_t> used;
|
||||
commandAccessPositions.assign(rows.size(),std::wstring::npos);
|
||||
std::vector<size_t> accessPositions(rows.size(),std::wstring::npos);
|
||||
const auto reserve=[&](size_t row,wchar_t key)
|
||||
{
|
||||
const auto &text=rows[row];
|
||||
for(size_t pos=0;pos<text.size() && text[pos]!=L'\t';++pos)
|
||||
if(towupper(text[pos])==key && used.insert(key).second)
|
||||
{
|
||||
commandAccessPositions[row]=pos;
|
||||
accessPositions[row]=pos;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@@ -4326,14 +4415,19 @@ void openMenu(HWND window, int index)
|
||||
for(size_t i=0;i<rows.size();++i)
|
||||
{
|
||||
const auto &text=rows[i];
|
||||
if(commandAccessPositions[i]==std::wstring::npos)
|
||||
if(accessPositions[i]==std::wstring::npos)
|
||||
for(const auto ch:text)
|
||||
{
|
||||
if(ch==L'\t') break;
|
||||
const auto key=towupper(ch);
|
||||
if(key>=L'A' && key<=L'Z' && reserve(i,static_cast<wchar_t>(key))) break;
|
||||
}
|
||||
SendMessageW(picker,LB_ADDSTRING,0,reinterpret_cast<LPARAM>(text.c_str()));
|
||||
}
|
||||
commandAccessPositions.clear();
|
||||
for(const auto action:commandActions)
|
||||
{
|
||||
commandAccessPositions.push_back(action<0?std::wstring::npos:accessPositions[action]);
|
||||
SendMessageW(picker,LB_ADDSTRING,0,reinterpret_cast<LPARAM>(action<0?L"":rows[action].c_str()));
|
||||
}
|
||||
RECT anchor{}, client{};
|
||||
GetWindowRect(menuItems[index],&anchor);
|
||||
@@ -4344,7 +4438,7 @@ void openMenu(HWND window, int index)
|
||||
const int x=std::clamp(static_cast<int>(anchor.left)+inset,inset,
|
||||
std::max(inset,static_cast<int>(client.right)-width-inset));
|
||||
const int y=anchor.bottom+5;
|
||||
const int height=static_cast<int>(rows.size())*static_cast<int>(SendMessageW(picker,LB_GETITEMHEIGHT,0,0));
|
||||
const int height=static_cast<int>(commandActions.size())*static_cast<int>(SendMessageW(picker,LB_GETITEMHEIGHT,0,0));
|
||||
SetWindowPos(pickerFrame,HWND_TOP,x-inset,y-inset,width+2*inset,height+2*inset,SWP_SHOWWINDOW);
|
||||
SetWindowPos(picker,HWND_TOP,x,y,width,height,SWP_SHOWWINDOW);
|
||||
SendMessageW(picker,LB_SETCURSEL,0,0);
|
||||
@@ -4850,6 +4944,15 @@ LRESULT CALLBACK filesWindow(HWND window, UINT message, WPARAM wp, LPARAM lp)
|
||||
}
|
||||
auto background=item.CtlType==ODT_BUTTON?Footer:item.hwndItem==picker?Dialog:PanelBlue;
|
||||
auto foreground=colors[Text];
|
||||
if(item.hwndItem==picker && commandPicker && item.itemID<commandActions.size() && commandActions[item.itemID]<0)
|
||||
{
|
||||
FillRect(item.hDC,&item.rcItem,teal);
|
||||
const auto y=(item.rcItem.top+item.rcItem.bottom)/2;
|
||||
const RECT line{item.rcItem.left,y,item.rcItem.right,y+1};
|
||||
SetDCBrushColor(item.hDC,colors[Frame]);
|
||||
FillRect(item.hDC,&line,static_cast<HBRUSH>(GetStockObject(DC_BRUSH)));
|
||||
return TRUE;
|
||||
}
|
||||
if(item.hwndItem==picker && !commandPicker && item.itemID==drivePaths.size())
|
||||
{
|
||||
auto row=item.rcItem;
|
||||
|
||||
@@ -306,7 +306,8 @@ void saveTextSchemeDraft(HWND dialog,ks::files::TextSettings &settings,ks::files
|
||||
[&](const auto &scheme){return sameSchemeName(name,scheme.name.c_str());});
|
||||
if(existing!=settings.schemes.end())
|
||||
{
|
||||
if(showFilesNotice(dialog,L"Replace the saved scheme with the current colors?",L"Save color scheme",true)!=IDYES) return;
|
||||
if(showFilesNotice(dialog,L"Replace the saved scheme with the current colors?",L"Save color scheme",
|
||||
{.confirm=true,.tone=FilesNoticeTone::neutral})!=IDYES) return;
|
||||
existing->colors=profile.colors;
|
||||
profile.scheme=existing->name;
|
||||
}
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
#include "platform.h"
|
||||
namespace ks
|
||||
{
|
||||
inline constexpr wchar_t AppVersion[] = L"0.6.89";
|
||||
inline constexpr wchar_t AppVersion[] = L"0.6.90";
|
||||
inline constexpr UINT UpdateMessage = WM_APP + 7;
|
||||
inline constexpr UINT UpdateCheckIntervalMs = 6 * 60 * 60 * 1000;
|
||||
inline constexpr UINT UpdateNotificationRetryMs = 5 * 60 * 1000;
|
||||
|
||||
@@ -113,6 +113,28 @@ function Read-Bounds($Window) {
|
||||
if (![SearchArrowTest]::GetWindowRect($Window,[ref]$bounds)) { throw 'Missing window bounds' }
|
||||
$bounds
|
||||
}
|
||||
function Assert-CompactNotice($Dialog, [int]$DefaultButton) {
|
||||
$text=[SearchArrowTest]::GetDlgItem($Dialog,100)
|
||||
if (([SearchArrowTest]::GetWindowStyle($text,-16).ToInt64() -band 0x00200000) -ne 0) {
|
||||
throw 'A short Files notice has a vertical scrollbar.'
|
||||
}
|
||||
$lines=[SearchArrowTest]::Send($text,0xBA,0,0).ToInt32()
|
||||
$body=Read-Bounds $text
|
||||
$button=[SearchArrowTest]::GetDlgItem($Dialog,$DefaultButton)
|
||||
$footer=Read-Bounds $button
|
||||
$lineHeight=($body.Bottom-$body.Top)/$lines
|
||||
$frame=Read-Bounds $Dialog
|
||||
if (($body.Top-$frame.Top) -gt 2*$lineHeight -or
|
||||
($frame.Bottom-$footer.Bottom) -gt $lineHeight) {
|
||||
throw 'A short Files notice has excess space between its content and frame.'
|
||||
}
|
||||
if (($footer.Top-$body.Bottom) -gt $lineHeight -or $footer.Top -le $body.Bottom) {
|
||||
throw 'A short Files notice has incorrect spacing between the text and the buttons.'
|
||||
}
|
||||
if ([SearchArrowTest]::FocusedControl($Dialog) -ne $button) {
|
||||
throw 'A Files notice has the wrong starting focus.'
|
||||
}
|
||||
}
|
||||
function Assert-FarFieldHeight($Dialog, [int[]]$Ids) {
|
||||
$heights=@(foreach ($id in $Ids) {
|
||||
$bounds=Read-Bounds ([SearchArrowTest]::GetDlgItem($Dialog,$id))
|
||||
@@ -197,6 +219,35 @@ $process = Start-Process -FilePath $copy -PassThru -WindowStyle Hidden
|
||||
try {
|
||||
$previousDpi=[SearchArrowTest]::SetThreadDpiAwarenessContext([IntPtr](-4))
|
||||
$main = Wait-Until { Find-Window 'KSwitcher.Files' $process.Id }
|
||||
Post $main 0x111 132
|
||||
$commandMenu=Wait-Until {
|
||||
$list=[SearchArrowTest]::GetDlgItem($main,121)
|
||||
if ([SearchArrowTest]::IsWindowVisible($list)) { $list }
|
||||
}
|
||||
$count=[SearchArrowTest]::Send($commandMenu,0x18B,0,0).ToInt32()
|
||||
$rows=@(for ($i=0; $i -lt $count; $i++) {
|
||||
$label=[Text.StringBuilder]::new(128)
|
||||
[void][SearchArrowTest]::GetText($commandMenu,0x189,$i,$label)
|
||||
$label.ToString()
|
||||
})
|
||||
if (($rows -join '|') -cne "Refresh`tF2|Folder shortcuts||View`tF3|Edit`tF4|View externally`tAlt+F3|Edit externally`tAlt+F4") {
|
||||
throw 'Commands did not group local actions or showed a LAN-only section.'
|
||||
}
|
||||
Post $commandMenu 0x100 40
|
||||
Post $commandMenu 0x100 40
|
||||
[void](Wait-Until { [SearchArrowTest]::Send($commandMenu,0x188,0,0).ToInt32() -eq 3 })
|
||||
Post $commandMenu 0x100 38
|
||||
[void](Wait-Until { [SearchArrowTest]::Send($commandMenu,0x188,0,0).ToInt32() -eq 1 })
|
||||
$rowHeight=[SearchArrowTest]::Send($commandMenu,0x1A1,0,0).ToInt32()
|
||||
$separatorPoint=((2*$rowHeight+1) -shl 16) -bor 4
|
||||
[void][SearchArrowTest]::Send($commandMenu,0x201,1,$separatorPoint)
|
||||
[void][SearchArrowTest]::Send($commandMenu,0x202,0,$separatorPoint)
|
||||
if ([SearchArrowTest]::Send($commandMenu,0x188,0,0).ToInt32() -ne 1) {
|
||||
throw 'A click selected the Commands separator.'
|
||||
}
|
||||
Post $commandMenu 0x100 27
|
||||
[void](Wait-Until { ![SearchArrowTest]::IsWindowVisible($commandMenu) })
|
||||
Write-Output 'Verified Commands grouping, arrow navigation, and inert separators.'
|
||||
$panel = Wait-Until { $handle = [SearchArrowTest]::GetDlgItem($main,102); if ($handle -ne [IntPtr]::Zero) { $handle } }
|
||||
$pathControl = [SearchArrowTest]::GetDlgItem($main,142)
|
||||
$pathText = [Text.StringBuilder]::new(1024)
|
||||
@@ -244,6 +295,7 @@ try {
|
||||
[void][SearchArrowTest]::SendText($nameField,0xC,0,$newFolder)
|
||||
Post $makeDialog 0x111 1
|
||||
$folderError=Wait-Until { Find-Window '#32770' $process.Id 100 }
|
||||
Assert-CompactNotice $folderError 1
|
||||
Post $folderError 0x111 1
|
||||
[void](Wait-Until {
|
||||
![SearchArrowTest]::IsWindow($folderError) -and
|
||||
@@ -477,6 +529,7 @@ try {
|
||||
$notice=[Text.StringBuilder]::new(256)
|
||||
[void][SearchArrowTest]::GetText([SearchArrowTest]::GetDlgItem($existing,100),0xD,$notice.Capacity,$notice)
|
||||
if (!$notice.ToString().Contains('already open')) { throw 'Reopening did not offer to switch to the editor.' }
|
||||
Assert-CompactNotice $existing 7
|
||||
Post $existing 0x111 6
|
||||
[void](Wait-Until { ![SearchArrowTest]::IsWindow($existing) })
|
||||
if (Find-Window 'KSwitcher.Files.Text' $process.Id 0 $editor) { throw 'Duplicate editor opened.' }
|
||||
|
||||
Reference in new issue
Block a user