Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f580892ca9 | ||
|
|
372c4cb8bd |
No files matched your search
+13
@@ -2,6 +2,19 @@
|
||||
|
||||
Far Manager є зразком для оформлення файлового менеджера. Основне вікно використовує системний заголовок Windows. Панелі, меню й діалоги операцій використовують моноширинний шрифт менеджера та оформлення в стилі Far.
|
||||
|
||||
## Скорочення
|
||||
|
||||
- Короткий 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
|
||||
|
||||
- Options у головному меню одразу відкриває стандартний діалог Windows. Ліворуч є пошук за назвою категорії чи параметра й дерево результатів. Вибір дочірнього вузла відкриває лише його редактор праворуч. Пошук за назвою окремої властивості відкриває вузол, що її містить. Перемикання вузлів зберігає незастосовані зміни.
|
||||
- File panel modes має вузли Panel layout, Wide panel columns і Status line columns. У Colors кожен колір має власний вузол. В Appearance шрифт редагується у вузлі Font. Діалог змінює розмір від розміру, який вміщує всі параметри. Ширина лівої навігації стала, дерево росте по висоті, а права область використовує додатковий простір. OK, Cancel та Apply лишаються внизу праворуч.
|
||||
- Reset selected to defaults скидає вибраний рівень дерева. Корінь All settings скидає всі параметри, навіть коли пошук приховує частину дерева. У редакторах із кількома параметрами Reset property скидає лише вибрану властивість. Скидання змінює чернетку до Apply або OK. Стандартний Panel background — `#000080`.
|
||||
- Apply доступна, коли чернетка відрізняється від застосованих налаштувань. Після Apply або повернення всіх значень до застосованих вона вимикається. Apply одразу зберігає зміни й перемальовує панелі за відкритим діалогом. OK робить те саме й закриває діалог. Cancel і Escape відкидають зміни після останнього Apply та повертають фокус до панелі.
|
||||
|
||||
## Панелі
|
||||
|
||||
- Меню Change drive показує літеру, тип, загальний обсяг і вільне місце. Відкриття меню запускає нове LAN-виявлення. Секція KSwitcher LAN показує збережені пари, зокрема офлайн. Після кожного відкриття меню асинхронно запитує диски й показує їх під назвою відповідного ПК. Поки триває запит, видно стан завантаження. Якщо запит не вдався, видно помилку. Повторне відкриття запускає новий запит. Кожна панель має власний диск і шлях, а в заголовку віддаленої панелі перед шляхом показано назву ПК.
|
||||
|
||||
+31
-16
@@ -1,26 +1,41 @@
|
||||
#include <windows.h>
|
||||
#include <commctrl.h>
|
||||
|
||||
1 ICON "kswitcher-files.ico"
|
||||
1 RT_MANIFEST "app.manifest"
|
||||
|
||||
101 DIALOGEX 0, 0, 256, 141
|
||||
STYLE DS_SETFONT | DS_MODALFRAME | DS_CENTER | WS_POPUP | WS_CAPTION | WS_SYSMENU
|
||||
CAPTION "Wide panel mode"
|
||||
101 DIALOGEX 0, 0, 397, 228
|
||||
STYLE DS_SETFONT | DS_MODALFRAME | DS_CENTER | WS_POPUP | WS_CAPTION | WS_SYSMENU | WS_THICKFRAME | WS_CLIPCHILDREN
|
||||
CAPTION "KSwitcher Files Options"
|
||||
FONT 9, "Segoe UI"
|
||||
BEGIN
|
||||
GROUPBOX "Panel columns — Name, Size", -1, 8, 6, 240, 59
|
||||
LTEXT "Size column &width (6-16 characters)", -1, 17, 22, 166, 10
|
||||
EDITTEXT 201, 196, 18, 41, 14, ES_NUMBER | ES_AUTOHSCROLL
|
||||
AUTOCHECKBOX "&Align file extensions", 202, 17, 42, 104, 12
|
||||
AUTOCHECKBOX "Align &folder extensions", 204, 130, 42, 108, 12
|
||||
GROUPBOX "Status line columns", -1, 8, 72, 240, 39
|
||||
AUTOCHECKBOX "Show &details", 203, 17, 83, 94, 11
|
||||
AUTOCHECKBOX "Name", 205, 17, 96, 47, 11
|
||||
AUTOCHECKBOX "Size", 206, 72, 96, 42, 11
|
||||
AUTOCHECKBOX "Date", 207, 128, 96, 45, 11
|
||||
AUTOCHECKBOX "Time", 208, 185, 96, 45, 11
|
||||
DEFPUSHBUTTON "OK", IDOK, 139, 119, 50, 14
|
||||
PUSHBUTTON "Cancel", IDCANCEL, 196, 119, 50, 14
|
||||
LTEXT "&Search settings", -1, 8, 8, 113, 10
|
||||
EDITTEXT 270, 8, 21, 113, 14, ES_AUTOHSCROLL
|
||||
CONTROL "", 271, "SysTreeView32", WS_TABSTOP | WS_BORDER | TVS_HASLINES | TVS_LINESATROOT | TVS_HASBUTTONS | TVS_SHOWSELALWAYS, 8, 40, 113, 158
|
||||
LTEXT "", 272, 131, 8, 253, 12
|
||||
AUTOCHECKBOX "Left panel: &Wide", 273, 132, 27, 115, 12
|
||||
AUTOCHECKBOX "Right panel: W&ide", 274, 251, 27, 125, 12
|
||||
GROUPBOX "Wide panel columns", 275, 129, 27, 259, 59
|
||||
LTEXT "Size column &width (6–16 characters)", 276, 138, 44, 175, 10
|
||||
EDITTEXT 201, 335, 41, 43, 14, ES_NUMBER | ES_AUTOHSCROLL
|
||||
AUTOCHECKBOX "&Align file extensions", 202, 138, 64, 108, 12
|
||||
AUTOCHECKBOX "Align &folder extensions", 204, 252, 64, 125, 12
|
||||
GROUPBOX "Status line columns", 277, 129, 27, 259, 46
|
||||
AUTOCHECKBOX "Show &details", 203, 138, 40, 100, 12
|
||||
AUTOCHECKBOX "Name", 205, 138, 55, 47, 12
|
||||
AUTOCHECKBOX "Size", 206, 196, 55, 42, 12
|
||||
AUTOCHECKBOX "Date", 207, 254, 55, 45, 12
|
||||
AUTOCHECKBOX "Time", 208, 312, 55, 45, 12
|
||||
PUSHBUTTON "Change &color…", 279, 131, 48, 82, 15
|
||||
LTEXT "", 280, 222, 50, 155, 12
|
||||
LTEXT "", 281, 131, 30, 254, 14
|
||||
PUSHBUTTON "Change &font…", 282, 131, 52, 82, 15
|
||||
PUSHBUTTON "Reset selected to defaults", 284, 131, 163, 125, 15
|
||||
COMBOBOX 285, 131, 183, 139, 95, CBS_DROPDOWNLIST | WS_TABSTOP | WS_VSCROLL
|
||||
PUSHBUTTON "Reset property", 286, 278, 183, 108, 15
|
||||
DEFPUSHBUTTON "OK", IDOK, 222, 207, 50, 14
|
||||
PUSHBUTTON "Cancel", IDCANCEL, 280, 207, 50, 14
|
||||
PUSHBUTTON "&Apply", 283, 338, 207, 50, 14
|
||||
END
|
||||
|
||||
102 DIALOGEX 0, 0, 350, 114
|
||||
|
||||
+57
-16
@@ -141,6 +141,8 @@ ULONG_PTR snippetReplayMarker()
|
||||
static const char marker{};
|
||||
return reinterpret_cast<ULONG_PTR>(&marker);
|
||||
}
|
||||
constexpr int OpenFiles = 25, FilesHotkeyFirst = 0x4B10, FilesHotkeySecond = 0x4B11;
|
||||
|
||||
std::vector<std::pair<int, std::wstring>> shortcuts(const Settings &s)
|
||||
{
|
||||
std::vector<std::pair<int, std::wstring>> out{{Toggle, s.toggle},
|
||||
@@ -681,6 +683,7 @@ App::App(HINSTANCE h, bool test, const std::filesystem::path &dataRoot, HWND ser
|
||||
}
|
||||
App::~App()
|
||||
{
|
||||
if (filesHotkeyId) UnregisterHotKey(window, filesHotkeyId);
|
||||
if (snippetPopup) DestroyWindow(snippetPopup);
|
||||
lanService.reset();
|
||||
stopBrowserChatMonitor();
|
||||
@@ -854,6 +857,19 @@ bool App::registerHotkeys(const Settings &s, bool rollback)
|
||||
registrationWarning = L"Keyboard interception could not start.";
|
||||
return false;
|
||||
}
|
||||
const auto files = Hotkey::parse(s.filesHotkey.value_or(L"")).value_or(Hotkey{});
|
||||
if (files != registeredFilesHotkey)
|
||||
{
|
||||
const int next = filesHotkeyId == FilesHotkeyFirst ? FilesHotkeySecond : FilesHotkeyFirst;
|
||||
if (files.key && !RegisterHotKey(window, next, files.modifiers | MOD_NOREPEAT, files.key))
|
||||
{
|
||||
if (!rollback) registrationWarning = L"The KSwitcher Files shortcut is unavailable. Choose another shortcut.";
|
||||
return false;
|
||||
}
|
||||
if (filesHotkeyId) UnregisterHotKey(window, filesHotkeyId);
|
||||
filesHotkeyId = files.key ? next : 0;
|
||||
registeredFilesHotkey = files;
|
||||
}
|
||||
hookBindings = std::move(bindings);
|
||||
return true;
|
||||
}
|
||||
@@ -1022,6 +1038,7 @@ void App::setHotkeyCapture(HWND field)
|
||||
}
|
||||
bool App::isHotkey(DWORD key, UINT mods)
|
||||
{
|
||||
if (registeredFilesHotkey.key && registeredFilesHotkey.key == key && registeredFilesHotkey.modifiers == mods) return true;
|
||||
for (auto &[id, text] : shortcuts(settings))
|
||||
{
|
||||
auto h = Hotkey::parse(text);
|
||||
@@ -1964,6 +1981,20 @@ void App::dispatch(int command)
|
||||
try
|
||||
{
|
||||
if (command == 23) { checkUpdate(true); return; }
|
||||
if (command == OpenFiles)
|
||||
{
|
||||
auto path = std::filesystem::path(executable()).parent_path() / L"KSwitcher.Files.exe";
|
||||
auto args = L"\"" + path.wstring() + L"\"";
|
||||
STARTUPINFOW startup{sizeof(startup)};
|
||||
PROCESS_INFORMATION process{};
|
||||
if (!CreateProcessW(path.c_str(), args.data(), nullptr, nullptr, FALSE, 0, nullptr,
|
||||
path.parent_path().c_str(), &startup, &process))
|
||||
throw std::runtime_error(utf8(lastError(L"Cannot start KSwitcher Files")));
|
||||
AllowSetForegroundWindow(process.dwProcessId);
|
||||
CloseHandle(process.hThread);
|
||||
CloseHandle(process.hProcess);
|
||||
return;
|
||||
}
|
||||
if (command == Screenshot)
|
||||
{
|
||||
if (!busy) { resetInput(); launchScreenshot(*this); }
|
||||
@@ -2769,7 +2800,7 @@ void App::trayMenu()
|
||||
AppendMenuW(menu, MF_STRING, 10, L"Clipboard &history");
|
||||
AppendMenuW(menu, MF_STRING, 24, L"&Capture screen region…");
|
||||
AppendMenuW(menu, MF_STRING, 11, L"&Settings…");
|
||||
AppendMenuW(menu, MF_STRING, 25, L"&Files…");
|
||||
AppendMenuW(menu, MF_STRING, OpenFiles, L"&Files…");
|
||||
SetMenuDefaultItem(menu, 11, FALSE);
|
||||
AppendMenuW(menu, MF_STRING, 12, L"S&nippets…");
|
||||
AppendMenuW(menu, MF_STRING, 13, L"&Keyboard shortcuts…");
|
||||
@@ -2822,20 +2853,9 @@ void App::trayMenu()
|
||||
}
|
||||
switch (cmd)
|
||||
{
|
||||
case 25:
|
||||
{
|
||||
auto path = std::filesystem::path(executable()).parent_path() / L"KSwitcher.Files.exe";
|
||||
auto command = L"\"" + path.wstring() + L"\"";
|
||||
STARTUPINFOW startup{sizeof(startup)};
|
||||
PROCESS_INFORMATION process{};
|
||||
if (!CreateProcessW(path.c_str(), command.data(), nullptr, nullptr, FALSE, 0, nullptr,
|
||||
path.parent_path().c_str(), &startup, &process))
|
||||
throw std::runtime_error(utf8(lastError(L"Cannot start KSwitcher Files")));
|
||||
AllowSetForegroundWindow(process.dwProcessId);
|
||||
CloseHandle(process.hThread);
|
||||
CloseHandle(process.hProcess);
|
||||
case OpenFiles:
|
||||
dispatch(OpenFiles);
|
||||
break;
|
||||
}
|
||||
case 24:
|
||||
dispatch(Screenshot);
|
||||
break;
|
||||
@@ -3015,7 +3035,11 @@ LRESULT CALLBACK App::windowProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp)
|
||||
return 0;
|
||||
}
|
||||
case WM_HOTKEY:
|
||||
app->dispatch((int)wp);
|
||||
if (app->filesHotkeyId && wp == static_cast<WPARAM>(app->filesHotkeyId))
|
||||
{
|
||||
app->dispatch(OpenFiles);
|
||||
}
|
||||
else if (wp != FilesHotkeyFirst && wp != FilesHotkeySecond) app->dispatch((int)wp);
|
||||
return 0;
|
||||
case MouseActionMessage:
|
||||
app->dispatchMouseActions();
|
||||
@@ -3341,7 +3365,24 @@ int App::execute()
|
||||
}
|
||||
keyboardHook = SetWindowsHookExW(WH_KEYBOARD_LL, keyProc, instance, 0);
|
||||
mouseHook = SetWindowsHookExW(WH_MOUSE_LL, mouseProc, instance, 0);
|
||||
registerHotkeys(settings);
|
||||
const bool useDefaultFilesHotkey = !settings.filesHotkey;
|
||||
if (useDefaultFilesHotkey)
|
||||
{
|
||||
const auto fallback = *Hotkey::parse(L"Ctrl+Alt+F");
|
||||
const auto bindings = shortcuts(settings);
|
||||
const bool conflict = std::any_of(bindings.begin(), bindings.end(), [&](const auto &binding)
|
||||
{
|
||||
return Hotkey::parse(binding.second) == fallback;
|
||||
});
|
||||
settings.filesHotkey = conflict ? L"" : L"Ctrl+Alt+F";
|
||||
}
|
||||
if (!registerHotkeys(settings) && settings.filesHotkey && !settings.filesHotkey->empty())
|
||||
{
|
||||
if (useDefaultFilesHotkey) settings.filesHotkey = L"";
|
||||
auto fallback = settings;
|
||||
fallback.filesHotkey = L"";
|
||||
registerHotkeys(fallback, true);
|
||||
}
|
||||
if (!keyboardHook || !mouseHook)
|
||||
report(L"Input tracking could not start. Automatic recent-text conversion is unavailable.");
|
||||
AddClipboardFormatListener(window);
|
||||
|
||||
@@ -169,6 +169,8 @@ class App
|
||||
HWND previousAttemptWindow{};
|
||||
int previousAttemptCommand{}, repeatAttempts{};
|
||||
std::vector<std::pair<int, Hotkey>> hookBindings;
|
||||
Hotkey registeredFilesHotkey{};
|
||||
int filesHotkeyId{};
|
||||
HotkeyHook hotkeyHook;
|
||||
ChatSwitchHook chatSwitchHook;
|
||||
HWND chatWindow{};
|
||||
|
||||
@@ -97,6 +97,7 @@ struct Settings
|
||||
std::wstring toggle = L"Ctrl+Alt+Shift+Space", casing = L"Ctrl+Alt+Shift+C",
|
||||
translit = L"Ctrl+Alt+Shift+T", undo = L"Ctrl+Alt+Shift+Z", clipboard = L"Win+V",
|
||||
screenshot = L"Win+Shift+S";
|
||||
std::optional<std::wstring> filesHotkey;
|
||||
std::vector<std::wstring> cycle, direct, directIds;
|
||||
// Exact application + visible chat name; an absent entry uses the global cycle.
|
||||
std::map<std::wstring, std::vector<std::wstring>> chatCycles;
|
||||
|
||||
+192
-142
@@ -280,13 +280,17 @@ bool dragging{};
|
||||
bool rememberElevationApproval{};
|
||||
HWND picker{}, pickerFrame{}, menuBar{};
|
||||
HWND menuItems[5]{};
|
||||
constexpr std::wstring_view menuAccessKeys=L"LFCOR";
|
||||
std::vector<size_t> commandAccessPositions;
|
||||
int menuIndex{};
|
||||
bool commandPicker{};
|
||||
int optionsPage{};
|
||||
int sizeWidth=6;
|
||||
bool alignExtensions=true, showDetails=true;
|
||||
bool alignFolders=false;
|
||||
DWORD statusFields=15;
|
||||
constexpr int defaultSizeWidth=6, defaultFontPoints=110;
|
||||
constexpr bool defaultWide=false, defaultAlignExtensions=true, defaultShowDetails=true, defaultAlignFolders=false;
|
||||
constexpr DWORD defaultStatusFields=15;
|
||||
int sizeWidth=defaultSizeWidth;
|
||||
bool alignExtensions=defaultAlignExtensions, showDetails=defaultShowDetails;
|
||||
bool alignFolders=defaultAlignFolders;
|
||||
DWORD statusFields=defaultStatusFields;
|
||||
std::vector<fs::path> drivePaths;
|
||||
int driveHover=-1;
|
||||
std::array<LONG,3> driveColumnStarts{};
|
||||
@@ -309,8 +313,9 @@ int drivePickerWidth{};
|
||||
int pickerPanel{};
|
||||
bool altAlone{};
|
||||
enum Color { PanelBlue, Text, Frame, Dialog, Selection, Footer, PanelCursor, ColorCount };
|
||||
COLORREF colors[]{RGB(0,0,128), RGB(230,255,255), RGB(0,255,255),
|
||||
constexpr std::array<COLORREF,ColorCount> defaultColors{RGB(0,0,128), RGB(230,255,255), RGB(0,255,255),
|
||||
RGB(0,128,128), RGB(0,0,0), RGB(0,128,128), RGB(0,128,128)};
|
||||
auto colors=defaultColors;
|
||||
constexpr auto schemeKey = L"Software\\KSwitcher\\Files";
|
||||
constexpr auto placementPreference = L"WindowPlacement";
|
||||
|
||||
@@ -345,9 +350,9 @@ void savePlacement(HWND window)
|
||||
&placement,sizeof(placement));
|
||||
}
|
||||
void chooseDrive(int index, bool fromMenu = false);
|
||||
void openMenu(HWND window, int index, int page = 0);
|
||||
void openMenu(HWND window, int index);
|
||||
void showFileDetails(const Panel &panel);
|
||||
void editWideMode(HWND owner);
|
||||
void editOptions(HWND owner);
|
||||
void layout(HWND window);
|
||||
|
||||
DWORD preference(const wchar_t *name, DWORD fallback)
|
||||
@@ -401,6 +406,16 @@ BOOL CALLBACK updateDialogFont(HWND window,LPARAM parameter)
|
||||
HBRUSH blue{}, teal{}, terminalBackground{};
|
||||
void layout(HWND window);
|
||||
|
||||
LOGFONTW defaultFilesFont()
|
||||
{
|
||||
LOGFONTW font{};
|
||||
font.lfWeight=FW_NORMAL;
|
||||
font.lfCharSet=DEFAULT_CHARSET;
|
||||
font.lfQuality=CLEARTYPE_QUALITY;
|
||||
wcscpy_s(font.lfFaceName,L"Consolas");
|
||||
return font;
|
||||
}
|
||||
|
||||
bool applyFont(HWND owner, LOGFONTW font, int points)
|
||||
{
|
||||
const auto dpi=GetDpiForWindow(owner);
|
||||
@@ -443,22 +458,19 @@ bool applyFont(HWND owner, LOGFONTW font, int points)
|
||||
return true;
|
||||
}
|
||||
|
||||
void chooseDisplayFont(HWND owner)
|
||||
bool chooseDisplayFont(HWND owner,LOGFONTW &font,int &points)
|
||||
{
|
||||
auto font=displayFont;
|
||||
CHOOSEFONTW dialog{};
|
||||
dialog.lStructSize=sizeof(dialog);
|
||||
dialog.hwndOwner=owner;
|
||||
dialog.lpLogFont=&font;
|
||||
dialog.iPointSize=points;
|
||||
dialog.Flags=CF_SCREENFONTS|CF_INITTOLOGFONTSTRUCT|CF_LIMITSIZE;
|
||||
dialog.nSizeMin=8;
|
||||
dialog.nSizeMax=24;
|
||||
if(!ChooseFontW(&dialog)) return;
|
||||
if(!applyFont(owner,font,dialog.iPointSize)) return;
|
||||
savePreference(L"FontPoints",fontPoints);
|
||||
RegSetKeyValueW(HKEY_CURRENT_USER,schemeKey,L"DisplayFont",REG_BINARY,&displayFont,sizeof(displayFont));
|
||||
layout(owner);
|
||||
RedrawWindow(owner,nullptr,nullptr,RDW_INVALIDATE|RDW_ALLCHILDREN);
|
||||
if(!ChooseFontW(&dialog)) return false;
|
||||
points=dialog.iPointSize;
|
||||
return true;
|
||||
}
|
||||
|
||||
void refreshColors()
|
||||
@@ -482,24 +494,15 @@ void loadColors()
|
||||
refreshColors();
|
||||
}
|
||||
|
||||
void changeColor(HWND owner, int index)
|
||||
void choosePanelColor(HWND owner,COLORREF &color,COLORREF *custom)
|
||||
{
|
||||
static COLORREF custom[16]{};
|
||||
CHOOSECOLORW dialog{};
|
||||
dialog.lStructSize=sizeof(dialog);
|
||||
dialog.hwndOwner=owner;
|
||||
dialog.rgbResult=colors[index];
|
||||
dialog.rgbResult=color;
|
||||
dialog.lpCustColors=custom;
|
||||
dialog.Flags=CC_RGBINIT | CC_FULLOPEN;
|
||||
if (!ChooseColorW(&dialog)) return;
|
||||
colors[index]=dialog.rgbResult;
|
||||
auto name=std::to_wstring(index);
|
||||
const auto result=RegSetKeyValueW(HKEY_CURRENT_USER, schemeKey, name.c_str(), REG_DWORD,
|
||||
&colors[index], sizeof(COLORREF));
|
||||
refreshColors();
|
||||
RedrawWindow(owner,nullptr,nullptr,RDW_INVALIDATE|RDW_ERASE|RDW_ALLCHILDREN);
|
||||
if(result!=ERROR_SUCCESS)
|
||||
MessageBoxW(owner,L"Could not save the color.",L"KSwitcher Files",MB_OK|MB_ICONWARNING);
|
||||
if(ChooseColorW(&dialog)) color=dialog.rgbResult;
|
||||
}
|
||||
|
||||
void paintText(HDC dc, RECT rect, const wchar_t *text, COLORREF foreground, COLORREF background, bool right=false)
|
||||
@@ -512,6 +515,18 @@ void paintText(HDC dc, RECT rect, const wchar_t *text, COLORREF foreground, COLO
|
||||
SetTextAlign(dc,alignment);
|
||||
}
|
||||
|
||||
void paintMenuLabel(HDC dc,RECT rect,const std::wstring &text,size_t access,COLORREF foreground,COLORREF background)
|
||||
{
|
||||
paintText(dc,rect,text.c_str(),foreground,background);
|
||||
if(access>=text.size()) return;
|
||||
SIZE prefix{};
|
||||
GetTextExtentPoint32W(dc,text.c_str(),static_cast<int>(access),&prefix);
|
||||
const auto mode=SetBkMode(dc,TRANSPARENT);
|
||||
SetTextColor(dc,RGB(255,255,0));
|
||||
ExtTextOutW(dc,rect.left+5+prefix.cx,rect.top+3,ETO_CLIPPED,&rect,text.c_str()+access,1,nullptr);
|
||||
SetBkMode(dc,mode);
|
||||
}
|
||||
|
||||
void paintDriveCaption(HDC dc,RECT row,const wchar_t *caption)
|
||||
{
|
||||
const int saved=SaveDC(dc);
|
||||
@@ -1020,16 +1035,6 @@ void selectDrive()
|
||||
if(row==2) chooseDrive(index);
|
||||
else setMode(index,row==1);
|
||||
}
|
||||
else if(menuIndex==3)
|
||||
{
|
||||
if(optionsPage==0)
|
||||
{
|
||||
if(row==2) chooseDisplayFont(owner);
|
||||
else openMenu(owner,3,row==0?1:2);
|
||||
}
|
||||
else if(optionsPage==1) editWideMode(owner);
|
||||
else if(optionsPage==2) changeColor(owner,static_cast<int>(row));
|
||||
}
|
||||
else
|
||||
{
|
||||
const int command=menuIndex==1?113+static_cast<int>(row):110+static_cast<int>(row);
|
||||
@@ -1080,9 +1085,7 @@ LRESULT CALLBACK pickerKeys(HWND window, UINT msg, WPARAM wp, LPARAM lp, UINT_PT
|
||||
}
|
||||
if(msg==WM_KEYDOWN && wp==VK_ESCAPE)
|
||||
{
|
||||
if(commandPicker && menuIndex==3 && optionsPage)
|
||||
openMenu(GetParent(window),3);
|
||||
else hidePicker();
|
||||
hidePicker();
|
||||
return 0;
|
||||
}
|
||||
if(msg==WM_KEYDOWN && wp==VK_RETURN){selectDrive();return 0;}
|
||||
@@ -2922,69 +2925,18 @@ LRESULT CALLBACK panelKeys(HWND window, UINT message, WPARAM wp, LPARAM lp, UINT
|
||||
return result;
|
||||
}
|
||||
|
||||
INT_PTR CALLBACK wideModeDialog(HWND dialog, UINT message, WPARAM wp, LPARAM)
|
||||
{
|
||||
constexpr int widthField=201, extensionsField=202, detailsField=203;
|
||||
if(message==WM_INITDIALOG)
|
||||
{
|
||||
installDialogButtonKeys(dialog);
|
||||
SetDlgItemInt(dialog,widthField,sizeWidth,FALSE);
|
||||
SendDlgItemMessageW(dialog,widthField,EM_SETLIMITTEXT,2,0);
|
||||
CheckDlgButton(dialog,extensionsField,alignExtensions?BST_CHECKED:BST_UNCHECKED);
|
||||
CheckDlgButton(dialog,detailsField,showDetails?BST_CHECKED:BST_UNCHECKED);
|
||||
CheckDlgButton(dialog,204,alignFolders?BST_CHECKED:BST_UNCHECKED);
|
||||
for(int i=0;i<4;++i) CheckDlgButton(dialog,205+i,(statusFields&(1u<<i))?BST_CHECKED:BST_UNCHECKED);
|
||||
return TRUE;
|
||||
}
|
||||
if(message==WM_CLOSE || (message==WM_COMMAND && LOWORD(wp)==IDCANCEL))
|
||||
{
|
||||
EndDialog(dialog,IDCANCEL);
|
||||
return TRUE;
|
||||
}
|
||||
if(message!=WM_COMMAND || LOWORD(wp)!=IDOK) return FALSE;
|
||||
BOOL valid{};
|
||||
auto width=GetDlgItemInt(dialog,widthField,&valid,FALSE);
|
||||
if(!valid || width<6 || width>16)
|
||||
{
|
||||
MessageBoxW(dialog,L"Enter a width from 6 to 16 characters.",L"Wide panel mode",MB_OK|MB_ICONWARNING);
|
||||
SetFocus(GetDlgItem(dialog,widthField));
|
||||
SendDlgItemMessageW(dialog,widthField,EM_SETSEL,0,-1);
|
||||
return TRUE;
|
||||
}
|
||||
sizeWidth=static_cast<int>(width);
|
||||
alignExtensions=IsDlgButtonChecked(dialog,extensionsField)==BST_CHECKED;
|
||||
showDetails=IsDlgButtonChecked(dialog,detailsField)==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);
|
||||
EndDialog(dialog,IDOK);
|
||||
return TRUE;
|
||||
}
|
||||
#include "files_options.inl"
|
||||
|
||||
void editWideMode(HWND owner)
|
||||
void openMenu(HWND window, int index)
|
||||
{
|
||||
const auto result=DialogBoxParamW(GetModuleHandleW(nullptr),MAKEINTRESOURCEW(101),owner,wideModeDialog,0);
|
||||
if(result==IDOK)
|
||||
if(index==3)
|
||||
{
|
||||
for(auto &panel:panels)
|
||||
{
|
||||
InvalidateRect(panel.list,nullptr,TRUE);
|
||||
InvalidateRect(panel.heading,nullptr,TRUE);
|
||||
}
|
||||
showFileDetails(panels[active]);
|
||||
hidePicker();
|
||||
showMenu(false);
|
||||
editOptions(window);
|
||||
return;
|
||||
}
|
||||
else if(result==-1) setStatus(L"Could not open Wide panel settings.");
|
||||
}
|
||||
|
||||
void openMenu(HWND window, int index, int page)
|
||||
{
|
||||
menuIndex=index;
|
||||
optionsPage=page;
|
||||
commandPicker=true;
|
||||
SendMessageW(picker,LB_RESETCONTENT,0,0);
|
||||
std::vector<std::wstring> rows;
|
||||
@@ -2997,12 +2949,34 @@ void openMenu(HWND window, int index, int page)
|
||||
}
|
||||
else if(index==1) rows={L"Copy\tF5",L"Move\tF6",L"Exit\tF10"};
|
||||
else if(index==2) rows={L"Refresh\tF2",L"View\tF3",L"Edit\tF4"};
|
||||
else if(page==0) rows={L"File panel modes",L"Colors",L"Appearance…"};
|
||||
else if(page==1) rows={L"Wide"};
|
||||
else if(page==2) rows={L"Panel background",L"Text",L"Frame",L"Popup background",
|
||||
L"Menu selection",L"Command bar",L"File cursor"};
|
||||
for(const auto &row:rows)
|
||||
SendMessageW(picker,LB_ADDSTRING,0,reinterpret_cast<LPARAM>(row.c_str()));
|
||||
std::set<wchar_t> used;
|
||||
commandAccessPositions.assign(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;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
if(index==0 || index==4) {reserve(0,L'B');reserve(1,L'W');reserve(2,L'C');}
|
||||
if(index==1) {reserve(0,L'C');reserve(2,L'X');}
|
||||
if(index==2) {reserve(1,L'V');reserve(2,L'E');}
|
||||
for(size_t i=0;i<rows.size();++i)
|
||||
{
|
||||
const auto &text=rows[i];
|
||||
if(commandAccessPositions[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()));
|
||||
}
|
||||
RECT anchor{}, client{};
|
||||
GetWindowRect(menuItems[index],&anchor);
|
||||
MapWindowPoints(nullptr,window,reinterpret_cast<POINT*>(&anchor),2);
|
||||
@@ -3018,10 +2992,59 @@ void openMenu(HWND window, int index, int page)
|
||||
showMenu(true);
|
||||
}
|
||||
|
||||
void toggleFilesMenu()
|
||||
{
|
||||
const bool visible=IsWindowVisible(menuBar);
|
||||
hidePicker();
|
||||
if(!visible) menuIndex=active==0?0:4;
|
||||
showMenu(!visible);
|
||||
if(visible) SetFocus(panels[active].list);
|
||||
}
|
||||
|
||||
bool handleFilesMenuKey(HWND window,UINT message,WPARAM key,LPARAM flags,bool modified)
|
||||
{
|
||||
const bool repeated=(flags & (1LL << 30))!=0;
|
||||
if(message==WM_SYSKEYDOWN && key==VK_MENU)
|
||||
{
|
||||
if(!repeated) altAlone=!modified;
|
||||
return !modified;
|
||||
}
|
||||
if((message==WM_SYSKEYUP || message==WM_KEYUP) && key==VK_MENU)
|
||||
{
|
||||
if(std::exchange(altAlone,false))
|
||||
{
|
||||
toggleFilesMenu();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if(message!=WM_KEYDOWN && message!=WM_SYSKEYDOWN) return false;
|
||||
altAlone=false;
|
||||
if(modified) return false;
|
||||
if(message==WM_KEYDOWN && key==VK_F9)
|
||||
{
|
||||
if(!repeated) toggleFilesMenu();
|
||||
return true;
|
||||
}
|
||||
if(message==WM_SYSKEYDOWN && (flags & (1LL << 29)) && key>=L'A' && key<=L'Z')
|
||||
{
|
||||
const auto category=menuAccessKeys.find(static_cast<wchar_t>(key));
|
||||
if(category!=menuAccessKeys.npos)
|
||||
{
|
||||
if(!repeated) openMenu(window,static_cast<int>(category));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
LRESULT CALLBACK filesWindow(HWND window, UINT message, WPARAM wp, LPARAM lp)
|
||||
{
|
||||
switch (message)
|
||||
{
|
||||
case WM_ACTIVATEAPP:
|
||||
if(!wp) altAlone=false;
|
||||
break;
|
||||
case WM_COPYDATA:
|
||||
{
|
||||
const auto *reply=reinterpret_cast<const COPYDATASTRUCT*>(lp);
|
||||
@@ -3058,22 +3081,18 @@ LRESULT CALLBACK filesWindow(HWND window, UINT message, WPARAM wp, LPARAM lp)
|
||||
if(RegGetValueW(HKEY_CURRENT_USER,schemeKey,L"DisplayFont",RRF_RT_REG_BINARY,nullptr,&font,&bytes)
|
||||
!=ERROR_SUCCESS || bytes!=sizeof(font))
|
||||
{
|
||||
font={};
|
||||
font.lfWeight=FW_NORMAL;
|
||||
font.lfCharSet=DEFAULT_CHARSET;
|
||||
font.lfQuality=CLEARTYPE_QUALITY;
|
||||
wcscpy_s(font.lfFaceName,L"Consolas");
|
||||
font=defaultFilesFont();
|
||||
}
|
||||
font.lfFaceName[LF_FACESIZE-1]=0;
|
||||
if(!applyFont(window,font,static_cast<int>(std::clamp(preference(L"FontPoints",110),80UL,240UL)))) return -1;
|
||||
if(!applyFont(window,font,static_cast<int>(std::clamp(preference(L"FontPoints",defaultFontPoints),80UL,240UL)))) return -1;
|
||||
loadColors();
|
||||
panels[0].wide=preference(L"LeftWide",0)!=0;
|
||||
panels[1].wide=preference(L"RightWide",0)!=0;
|
||||
sizeWidth=static_cast<int>(std::clamp(preference(L"SizeWidth",6),6UL,16UL));
|
||||
alignExtensions=preference(L"AlignExtensions",1)!=0;
|
||||
showDetails=preference(L"ShowDetails",1)!=0;
|
||||
alignFolders=preference(L"AlignFolders",0)!=0;
|
||||
statusFields=preference(L"StatusFields",15)&15;
|
||||
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;
|
||||
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"};
|
||||
@@ -3424,17 +3443,21 @@ LRESULT CALLBACK filesWindow(HWND window, UINT message, WPARAM wp, LPARAM lp)
|
||||
}
|
||||
}
|
||||
auto shortcut=text.find(L'\t');
|
||||
if(item.hwndItem==picker && commandPicker && shortcut!=text.npos)
|
||||
if(item.hwndItem==picker && commandPicker && item.itemID<commandAccessPositions.size())
|
||||
{
|
||||
auto labelRect=item.rcItem, keyRect=item.rcItem;
|
||||
SelectObject(item.hDC,mono);
|
||||
SIZE extent{};
|
||||
const auto key=text.c_str()+shortcut+1;
|
||||
GetTextExtentPoint32W(item.hDC,key,lstrlenW(key),&extent);
|
||||
keyRect.left=std::max(keyRect.left,keyRect.right-extent.cx-16);
|
||||
labelRect.right=keyRect.left;
|
||||
paintText(item.hDC,labelRect,text.substr(0,shortcut).c_str(),colors[Text],colors[background]);
|
||||
paintText(item.hDC,keyRect,key,colors[Text],colors[background],true);
|
||||
if(shortcut!=text.npos)
|
||||
{
|
||||
const auto key=text.c_str()+shortcut+1;
|
||||
GetTextExtentPoint32W(item.hDC,key,lstrlenW(key),&extent);
|
||||
keyRect.left=std::max(keyRect.left,keyRect.right-extent.cx-16);
|
||||
labelRect.right=keyRect.left;
|
||||
paintText(item.hDC,keyRect,key,colors[Text],colors[background],true);
|
||||
}
|
||||
paintMenuLabel(item.hDC,labelRect,text.substr(0,shortcut),commandAccessPositions[item.itemID],
|
||||
colors[Text],colors[background]);
|
||||
}
|
||||
else if(item.hwndItem==picker && !commandPicker && item.itemID<drivePaths.size())
|
||||
{
|
||||
@@ -3487,6 +3510,8 @@ LRESULT CALLBACK filesWindow(HWND window, UINT message, WPARAM wp, LPARAM lp)
|
||||
ETO_OPAQUE|ETO_CLIPPED,&bounds,text.c_str(),lstrlenW(text.c_str()),nullptr);
|
||||
|
||||
}
|
||||
else if(item.CtlID>=130 && item.CtlID<=134)
|
||||
paintMenuLabel(item.hDC,item.rcItem,text,0,foreground,colors[background]);
|
||||
else paintText(item.hDC,item.rcItem,text.c_str(),foreground,colors[background]);
|
||||
if(item.CtlID==102 || item.CtlID==103)
|
||||
{
|
||||
@@ -3621,7 +3646,7 @@ int WINAPI wWinMain(HINSTANCE instance, HINSTANCE, PWSTR, int show)
|
||||
}
|
||||
LocalFree(args);
|
||||
}
|
||||
INITCOMMONCONTROLSEX controls{sizeof(controls), ICC_STANDARD_CLASSES|ICC_PROGRESS_CLASS};
|
||||
INITCOMMONCONTROLSEX controls{sizeof(controls), ICC_STANDARD_CLASSES|ICC_PROGRESS_CLASS|ICC_TREEVIEW_CLASSES};
|
||||
InitCommonControlsEx(&controls);
|
||||
WNDCLASSW type{};
|
||||
type.hInstance = instance;
|
||||
@@ -3662,23 +3687,12 @@ int WINAPI wWinMain(HINSTANCE instance, HINSTANCE, PWSTR, int show)
|
||||
if(terminalVisible || startTerminal(window)) showTerminal(window,!terminalVisible);
|
||||
continue;
|
||||
}
|
||||
if (message.message == WM_SYSKEYDOWN && message.wParam == VK_MENU)
|
||||
{
|
||||
altAlone=true;
|
||||
const bool filesMessage=message.hwnd==window || IsChild(window,message.hwnd);
|
||||
const bool modified=(GetKeyState(VK_CONTROL)&0x8000) || (GetKeyState(VK_SHIFT)&0x8000) ||
|
||||
(GetKeyState(VK_LWIN)&0x8000) || (GetKeyState(VK_RWIN)&0x8000);
|
||||
if(filesMessage && !copyInProgress &&
|
||||
handleFilesMenuKey(window,message.message,message.wParam,message.lParam,modified))
|
||||
continue;
|
||||
}
|
||||
if ((message.message == WM_SYSKEYUP || message.message == WM_KEYUP) && message.wParam == VK_MENU)
|
||||
{
|
||||
if(altAlone)
|
||||
{
|
||||
hidePicker();
|
||||
menuIndex=active==0?0:4;
|
||||
showMenu(true);
|
||||
}
|
||||
altAlone=false;
|
||||
continue;
|
||||
}
|
||||
if(message.message==WM_SYSKEYDOWN || message.message==WM_KEYDOWN) altAlone=false;
|
||||
if((message.message==WM_LBUTTONDOWN || message.message==WM_RBUTTONDOWN ||
|
||||
message.message==WM_NCLBUTTONDOWN) && message.hwnd!=menuBar && message.hwnd!=picker &&
|
||||
std::find(std::begin(menuItems),std::end(menuItems),message.hwnd)==std::end(menuItems))
|
||||
@@ -3686,6 +3700,42 @@ int WINAPI wWinMain(HINSTANCE instance, HINSTANCE, PWSTR, int show)
|
||||
showMenu(false);
|
||||
if(message.hwnd!=picker) hidePicker();
|
||||
}
|
||||
if((message.message==WM_CHAR || message.message==WM_SYSCHAR) && IsWindowVisible(menuBar))
|
||||
{
|
||||
const auto letter=towupper(static_cast<wchar_t>(message.wParam));
|
||||
if(letter>=L'A' && letter<=L'Z')
|
||||
{
|
||||
if(!IsWindowVisible(picker))
|
||||
{
|
||||
const auto category=menuAccessKeys.find(static_cast<wchar_t>(letter));
|
||||
if(category!=menuAccessKeys.npos) openMenu(window,static_cast<int>(category));
|
||||
}
|
||||
else if(commandPicker)
|
||||
{
|
||||
for(size_t row=0;row<commandAccessPositions.size();++row)
|
||||
{
|
||||
const auto pos=commandAccessPositions[row];
|
||||
if(pos==std::wstring::npos) continue;
|
||||
const auto length=SendMessageW(picker,LB_GETTEXTLEN,row,0);
|
||||
if(length<=0 || pos>=static_cast<size_t>(length)) continue;
|
||||
std::wstring label(static_cast<size_t>(length)+1,L'\0');
|
||||
SendMessageW(picker,LB_GETTEXT,row,reinterpret_cast<LPARAM>(label.data()));
|
||||
if(towupper(label[pos])==letter)
|
||||
{
|
||||
SendMessageW(picker,LB_SETCURSEL,row,0);
|
||||
selectDrive();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
TranslateMessage(&message);
|
||||
DispatchMessageW(&message);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if(message.message==WM_KEYDOWN && IsWindowVisible(menuBar) && !IsWindowVisible(picker))
|
||||
{
|
||||
if(message.wParam==VK_ESCAPE){showMenu(false);SetFocus(panels[active].list);continue;}
|
||||
|
||||
@@ -0,0 +1,426 @@
|
||||
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 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 };
|
||||
constexpr const wchar_t *panelSectionNames[]{L"Panel layout",L"Wide panel columns",L"Status line columns"};
|
||||
constexpr int panelLayoutControls[]{273,274};
|
||||
constexpr int wideColumnControls[]{275,276,201,202,204};
|
||||
constexpr int statusColumnControls[]{277,203,205,206,207,208};
|
||||
constexpr int colorOptionControls[]{279,280};
|
||||
constexpr int appearanceControls[]{281,282};
|
||||
|
||||
enum class OptionsAnchor { StretchWidth, StretchHeight, BottomRight };
|
||||
struct OptionsControl
|
||||
{
|
||||
int id;
|
||||
OptionsAnchor anchor;
|
||||
RECT bounds{};
|
||||
};
|
||||
|
||||
struct PanelProperty
|
||||
{
|
||||
int section, control;
|
||||
const wchar_t *name;
|
||||
DWORD defaultValue;
|
||||
};
|
||||
constexpr PanelProperty panelProperties[]{
|
||||
{PanelLayout,273,L"Left panel: Wide",defaultWide},{PanelLayout,274,L"Right panel: Wide",defaultWide},
|
||||
{WideColumns,201,L"Size column width",defaultSizeWidth},
|
||||
{WideColumns,202,L"Align file extensions",defaultAlignExtensions},
|
||||
{WideColumns,204,L"Align folder extensions",defaultAlignFolders},
|
||||
{StatusColumns,203,L"Show details",defaultShowDetails},
|
||||
{StatusColumns,205,L"Name",defaultStatusFields&1},{StatusColumns,206,L"Size",(defaultStatusFields>>1)&1},
|
||||
{StatusColumns,207,L"Date",(defaultStatusFields>>2)&1},{StatusColumns,208,L"Time",(defaultStatusFields>>3)&1}};
|
||||
void resetPanelProperty(HWND dialog,const PanelProperty &property)
|
||||
{
|
||||
if(property.control==201) SetDlgItemInt(dialog,property.control,property.defaultValue,FALSE);
|
||||
else CheckDlgButton(dialog,property.control,property.defaultValue);
|
||||
}
|
||||
|
||||
struct OptionsDraft
|
||||
{
|
||||
LOGFONTW font=displayFont;
|
||||
int points=fontPoints;
|
||||
std::array<COLORREF,ColorCount> palette{};
|
||||
std::array<COLORREF,16> custom{};
|
||||
int page=PanelModes, section=PanelLayout;
|
||||
HTREEITEM wideColumnsNode{};
|
||||
RECT initialClient{};
|
||||
UINT initialDpi=96;
|
||||
OptionsControl controls[8]{
|
||||
{optionsTree,OptionsAnchor::StretchHeight},
|
||||
{optionsTitle,OptionsAnchor::StretchWidth},
|
||||
{275,OptionsAnchor::StretchWidth},{277,OptionsAnchor::StretchWidth},
|
||||
{281,OptionsAnchor::StretchWidth},
|
||||
{IDOK,OptionsAnchor::BottomRight},{IDCANCEL,OptionsAnchor::BottomRight},
|
||||
{optionsApply,OptionsAnchor::BottomRight}};
|
||||
|
||||
};
|
||||
|
||||
bool optionsFontChanged(const OptionsDraft &draft)
|
||||
{
|
||||
auto pending=draft.font, applied=displayFont;
|
||||
// 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 ||
|
||||
memcmp(&pending,&applied,offsetof(LOGFONTW,lfFaceName))!=0 ||
|
||||
_wcsicmp(pending.lfFaceName,applied.lfFaceName)!=0;
|
||||
}
|
||||
|
||||
void updateOptionsApply(HWND dialog,const OptionsDraft &draft)
|
||||
{
|
||||
BOOL valid{};
|
||||
const auto width=GetDlgItemInt(dialog,201,&valid,FALSE);
|
||||
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 || optionsFontChanged(draft);
|
||||
EnableWindow(GetDlgItem(dialog,optionsApply),dirty);
|
||||
}
|
||||
|
||||
LRESULT CALLBACK optionsGroupBackground(HWND group,UINT message,WPARAM wp,LPARAM lp,UINT_PTR,DWORD_PTR)
|
||||
{
|
||||
if(message==WM_ERASEBKGND)
|
||||
{
|
||||
const auto dc=reinterpret_cast<HDC>(wp);
|
||||
RECT client{};
|
||||
GetClientRect(group,&client);
|
||||
FillRect(dc,&client,GetSysColorBrush(COLOR_3DFACE));
|
||||
return TRUE;
|
||||
}
|
||||
return DefSubclassProc(group,message,wp,lp);
|
||||
}
|
||||
|
||||
void layoutOptions(HWND dialog,const OptionsDraft &draft)
|
||||
{
|
||||
const auto dpi=GetDpiForWindow(dialog);
|
||||
const auto scale=[&](LONG value){return MulDiv(value,dpi,draft.initialDpi);};
|
||||
RECT client{};
|
||||
GetClientRect(dialog,&client);
|
||||
const auto dx=std::max(0L,client.right-scale(draft.initialClient.right));
|
||||
const auto dy=std::max(0L,client.bottom-scale(draft.initialClient.bottom));
|
||||
for(const auto &control:draft.controls)
|
||||
{
|
||||
const auto &bounds=control.bounds;
|
||||
auto x=scale(bounds.left),y=scale(bounds.top);
|
||||
auto width=scale(bounds.right)-x,height=scale(bounds.bottom)-y;
|
||||
switch(control.anchor)
|
||||
{
|
||||
case OptionsAnchor::StretchWidth: width+=dx;break;
|
||||
case OptionsAnchor::StretchHeight: height+=dy;break;
|
||||
case OptionsAnchor::BottomRight: x+=dx;y+=dy;break;
|
||||
}
|
||||
SetWindowPos(GetDlgItem(dialog,control.id),nullptr,x,y,width,height,
|
||||
SWP_NOZORDER|SWP_NOACTIVATE|SWP_NOCOPYBITS|SWP_NOREDRAW);
|
||||
}
|
||||
// Paint the final layout together, including the areas vacated by moved controls.
|
||||
RedrawWindow(dialog,nullptr,nullptr,RDW_INVALIDATE|RDW_ERASE|RDW_ALLCHILDREN|RDW_UPDATENOW);
|
||||
}
|
||||
|
||||
void showOptionsPage(HWND dialog,OptionsDraft &draft,int page,int section)
|
||||
{
|
||||
draft.page=page;
|
||||
draft.section=section;
|
||||
const wchar_t *title=L"No matching settings";
|
||||
if(page==allOptions) title=L"All settings";
|
||||
else if(page>=0)
|
||||
{
|
||||
title=optionsTitles[page];
|
||||
if(section>=0)
|
||||
title=page==PanelModes?panelSectionNames[section]:page==PanelColors?colorNames[section]:L"Font";
|
||||
}
|
||||
SetDlgItemTextW(dialog,optionsTitle,title);
|
||||
const auto show=[&](auto &controls,bool visible)
|
||||
{
|
||||
for(const auto id:controls) ShowWindow(GetDlgItem(dialog,id),visible?SW_SHOW:SW_HIDE);
|
||||
};
|
||||
EnableWindow(GetDlgItem(dialog,optionsReset),page>=0);
|
||||
const bool individual=page==PanelModes && section>=0;
|
||||
ShowWindow(GetDlgItem(dialog,optionsProperty),individual?SW_SHOW:SW_HIDE);
|
||||
ShowWindow(GetDlgItem(dialog,optionsResetProperty),individual?SW_SHOW:SW_HIDE);
|
||||
if(individual)
|
||||
{
|
||||
SendDlgItemMessageW(dialog,optionsProperty,CB_RESETCONTENT,0,0);
|
||||
for(size_t i=0;i<std::size(panelProperties);++i)
|
||||
{
|
||||
const auto &property=panelProperties[i];
|
||||
if(property.section!=section) continue;
|
||||
const auto row=SendDlgItemMessageW(dialog,optionsProperty,CB_ADDSTRING,0,
|
||||
reinterpret_cast<LPARAM>(property.name));
|
||||
if(row>=0) SendDlgItemMessageW(dialog,optionsProperty,CB_SETITEMDATA,row,i);
|
||||
}
|
||||
SendDlgItemMessageW(dialog,optionsProperty,CB_SETCURSEL,0,0);
|
||||
}
|
||||
show(panelLayoutControls,page==PanelModes && section==PanelLayout);
|
||||
show(wideColumnControls,page==PanelModes && section==WideColumns);
|
||||
show(statusColumnControls,page==PanelModes && section==StatusColumns);
|
||||
show(colorOptionControls,page==PanelColors && section>=0);
|
||||
show(appearanceControls,page==Appearance && section>=0);
|
||||
if(page==PanelColors && section>=0)
|
||||
{
|
||||
const auto color=draft.palette[section];
|
||||
wchar_t hex[8]{};
|
||||
swprintf_s(hex,L"#%02X%02X%02X",unsigned(GetRValue(color)),unsigned(GetGValue(color)),unsigned(GetBValue(color)));
|
||||
SetDlgItemTextW(dialog,280,hex);
|
||||
}
|
||||
if(page==Appearance)
|
||||
{
|
||||
const auto description=std::wstring(draft.font.lfFaceName)+L" · "+std::to_wstring(draft.points/10)+
|
||||
(draft.points%10?L"."+std::to_wstring(draft.points%10):L"")+L" pt";
|
||||
SetDlgItemTextW(dialog,281,description.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
void filterOptions(HWND dialog,OptionsDraft &draft)
|
||||
{
|
||||
wchar_t text[256]{};
|
||||
GetDlgItemTextW(dialog,optionsSearch,text,std::size(text));
|
||||
std::wstring query=text;
|
||||
if(!query.empty()) CharLowerBuffW(query.data(),static_cast<DWORD>(query.size()));
|
||||
const auto matches=[&](const wchar_t *label)
|
||||
{
|
||||
std::wstring lower=label;
|
||||
CharLowerBuffW(lower.data(),static_cast<DWORD>(lower.size()));
|
||||
return lower.find(query)!=std::wstring::npos;
|
||||
};
|
||||
const HWND tree=GetDlgItem(dialog,optionsTree);
|
||||
const auto add=[&](const wchar_t *label,int page,int option,HTREEITEM parent)
|
||||
{
|
||||
TVINSERTSTRUCTW insert{};
|
||||
insert.hParent=parent;
|
||||
insert.hInsertAfter=TVI_LAST;
|
||||
insert.item.mask=TVIF_TEXT|TVIF_PARAM;
|
||||
insert.item.pszText=const_cast<wchar_t*>(label);
|
||||
insert.item.lParam=(page<<8)|(option+1);
|
||||
return TreeView_InsertItem(tree,&insert);
|
||||
};
|
||||
const int previousPage=draft.page,previousSection=draft.section;
|
||||
TreeView_DeleteAllItems(tree);
|
||||
draft.wideColumnsNode=nullptr;
|
||||
const auto root=add(L"All settings",allOptions,-1,TVI_ROOT);
|
||||
HTREEITEM first{},selected{};
|
||||
for(int page=PanelModes;page<=Appearance;++page)
|
||||
{
|
||||
HTREEITEM category{};
|
||||
const bool wholeCategory=matches(optionsTitles[page]) || (!query.empty() && matches(L"All settings"));
|
||||
const auto addSection=[&](const wchar_t *label,int section,bool aliasMatches=false)
|
||||
{
|
||||
if(!wholeCategory && !aliasMatches && !matches(label)) return;
|
||||
if(!category) category=add(optionsTitles[page],page,-1,root);
|
||||
const auto node=add(label,page,section,category);
|
||||
if(!first) first=node;
|
||||
if(page==previousPage && section==previousSection) selected=node;
|
||||
if(page==PanelModes && section==WideColumns) draft.wideColumnsNode=node;
|
||||
};
|
||||
if(page==PanelModes)
|
||||
{
|
||||
for(int section=PanelLayout;section<=StatusColumns;++section)
|
||||
{
|
||||
const bool propertyMatches=std::any_of(std::begin(panelProperties),std::end(panelProperties),
|
||||
[&](const auto &property){return property.section==section && matches(property.name);});
|
||||
addSection(panelSectionNames[section],section,propertyMatches);
|
||||
}
|
||||
}
|
||||
else if(page==PanelColors)
|
||||
for(int color=0;color<ColorCount;++color) addSection(colorNames[color],color);
|
||||
else addSection(L"Font",0,matches(L"Display font"));
|
||||
if(category) TreeView_Expand(tree,category,TVE_EXPAND);
|
||||
}
|
||||
TreeView_Expand(tree,root,TVE_EXPAND);
|
||||
TreeView_SelectItem(tree,query.empty() && previousPage==allOptions?root:selected?selected:first?first:root);
|
||||
if(!first) SetDlgItemTextW(dialog,optionsTitle,L"No matching settings");
|
||||
}
|
||||
|
||||
INT_PTR CALLBACK optionsDialog(HWND dialog,UINT message,WPARAM wp,LPARAM lp)
|
||||
{
|
||||
if(message==WM_INITDIALOG)
|
||||
{
|
||||
auto &draft=*reinterpret_cast<OptionsDraft*>(lp);
|
||||
SetWindowLongPtrW(dialog,DWLP_USER,lp);
|
||||
draft.initialDpi=GetDpiForWindow(dialog);
|
||||
GetClientRect(dialog,&draft.initialClient);
|
||||
for(auto &control:draft.controls) control.bounds=dialogControlBounds(dialog,control.id);
|
||||
// Group boxes are behind their sibling controls. Erase their full client
|
||||
// area on resize while clipping out the controls inside the frame.
|
||||
for(const auto id:{275,277})
|
||||
{
|
||||
const auto group=GetDlgItem(dialog,id);
|
||||
SetWindowLongPtrW(group,GWL_STYLE,GetWindowLongPtrW(group,GWL_STYLE)|WS_CLIPSIBLINGS);
|
||||
SetWindowPos(group,HWND_BOTTOM,0,0,0,0,SWP_NOMOVE|SWP_NOSIZE|SWP_NOACTIVATE);
|
||||
SetWindowSubclass(group,optionsGroupBackground,1,0);
|
||||
}
|
||||
SetDlgItemInt(dialog,201,sizeWidth,FALSE);
|
||||
SendDlgItemMessageW(dialog,201,EM_SETLIMITTEXT,2,0);
|
||||
SendDlgItemMessageW(dialog,optionsSearch,EM_SETLIMITTEXT,255,0);
|
||||
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);
|
||||
filterOptions(dialog,draft);
|
||||
updateOptionsApply(dialog,draft);
|
||||
return TRUE;
|
||||
}
|
||||
auto *draft=reinterpret_cast<OptionsDraft*>(GetWindowLongPtrW(dialog,DWLP_USER));
|
||||
if(!draft) return FALSE;
|
||||
if(message==WM_GETMINMAXINFO)
|
||||
{
|
||||
const auto dpi=GetDpiForWindow(dialog);
|
||||
RECT minimum{0,0,MulDiv(draft->initialClient.right,dpi,draft->initialDpi),
|
||||
MulDiv(draft->initialClient.bottom,dpi,draft->initialDpi)};
|
||||
AdjustWindowRectExForDpi(&minimum,static_cast<DWORD>(GetWindowLongPtrW(dialog,GWL_STYLE)),
|
||||
FALSE,static_cast<DWORD>(GetWindowLongPtrW(dialog,GWL_EXSTYLE)),dpi);
|
||||
auto &limits=*reinterpret_cast<MINMAXINFO*>(lp);
|
||||
limits.ptMinTrackSize.x=minimum.right-minimum.left;
|
||||
limits.ptMinTrackSize.y=minimum.bottom-minimum.top;
|
||||
return TRUE;
|
||||
}
|
||||
if(message==WM_SIZE && wp!=SIZE_MINIMIZED)
|
||||
{
|
||||
layoutOptions(dialog,*draft);
|
||||
return TRUE;
|
||||
}
|
||||
if(message==WM_NOTIFY && wp==optionsTree)
|
||||
{
|
||||
const auto &change=*reinterpret_cast<const NMTREEVIEWW*>(lp);
|
||||
if(change.hdr.code==TVN_SELCHANGEDW && change.itemNew.hItem)
|
||||
showOptionsPage(dialog,*draft,static_cast<int>(change.itemNew.lParam>>8),static_cast<int>(change.itemNew.lParam&255)-1);
|
||||
return FALSE;
|
||||
}
|
||||
if(message==WM_CLOSE || (message==WM_COMMAND && LOWORD(wp)==IDCANCEL))
|
||||
{
|
||||
EndDialog(dialog,IDCANCEL);
|
||||
return TRUE;
|
||||
}
|
||||
if(message!=WM_COMMAND) return FALSE;
|
||||
switch(LOWORD(wp))
|
||||
{
|
||||
case optionsSearch:
|
||||
if(HIWORD(wp)==EN_CHANGE) filterOptions(dialog,*draft);
|
||||
return TRUE;
|
||||
case 279:
|
||||
if(draft->page==PanelColors && draft->section>=0)
|
||||
{
|
||||
choosePanelColor(dialog,draft->palette[draft->section],draft->custom.data());
|
||||
showOptionsPage(dialog,*draft,PanelColors,draft->section);
|
||||
updateOptionsApply(dialog,*draft);
|
||||
}
|
||||
return TRUE;
|
||||
case 282:
|
||||
chooseDisplayFont(dialog,draft->font,draft->points);
|
||||
showOptionsPage(dialog,*draft,Appearance,0);
|
||||
updateOptionsApply(dialog,*draft);
|
||||
return TRUE;
|
||||
case optionsReset:
|
||||
if(draft->page==allOptions || draft->page==PanelModes)
|
||||
for(const auto &property:panelProperties)
|
||||
if(draft->section<0 || property.section==draft->section) resetPanelProperty(dialog,property);
|
||||
if(draft->page==allOptions || (draft->page==PanelColors && draft->section<0))
|
||||
draft->palette=defaultColors;
|
||||
else if(draft->page==PanelColors)
|
||||
draft->palette[draft->section]=defaultColors[draft->section];
|
||||
if(draft->page==allOptions || draft->page==Appearance)
|
||||
{
|
||||
draft->font=defaultFilesFont();
|
||||
draft->points=defaultFontPoints;
|
||||
}
|
||||
showOptionsPage(dialog,*draft,draft->page,draft->section);
|
||||
updateOptionsApply(dialog,*draft);
|
||||
return TRUE;
|
||||
case optionsResetProperty:
|
||||
{
|
||||
const auto row=SendDlgItemMessageW(dialog,optionsProperty,CB_GETCURSEL,0,0);
|
||||
if(row>=0)
|
||||
{
|
||||
const auto property=SendDlgItemMessageW(dialog,optionsProperty,CB_GETITEMDATA,row,0);
|
||||
if(property>=0 && static_cast<size_t>(property)<std::size(panelProperties))
|
||||
resetPanelProperty(dialog,panelProperties[property]);
|
||||
updateOptionsApply(dialog,*draft);
|
||||
}
|
||||
return TRUE;
|
||||
}
|
||||
case 201:
|
||||
case 202: case 203: case 204: case 205: case 206: case 207: case 208:
|
||||
case 273: case 274:
|
||||
updateOptionsApply(dialog,*draft);
|
||||
return TRUE;
|
||||
case optionsApply:
|
||||
case IDOK:
|
||||
{
|
||||
BOOL valid{};
|
||||
const auto width=GetDlgItemInt(dialog,201,&valid,FALSE);
|
||||
if(!valid || width<6 || width>16)
|
||||
{
|
||||
SetDlgItemTextW(dialog,optionsSearch,L"");
|
||||
TreeView_SelectItem(GetDlgItem(dialog,optionsTree),draft->wideColumnsNode);
|
||||
MessageBoxW(dialog,L"Enter a width from 6 to 16 characters.",optionsTitles[PanelModes],MB_OK|MB_ICONWARNING);
|
||||
SetFocus(GetDlgItem(dialog,201));
|
||||
SendDlgItemMessageW(dialog,201,EM_SETSEL,0,-1);
|
||||
return TRUE;
|
||||
}
|
||||
const auto owner=GetWindow(dialog,GW_OWNER);
|
||||
const bool fontChanged=optionsFontChanged(*draft);
|
||||
if(fontChanged && !applyFont(owner,draft->font,draft->points))
|
||||
{
|
||||
MessageBoxW(dialog,L"Could not apply the selected font.",optionsTitles[Appearance],MB_OK|MB_ICONWARNING);
|
||||
return TRUE;
|
||||
}
|
||||
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(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);
|
||||
}
|
||||
layout(owner);
|
||||
showFileDetails(panels[active]);
|
||||
RedrawWindow(owner,nullptr,nullptr,RDW_INVALIDATE|RDW_ERASE|RDW_ALLCHILDREN|RDW_UPDATENOW);
|
||||
if(LOWORD(wp)==IDOK) EndDialog(dialog,IDOK);
|
||||
else updateOptionsApply(dialog,*draft);
|
||||
return TRUE;
|
||||
}
|
||||
}
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
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,
|
||||
reinterpret_cast<LPARAM>(&draft));
|
||||
SetFocus(panel);
|
||||
if(result==-1) setStatus(L"Could not open Files Options.");
|
||||
}
|
||||
@@ -205,6 +205,7 @@ void validateSettings(const Settings &s)
|
||||
throw std::runtime_error("Use Tesseract language codes separated by +, for example ukr+eng+rus");
|
||||
std::vector<Hotkey> used;
|
||||
std::vector<std::wstring> keys{s.toggle, s.casing, s.translit, s.undo, s.clipboard, s.screenshot};
|
||||
if (s.filesHotkey) keys.push_back(*s.filesHotkey);
|
||||
keys.insert(keys.end(), s.direct.begin(), s.direct.end());
|
||||
for (auto &text : keys)
|
||||
{
|
||||
@@ -370,6 +371,7 @@ Settings decodeSettings(const Bytes &bytes, const std::vector<Layout> &layouts)
|
||||
s.undo = str(j, "undoHotkey", s.undo);
|
||||
s.clipboard = str(j, "clipboardHotkey", s.clipboard);
|
||||
s.screenshot = str(j, "screenshotHotkey", s.screenshot);
|
||||
if (j.contains("filesHotkey")) s.filesHotkey = wide(j.at("filesHotkey").get<std::string>());
|
||||
s.screenshotAutoOpenEditor = j.value("screenshotAutoOpenEditor", false);
|
||||
if (j.contains("layouts"))
|
||||
{
|
||||
@@ -529,6 +531,7 @@ Bytes encodeSettings(const Settings &s)
|
||||
{"clipboardHotkey", utf8(s.clipboard)},
|
||||
{"screenshotHotkey", utf8(s.screenshot)},
|
||||
{"screenshotAutoOpenEditor", s.screenshotAutoOpenEditor}};
|
||||
if (s.filesHotkey) j["filesHotkey"] = utf8(*s.filesHotkey);
|
||||
j["layouts"] = Json::array();
|
||||
j["appLanguageSets"] = Json::object();
|
||||
for (const auto &[name, set] : s.appLanguageSets)
|
||||
|
||||
+6
-1
@@ -119,7 +119,7 @@ std::wstring fieldLabel(const std::string &key)
|
||||
static const std::map<std::string, std::wstring> labels{
|
||||
{"toggleHotkey", L"Next layout shortcut"}, {"caseHotkey", L"Change case shortcut"},
|
||||
{"translitHotkey", L"Transliteration shortcut"}, {"undoHotkey", L"Undo shortcut"},
|
||||
{"clipboardHotkey", L"Clipboard shortcut"}, {"screenshotHotkey", L"Screenshot shortcut"},
|
||||
{"clipboardHotkey", L"Clipboard shortcut"}, {"screenshotHotkey", L"Screenshot shortcut"}, {"filesHotkey", L"Files shortcut"},
|
||||
{"layouts", L"Layout cycle order"}, {"layoutHotkeys", L"Layout shortcuts"},
|
||||
{"chatCycles", L"Chat languages"}, {"mouseBindings", L"Mouse buttons"},
|
||||
{"convertLastWord", L"Convert recent word"}, {"switchSystemLayout", L"Switch system layout"},
|
||||
@@ -213,6 +213,7 @@ void collisionPaths(const Json &j, std::set<std::string> &paths)
|
||||
if (!inserted) { paths.insert(path); paths.insert(it->second); }
|
||||
};
|
||||
for (const auto *key : {"toggleHotkey", "caseHotkey", "translitHotkey", "undoHotkey", "clipboardHotkey", "screenshotHotkey"}) add(j.at(key), std::string("/") + key);
|
||||
if (j.contains("filesHotkey")) add(j.at("filesHotkey"), "/filesHotkey");
|
||||
for (const auto &[id, key] : j.at("layoutHotkeys").items()) add(key, "/layoutHotkeys/" + pointerKey(id));
|
||||
}
|
||||
}
|
||||
@@ -223,6 +224,10 @@ SyncMerge mergeSyncProfile(const Bytes &base, const Bytes &local, const Bytes &r
|
||||
// client's chat profiles until both copies understand this field.
|
||||
if (!remoteProfile.contains("chatCycles")) remoteProfile["chatCycles"] = localProfile.value("chatCycles", Json::object());
|
||||
if (!localProfile.contains("chatCycles")) localProfile["chatCycles"] = remoteProfile.at("chatCycles");
|
||||
if (!remoteProfile.contains("filesHotkey") && localProfile.contains("filesHotkey"))
|
||||
remoteProfile["filesHotkey"] = localProfile.at("filesHotkey");
|
||||
if (!localProfile.contains("filesHotkey") && remoteProfile.contains("filesHotkey"))
|
||||
localProfile["filesHotkey"] = remoteProfile.at("filesHotkey");
|
||||
// Older server backups omit the newly synchronized fields. An explicit
|
||||
// empty value from a current client still means the user cleared it.
|
||||
preserveMissingSettings(remoteProfile, localProfile);
|
||||
|
||||
+6
-4
@@ -1460,6 +1460,7 @@ void fillSettings(HWND w, SettingsView *v)
|
||||
value(w, 213, s.undo);
|
||||
value(w, 214, s.clipboard);
|
||||
value(w, 215, s.screenshot);
|
||||
value(w, 216, s.filesHotkey.value_or(L""));
|
||||
value(w, 240, std::to_wstring(s.maxItems));
|
||||
value(w, 241, std::to_wstring(s.maxMiB));
|
||||
value(w, 242, std::to_wstring(s.maxImageMiB));
|
||||
@@ -1513,6 +1514,7 @@ 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);
|
||||
s.maxItems = integer(w, 240);
|
||||
s.maxMiB = integer(w, 241);
|
||||
s.maxImageMiB = integer(w, 242);
|
||||
@@ -2522,13 +2524,13 @@ void showSettings(App &app, bool visible)
|
||||
page();
|
||||
|
||||
label(w, L"Click a shortcut field and press the desired keys", 24, 76, 660);
|
||||
const wchar_t *names[] = {L"Next layout", L"Change case", L"Transliterate", L"Undo", L"Clipboard history", L"Screen capture"};
|
||||
for (int i = 0; i < 6; ++i)
|
||||
const wchar_t *names[] = {L"Next layout", L"Change case", L"Transliterate", L"Undo", L"Clipboard history", L"Screen capture", L"Open KSwitcher Files"};
|
||||
for (int i = 0; i < static_cast<int>(std::size(names)); ++i)
|
||||
{
|
||||
label(w, names[i], 24, 116 + i * 40, 180);
|
||||
hotkeyEdit(app, w, 210 + i, 230, 112 + i * 40, 454);
|
||||
}
|
||||
checkbox(w, L"Open editor after screen capt&ure", 209, 24, 378, 660);
|
||||
checkbox(w, L"Open editor after screen capt&ure", 209, 24, 400, 660);
|
||||
page();
|
||||
|
||||
checkbox(w, L"Record clip&board history", 203, 24, 80, 660);
|
||||
@@ -2623,7 +2625,7 @@ void showShortcuts(App &app)
|
||||
auto &s = app.settings;
|
||||
std::wstring text = L"Next layout: " + s.toggle + L"\nChange case: " + s.casing + L"\nTransliterate: " +
|
||||
s.translit + L"\nUndo: " + s.undo + L"\nClipboard history: " + s.clipboard +
|
||||
L"\nScreen capture: " + s.screenshot + L"\n";
|
||||
L"\nScreen capture: " + s.screenshot + L"\nOpen KSwitcher Files: " + s.filesHotkey.value_or(L"") + L"\n";
|
||||
for (size_t i = 0; i < std::min(app.layouts.size(), s.direct.size()); ++i)
|
||||
if (!s.direct[i].empty())
|
||||
text += L"\n" + app.layouts[i].name + L": " + s.direct[i];
|
||||
|
||||
@@ -4,6 +4,21 @@
|
||||
using namespace ks;
|
||||
int main(){try{
|
||||
Settings base;base.autoCorrect=false;base.toggle=L"Ctrl+Alt+F11";
|
||||
if(decodeSettings(encodeSettings(base),{}).filesHotkey)
|
||||
throw std::runtime_error("missing Files shortcut did not survive round-trip");
|
||||
auto cleared=base;
|
||||
cleared.filesHotkey=L"";
|
||||
const auto clearedRoundTrip=decodeSettings(encodeSettings(cleared),{});
|
||||
if(!clearedRoundTrip.filesHotkey || !clearedRoundTrip.filesHotkey->empty())
|
||||
throw std::runtime_error("explicitly cleared Files shortcut was lost");
|
||||
auto duplicate=base;
|
||||
duplicate.filesHotkey=base.toggle;
|
||||
bool duplicateRejected=false;
|
||||
try {validateSettings(duplicate);} catch(const std::exception &e)
|
||||
{
|
||||
duplicateRejected=std::string(e.what())=="Keyboard shortcuts must be unique";
|
||||
}
|
||||
if(!duplicateRejected) throw std::runtime_error("duplicate Files shortcut accepted");
|
||||
auto draft=base,current=base;draft.toggle=L"Ctrl+Alt+F12";current.autoCorrect=true;
|
||||
auto merged=decodeSettings(mergeConfigurationSettings(encodeSettings(base),encodeSettings(draft),encodeSettings(current)),{});
|
||||
if(merged.toggle!=draft.toggle||!merged.autoCorrect)throw std::runtime_error("independent changes lost");
|
||||
|
||||
Reference in new issue
Block a user