Compare commits

..
14 changed files with 1452 additions and 205 deletions

No files matched your search

+21
View File
@@ -2,6 +2,27 @@
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 та повертають фокус до панелі.
- Colors → File type styles налаштовує текст, фон, підкреслення й закреслення для правил за розширенням і групи System and hidden. Розширення можна вибрати зі списку Windows або додати вручну з крапкою чи без неї. Вони зберігаються в KSwitcher, незалежно від асоціацій Windows. Курсор, позначки й результат передачі зберігають свої сигнальні кольори. Кольори правила можна перевизначити окремо для звичайного рядка, позначки Insert, курсора у фокусі та курсора без фокуса. Позначка має пріоритет над курсором, а підсвічування передачі має пріоритет над кольорами курсора. Без перевизначення стан зберігає звичайний вигляд панелі. Мініпанель унизу редактора одразу показує всі чотири стани на синтетичних рядках до Apply.
- Нижня область показує живе прев’ю для кольорів, шрифту, режимів панелей, широких колонок і рядка стану. Для Popup background, Menu selection і Command bar зразок показує відповідний елемент. Прев’ю читає чернетку, зокрема після Reset, і не потребує реальних файлів. Воно заповнює простір від останнього видимого елемента редактора до нижніх кнопок зі сталими відступами. Зі зміною розміру росте область зразка, а рядки зберігають масштаб шрифту.
- Підписи File type, Windows extension or custom extension і Row state стоять над відповідними полями зі спільним лівим краєм. Ширина списків враховує найдовші значення, шрифт, DPI й доступне місце. Add і Remove стоять одразу після своїх полів.
- Початковий набір правил охоплює EXE, Markdown, системні й приховані елементи, архіви, зображення, аудіо й відео, офісні документи й PDF, код і скрипти. Збережений набір правил завантажується як є, включно з видаленими правилами. Reset у File type styles відновлює весь початковий набір у чернетці.
## Панелі
- Меню Change drive показує літеру, тип, загальний обсяг і вільне місце. Відкриття меню запускає нове LAN-виявлення. Секція KSwitcher LAN показує збережені пари, зокрема офлайн. Після кожного відкриття меню асинхронно запитує диски й показує їх під назвою відповідного ПК. Поки триває запит, видно стан завантаження. Якщо запит не вдався, видно помилку. Повторне відкриття запускає новий запит. Кожна панель має власний диск і шлях, а в заголовку віддаленої панелі перед шляхом показано назву ПК.
+46 -16
View File
@@ -1,26 +1,56 @@
#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, 310
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, 240
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, 181, 125, 15
COMBOBOX 285, 131, 183, 139, 95, CBS_DROPDOWNLIST | WS_TABSTOP | WS_VSCROLL
PUSHBUTTON "Reset property", 286, 278, 183, 108, 15
LTEXT "File type", 287, 131, 26, 75, 10
COMBOBOX 288, 131, 38, 151, 100, CBS_DROPDOWNLIST | WS_TABSTOP | WS_VSCROLL
PUSHBUTTON "&Remove", 289, 290, 38, 77, 14
LTEXT "Windows extension or custom extension", 290, 131, 63, 246, 10
COMBOBOX 291, 131, 75, 151, 108, CBS_DROPDOWN | CBS_AUTOHSCROLL | WS_TABSTOP | WS_VSCROLL
PUSHBUTTON "A&dd", 292, 290, 75, 77, 14
LTEXT "Row state", 300, 131, 100, 72, 10
COMBOBOX 301, 131, 112, 174, 80, CBS_DROPDOWNLIST | WS_TABSTOP
PUSHBUTTON "&Foreground…", 293, 131, 135, 78, 14
PUSHBUTTON "&Background…", 294, 216, 135, 78, 14
PUSHBUTTON "&Inherit colors", 295, 301, 135, 83, 14
LTEXT "", 296, 131, 152, 253, 13
AUTOCHECKBOX "&Underline", 297, 131, 166, 80, 12
AUTOCHECKBOX "Stri&kethrough", 298, 218, 166, 100, 12
CONTROL "Settings preview", 299, "Static", SS_OWNERDRAW | WS_BORDER, 131, 202, 253, 81
DEFPUSHBUTTON "OK", IDOK, 222, 289, 50, 14
PUSHBUTTON "Cancel", IDCANCEL, 280, 289, 50, 14
PUSHBUTTON "&Apply", 283, 338, 289, 50, 14
END
102 DIALOGEX 0, 0, 350, 114
+57 -16
View File
@@ -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);
+2
View File
@@ -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{};
+1
View File
@@ -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;
+230 -168
View File
@@ -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)
@@ -362,6 +367,8 @@ void savePreference(const wchar_t *name, DWORD value)
RegSetKeyValueW(HKEY_CURRENT_USER,schemeKey,name,REG_DWORD,&value,sizeof(value));
}
#include "files_styles.inl"
constexpr const wchar_t *panelPathKeys[]{L"LeftPath",L"RightPath"};
constexpr const wchar_t *panelCursorKeys[]{L"LeftCursor",L"RightCursor"};
@@ -401,6 +408,18 @@ 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;
}
std::array<HFONT,4> fileStyleFonts{};
bool applyFont(HWND owner, LOGFONTW font, int points)
{
const auto dpi=GetDpiForWindow(owner);
@@ -419,6 +438,14 @@ bool applyFont(HWND owner, LOGFONTW font, int points)
cellWidth=std::max(1L,metrics.tmAveCharWidth);
auto old=mono;
mono=next;
for(unsigned i=1;i<fileStyleFonts.size();++i)
{
if(fileStyleFonts[i]) DeleteObject(fileStyleFonts[i]);
auto decorated=font;
decorated.lfUnderline=(i&1)!=0;
decorated.lfStrikeOut=(i&2)!=0;
fileStyleFonts[i]=CreateFontIndirectW(&decorated);
}
displayFont=font;
fontPoints=points;
terminalView.setFont(displayFont,fontPoints);
@@ -443,22 +470,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,36 +506,41 @@ void loadColors()
refreshColors();
}
void changeColor(HWND owner, int index)
bool 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)) return false;
color=dialog.rgbResult;
return true;
}
void paintText(HDC dc, RECT rect, const wchar_t *text, COLORREF foreground, COLORREF background, bool right=false)
void paintText(HDC dc, RECT rect, const wchar_t *text, COLORREF foreground, COLORREF background, bool right=false,HFONT font=nullptr)
{
SetTextColor(dc, foreground);
SetBkColor(dc, background);
SelectObject(dc, mono);
SelectObject(dc, font?font:mono);
auto alignment=SetTextAlign(dc,right?TA_RIGHT:TA_LEFT);
ExtTextOutW(dc,right?rect.right-5:rect.left+5,rect.top+3,ETO_OPAQUE|ETO_CLIPPED,&rect,text,lstrlenW(text),nullptr);
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 +1049,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 +1099,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;}
@@ -2845,6 +2862,7 @@ LRESULT CALLBACK panelKeys(HWND window, UINT message, WPARAM wp, LPARAM lp, UINT
}
if (message == WM_KEYDOWN && (wp == VK_LEFT || wp == VK_RIGHT))
return scrollList(window, message, wp == VK_LEFT ? VK_HOME : VK_END, lp);
if(message==WM_KILLFOCUS || message==WM_SETFOCUS) InvalidateRect(window,nullptr,FALSE);
if (message == WM_SETFOCUS)
{
const auto previous=active;
@@ -2922,69 +2940,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 +2964,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 +3007,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 +3096,19 @@ 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;
loadFileStyles();
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"};
@@ -3393,48 +3428,47 @@ LRESULT CALLBACK filesWindow(HWND window, UINT message, WPARAM wp, LPARAM lp)
if(item.CtlID==102 || item.CtlID==103)
{
const auto cursor=SendMessageW(item.hwndItem,LB_GETCURSEL,0,0);
if(item.hwndItem==panels[active].list && cursor!=LB_ERR &&
if(GetFocus()==item.hwndItem && cursor!=LB_ERR &&
static_cast<UINT>(cursor)==item.itemID) background=PanelCursor;
}
else if(item.itemState&(ODS_SELECTED|ODS_FOCUS))
background=Selection;
COLORREF rowBackground=colors[background];
HFONT rowFont{};
if(item.CtlID==102 || item.CtlID==103)
{
const auto &panel=panels[item.CtlID-102];
if(item.itemID<panel.rows.size() && panel.sizes[item.itemID]!=L"Folder" &&
panel.sizes[item.itemID]!=L"Up")
if(item.itemID<panel.rows.size() && panel.sizes[item.itemID]!=L"Up")
{
const auto extension=panel.rows[item.itemID].extension();
if(CompareStringOrdinal(extension.c_str(),-1,L".exe",-1,TRUE)==CSTR_EQUAL)
foreground=RGB(85,255,85);
else if(CompareStringOrdinal(extension.c_str(),-1,L".md",-1,TRUE)==CSTR_EQUAL)
foreground=RGB(231,192,247);
}
if(item.itemID<panel.marked.size() && panel.marked[item.itemID])
foreground=RGB(255,255,0);
else if(transferHighlightPhase%2==0 && item.itemID<panel.rows.size() &&
panel.sizes[item.itemID]!=L"Up" && panel.transferResults.contains(panel.rows[item.itemID]))
foreground=RGB(255,223,115);
else if(item.itemID<panel.dimmed.size() && panel.dimmed[item.itemID])
{
auto bg=colors[background];
foreground=RGB((3*GetRValue(foreground)+2*GetRValue(bg))/5,
(3*GetGValue(foreground)+2*GetGValue(bg))/5,
(3*GetBValue(foreground)+2*GetBValue(bg))/5);
const bool marked=panel.marked[item.itemID];
const bool transferred=transferHighlightPhase%2==0 && panel.transferResults.contains(panel.rows[item.itemID]);
const bool cursor=SendMessageW(panel.list,LB_GETCURSEL,0,0)==item.itemID;
const auto state=marked?FileRowState::Marked:!cursor?FileRowState::Normal:
GetFocus()==panel.list?FileRowState::Focused:FileRowState::Unfocused;
const auto resolved=resolveFileStyle(panel.rows[item.itemID].extension().native(),
panel.sizes[item.itemID]!=L"Folder",panel.dimmed[item.itemID],
background==PanelCursor || marked || transferred,foreground,rowBackground,fileStyles,state,transferred);
foreground=resolved.foreground;
rowBackground=resolved.background;
rowFont=fileStyleFonts[resolved.decorations];
}
}
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,7 +3521,9 @@ 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 paintText(item.hDC,item.rcItem,text.c_str(),foreground,colors[background]);
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,rowBackground,false,rowFont);
if(item.CtlID==102 || item.CtlID==103)
{
const auto &panel=panels[item.CtlID-102];
@@ -3510,16 +3546,16 @@ LRESULT CALLBACK filesWindow(HWND window, UINT message, WPARAM wp, LPARAM lp)
end.left=std::max(end.left+static_cast<LONG>(cellWidth),end.right-width);
end.left=std::min(end.left,end.right);
name.right=end.left;
paintText(item.hDC,end,extension.c_str()+1,foreground,colors[background],true);
paintText(item.hDC,end,extension.c_str()+1,foreground,rowBackground,true,rowFont);
text=panel.rows[item.itemID].stem().wstring();
}
}
paintText(item.hDC,name,text.c_str(),foreground,colors[background]);
paintText(item.hDC,name,text.c_str(),foreground,rowBackground,false,rowFont);
const auto &amount=panel.sizes[item.itemID];
auto visible=amount;
if(amount!=L"Folder" && amount!=L"Up")
visible=formatSize(std::stoull(amount),static_cast<size_t>(sizeWidth));
paintText(item.hDC,size,visible.c_str(),foreground,colors[background],true);
paintText(item.hDC,size,visible.c_str(),foreground,rowBackground,true);
RECT line{size.left,size.top,size.left+1,size.bottom};
SetDCBrushColor(item.hDC,colors[Frame]);
FillRect(item.hDC,&line,static_cast<HBRUSH>(GetStockObject(DC_BRUSH)));
@@ -3621,7 +3657,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 +3698,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 +3711,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;}
@@ -3715,6 +3776,7 @@ int WINAPI wWinMain(HINSTANCE instance, HINSTANCE, PWSTR, int show)
TranslateMessage(&message);
DispatchMessageW(&message);
}
for(auto font:fileStyleFonts) if(font) DeleteObject(font);
DeleteObject(mono);
DeleteObject(blue);
DeleteObject(teal);
+537
View File
@@ -0,0 +1,537 @@
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{};
std::vector<FileStyle> styles=fileStyles;
int styleIndex=0;
bool styleSuggestionsLoaded{};
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}};
};
#include "files_style_options.inl"
#include "files_options_preview.inl"
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 || draft.styles!=fileStyles || optionsFontChanged(draft);
EnableWindow(GetDlgItem(dialog,optionsApply),dirty);
InvalidateRect(GetDlgItem(dialog,fileStylePreview),nullptr,FALSE);
}
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 layoutOptionsPreview(HWND dialog)
{
const auto preview=GetDlgItem(dialog,fileStylePreview);
if(!(GetWindowLongPtrW(preview,GWL_STYLE)&WS_VISIBLE)) return;
RECT gap{0,0,6,5};
MapDialogRect(dialog,&gap);
const auto title=dialogControlBounds(dialog,optionsTitle);
const auto buttons=dialogControlBounds(dialog,IDOK);
LONG top=title.bottom;
for(auto child=GetWindow(dialog,GW_CHILD);child;child=GetWindow(child,GW_HWNDNEXT))
{
const auto id=GetDlgCtrlID(child);
if(child==preview || id==IDOK || id==IDCANCEL || id==optionsApply ||
!(GetWindowLongPtrW(child,GWL_STYLE)&WS_VISIBLE)) continue;
const auto bounds=dialogControlBounds(dialog,id);
if(id!=-1 && bounds.left>=title.left-4) top=std::max(top,bounds.bottom);
}
top+=gap.bottom;
SetWindowPos(preview,nullptr,title.left,top,title.right-title.left,
std::max(1L,buttons.top-gap.bottom-top),SWP_NOZORDER|SWP_NOACTIVATE|SWP_NOCOPYBITS|SWP_NOREDRAW);
}
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);
}
if(draft.page==PanelColors && draft.section==fileStyleSection) layoutFileStyleFields(dialog,false);
layoutOptionsPreview(dialog);
// 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?(section==fileStyleSection?L"File type styles":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);
const bool fileTypes=page==PanelColors && section==fileStyleSection;
constexpr int fileStyleControls[]{287,288,289,290,291,292,293,294,295,296,297,298,300,fileStyleState};
show(fileStyleControls,fileTypes);
const auto preview=GetDlgItem(dialog,fileStylePreview);
ShowWindow(preview,page>=0 && page<allOptions && section>=0?SW_SHOW:SW_HIDE);
layoutOptionsPreview(dialog);
RedrawWindow(dialog,nullptr,nullptr,RDW_INVALIDATE|RDW_ERASE|RDW_ALLCHILDREN);
show(colorOptionControls,page==PanelColors && section>=0 && !fileTypes);
if(fileTypes)
{
if(!draft.styleSuggestionsLoaded)
{
fillFileStyleSuggestions(dialog);
draft.styleSuggestionsLoaded=true;
}
updateFileStyleEditor(dialog,draft,true);
}
show(appearanceControls,page==Appearance && section>=0);
if(page==PanelColors && section>=0 && !fileTypes)
{
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);
const bool extensionMatches=std::any_of(draft.styles.begin(),draft.styles.end(),
[&](const auto &style){return matches(style.extension.c_str());});
addSection(L"File type styles",fileStyleSection,extensionMatches || matches(L"System and hidden") ||
matches(L"Underline") || matches(L"Strikethrough") || matches(L"Foreground"));
}
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_DRAWITEM && wp==fileStylePreview)
{
const auto &item=*reinterpret_cast<const DRAWITEMSTRUCT*>(lp);
if(draft->page==PanelColors && draft->section==fileStyleSection) paintFileStylePreview(item,*draft);
else paintOptionsPreview(dialog,item,*draft);
return TRUE;
}
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 && draft->section<ColorCount)
{
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 fileStyleState:
if(HIWORD(wp)==CBN_SELCHANGE) updateFileStyleEditor(dialog,*draft,false);
return TRUE;
case fileStyleRules:
if(HIWORD(wp)==CBN_SELCHANGE)
{
draft->styleIndex=static_cast<int>(SendDlgItemMessageW(dialog,fileStyleRules,CB_GETCURSEL,0,0));
updateFileStyleEditor(dialog,*draft,false);
}
return TRUE;
case 292:
if(addFileStyle(dialog,*draft)) updateOptionsApply(dialog,*draft);
return TRUE;
case 289: case 293: case 294: case 295: case 297: case 298:
if(draft->styleIndex>=0)
{
auto &style=draft->styles[draft->styleIndex];
const auto state=SendDlgItemMessageW(dialog,fileStyleState,CB_GETCURSEL,0,0);
auto &foreground=state>0?style.states[state-1].foreground:style.foreground;
auto &background=state>0?style.states[state-1].background:style.background;
switch(LOWORD(wp))
{
case 289:
draft->styles.erase(draft->styles.begin()+draft->styleIndex);
break;
case 293: case 294:
{
auto &field=LOWORD(wp)==293?foreground:background;
auto color=field.value_or(draft->palette[LOWORD(wp)==293?Text:PanelBlue]);
if(choosePanelColor(dialog,color,draft->custom.data())) field=color;
break;
}
case 295: foreground.reset();background.reset();break;
case 297: style.underline=IsDlgButtonChecked(dialog,297)==BST_CHECKED;break;
case 298: style.strikeout=IsDlgButtonChecked(dialog,298)==BST_CHECKED;break;
}
updateFileStyleEditor(dialog,*draft,LOWORD(wp)==289);
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->section< ColorCount)
draft->palette[draft->section]=defaultColors[draft->section];
if(draft->page==allOptions || (draft->page==PanelColors &&
(draft->section<0 || draft->section==fileStyleSection)))
draft->styles=defaultFileStyles;
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 stylesChanged=draft->styles!=fileStyles;
if(stylesChanged && !saveFileStyles(draft->styles))
{
MessageBoxW(dialog,L"Could not save the file type styles.",L"KSwitcher Files Options",MB_OK|MB_ICONWARNING);
return TRUE;
}
const bool fontChanged=optionsFontChanged(*draft);
if(fontChanged && !applyFont(owner,draft->font,draft->points))
{
if(stylesChanged) saveFileStyles(fileStyles);
MessageBoxW(dialog,L"Could not apply the selected font.",optionsTitles[Appearance],MB_OK|MB_ICONWARNING);
return TRUE;
}
if(stylesChanged) fileStyles=draft->styles;
sizeWidth=static_cast<int>(width);
alignExtensions=IsDlgButtonChecked(dialog,202)==BST_CHECKED;
showDetails=IsDlgButtonChecked(dialog,203)==BST_CHECKED;
alignFolders=IsDlgButtonChecked(dialog,204)==BST_CHECKED;
statusFields=0;
for(int i=0;i<4;++i)
if(IsDlgButtonChecked(dialog,205+i)==BST_CHECKED) statusFields|=1u<<i;
savePreference(L"SizeWidth",sizeWidth);
savePreference(L"AlignExtensions",alignExtensions);
savePreference(L"ShowDetails",showDetails);
savePreference(L"AlignFolders",alignFolders);
savePreference(L"StatusFields",statusFields);
for(int i=0;i<ColorCount;++i)
if(colors[i]!=draft->palette[i])
{
colors[i]=draft->palette[i];
savePreference(std::to_wstring(i).c_str(),colors[i]);
}
refreshColors();
if(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.");
}
+139
View File
@@ -0,0 +1,139 @@
void paintOptionsPreview(HWND dialog,const DRAWITEMSTRUCT &item,const OptionsDraft &draft)
{
const auto dc=item.hDC;
const auto saved=SaveDC(dc);
if(!saved) return;
IntersectClipRect(dc,item.rcItem.left,item.rcItem.top,item.rcItem.right,item.rcItem.bottom);
const auto dpi=GetDpiForWindow(item.hwndItem);
const auto px=[&](int value){return MulDiv(value,dpi,96);};
auto font=draft.font;
font.lfHeight=-MulDiv(draft.points,dpi,720);
font.lfWidth=0;
const auto previewFont=CreateFontIndirectW(&font);
SelectObject(dc,previewFont?previewFont:mono);
TEXTMETRICW metrics{};
GetTextMetricsW(dc,&metrics);
const auto rowHeight=std::max<LONG>(px(18),metrics.tmHeight+px(4));
const auto &palette=draft.palette;
const auto fill=[&](RECT bounds,COLORREF color)
{
SetBkColor(dc,color);
ExtTextOutW(dc,0,0,ETO_OPAQUE|ETO_CLIPPED,&bounds,nullptr,0,nullptr);
};
const auto row=[&](RECT bounds,std::wstring_view label,COLORREF fg,COLORREF bg,bool right=false)
{
fill(bounds,bg);
bounds.left+=px(5);
bounds.right-=px(5);
SetBkMode(dc,TRANSPARENT);
SetTextColor(dc,fg);
DrawTextW(dc,label.data(),static_cast<int>(label.size()),&bounds,
DT_SINGLELINE|DT_VCENTER|DT_NOPREFIX|DT_END_ELLIPSIS|(right?DT_RIGHT:DT_LEFT));
};
const auto checked=[&](int id){return IsDlgButtonChecked(dialog,id)==BST_CHECKED;};
auto bounds=item.rcItem;
fill(bounds,palette[PanelBlue]);
const auto frame=[&](RECT rect)
{
SetDCBrushColor(dc,palette[Frame]);
FrameRect(dc,&rect,static_cast<HBRUSH>(GetStockObject(DC_BRUSH)));
InflateRect(&rect,-px(2),-px(2));
FrameRect(dc,&rect,static_cast<HBRUSH>(GetStockObject(DC_BRUSH)));
};
const auto panel=[&](RECT panelBounds,bool wide,bool cursor,std::wstring_view path)
{
frame(panelBounds);
InflateRect(&panelBounds,-px(4),-px(4));
auto line=panelBounds;
line.bottom=line.top+rowHeight;
row(line,path,palette[Frame],palette[PanelBlue]);
OffsetRect(&line,0,rowHeight);
BOOL valid{};
const auto input=GetDlgItemInt(dialog,201,&valid,FALSE);
const auto columns=valid?std::clamp(input,6u,16u):static_cast<UINT>(sizeWidth);
const auto entry=[&](std::wstring_view stem,std::wstring_view ext,bool folder,bool highlight)
{
const auto bg=highlight?palette[PanelCursor]:palette[PanelBlue];
auto name=line;
if(wide)
{
name.right=std::max(name.left+px(20),name.right-static_cast<LONG>(columns+2)*metrics.tmAveCharWidth);
name.right=std::min(name.right,line.right);
auto amount=line;
amount.left=name.right;
row(amount,folder?L"Folder":formatSize(48382,columns),palette[Text],bg,true);
fill({amount.left,amount.top,amount.left+px(1),amount.bottom},palette[Frame]);
}
if(wide && checked(folder?204:202) && name.right-name.left>px(70))
{
auto extension=name;
extension.left=extension.right-std::min<LONG>(px(60),(extension.right-extension.left)/2);
name.right=extension.left;
row(extension,ext,palette[Text],bg,true);
row(name,stem,palette[Text],bg);
}
else row(name,std::wstring(stem)+L"."+std::wstring(ext),palette[Text],bg);
OffsetRect(&line,0,rowHeight);
};
entry(L"report",L"txt",false,cursor);
entry(L"examples",L"old",true,false);
};
if(draft.page==PanelColors && (draft.section==Dialog || draft.section==Selection))
{
InflateRect(&bounds,-px(12),-px(10));
const auto maxWidth=px(340);
bounds.right=std::min(bounds.right,bounds.left+maxWidth);
fill(bounds,palette[Dialog]);
frame(bounds);
InflateRect(&bounds,-px(5),-px(5));
auto line=bounds;
line.bottom=line.top+rowHeight;
row(line,L"Change drive",palette[Text],palette[Dialog]);
OffsetRect(&line,0,rowHeight);
row(line,L"C: fixed",palette[Text],palette[Selection]);
OffsetRect(&line,0,rowHeight);
row(line,L"D: fixed",palette[Text],palette[Dialog]);
}
else if(draft.page==PanelColors && draft.section==Footer)
{
auto bar=bounds;
bar.top=bar.bottom-rowHeight;
row(bar,L"F2 Refresh F3 View F5 Copy F6 Move",palette[Text],palette[Footer]);
bounds.bottom=bar.top;
panel(bounds,false,false,L"C:\\Example");
}
else if(draft.page==PanelModes && draft.section==PanelLayout)
{
auto left=bounds,right=bounds;
left.right=(bounds.left+bounds.right)/2;
right.left=left.right+px(4);
panel(left,checked(273),true,L"C:\\Example");
panel(right,checked(274),false,L"D:\\Example");
}
else if(draft.page==PanelModes && draft.section==StatusColumns)
{
auto statusRow=bounds;
statusRow.top=statusRow.bottom-rowHeight;
bounds.bottom=statusRow.top;
panel(bounds,true,true,L"C:\\Example");
std::wstring label;
if(checked(203))
{
constexpr const wchar_t *values[]{L"report.txt",L"47 KiB",L"2026-11-14",L"10:30"};
for(int i=0;i<4;++i)
if(checked(205+i))
{
if(!label.empty()) label+=L" ";
label+=values[i];
}
}
row(statusRow,label,palette[Text],palette[PanelBlue]);
}
else panel(bounds,draft.page==PanelModes || checked(273),
draft.page==PanelColors && draft.section==PanelCursor,L"C:\\Example");
RestoreDC(dc,saved);
if(previewFont) DeleteObject(previewFont);
}
+209
View File
@@ -0,0 +1,209 @@
constexpr int fileStyleSection=ColorCount;
constexpr int fileStyleRules=288,fileStyleSuggestion=291,fileStylePreview=299,fileStyleState=301;
constexpr const wchar_t *fileStyleStateNames[]{L"Normal",L"Marked (Insert)",L"Focused cursor",L"Unfocused cursor"};
void layoutFileStyleFields(HWND dialog,bool redraw)
{
RECT client{};
GetClientRect(dialog,&client);
RECT spacing{0,0,8,0};
MapDialogRect(dialog,&spacing);
const auto gap=spacing.right;
const auto dpi=GetDpiForWindow(dialog);
const auto arrow=GetSystemMetricsForDpi(SM_CXVSCROLL,dpi);
const auto border=GetSystemMetricsForDpi(SM_CXEDGE,dpi);
const auto flags=SWP_NOZORDER|SWP_NOACTIVATE|SWP_NOCOPYBITS|SWP_NOREDRAW;
const auto fit=[&](int id,int buttonId)
{
const auto combo=GetDlgItem(dialog,id);
const auto bounds=dialogControlBounds(dialog,id);
const auto dc=GetDC(combo);
if(!dc) return;
const auto previous=SelectObject(dc,reinterpret_cast<HFONT>(SendMessageW(combo,WM_GETFONT,0,0)));
SIZE extent{};
const std::wstring_view example=L".example";
GetTextExtentPoint32W(dc,example.data(),static_cast<int>(example.size()),&extent);
LONG longest=extent.cx;
const auto count=SendMessageW(combo,CB_GETCOUNT,0,0);
for(LRESULT row=0;row<count;++row)
{
const auto length=SendMessageW(combo,CB_GETLBTEXTLEN,row,0);
if(length<=0) continue;
std::wstring item(static_cast<size_t>(length)+1,L'\0');
if(SendMessageW(combo,CB_GETLBTEXT,row,reinterpret_cast<LPARAM>(item.data()))!=CB_ERR &&
GetTextExtentPoint32W(dc,item.data(),static_cast<int>(length),&extent))
longest=std::max(longest,extent.cx);
}
if(previous) SelectObject(dc,previous);
ReleaseDC(combo,dc);
const auto button=buttonId?dialogControlBounds(dialog,buttonId):RECT{};
const auto buttonWidth=button.right-button.left;
const auto available=client.right-bounds.left-gap-(buttonId?gap+buttonWidth:0);
const auto width=std::max(1L,std::min(available,longest+arrow+2*border+gap));
RECT dropped{};
SendMessageW(combo,CB_GETDROPPEDCONTROLRECT,0,reinterpret_cast<LPARAM>(&dropped));
SetWindowPos(combo,nullptr,bounds.left,bounds.top,width,dropped.bottom-dropped.top,flags);
if(buttonId)
SetWindowPos(GetDlgItem(dialog,buttonId),nullptr,bounds.left+width+gap,button.top,
buttonWidth,button.bottom-button.top,flags);
};
fit(fileStyleRules,289);
fit(fileStyleSuggestion,292);
fit(fileStyleState,0);
if(redraw)
RedrawWindow(dialog,nullptr,nullptr,RDW_INVALIDATE|RDW_ERASE|RDW_ALLCHILDREN);
}
void fillFileStyleSuggestions(HWND dialog)
{
std::set<std::wstring> extensions;
for(DWORD i=0;;++i)
{
wchar_t name[256]{};
DWORD length=std::size(name);
const auto result=RegEnumKeyExW(HKEY_CLASSES_ROOT,i,name,&length,nullptr,nullptr,nullptr,nullptr);
if(result==ERROR_MORE_DATA) continue;
if(result!=ERROR_SUCCESS) break;
if(name[0]!=L'.') continue;
auto extension=fileStyleExtension(name);
if(!extension.empty()) extensions.insert(std::move(extension));
}
for(const auto &extension:extensions)
SendDlgItemMessageW(dialog,fileStyleSuggestion,CB_ADDSTRING,0,reinterpret_cast<LPARAM>(extension.c_str()));
SendDlgItemMessageW(dialog,fileStyleSuggestion,CB_LIMITTEXT,255,0);
for(const auto label:fileStyleStateNames)
SendDlgItemMessageW(dialog,fileStyleState,CB_ADDSTRING,0,reinterpret_cast<LPARAM>(label));
SendDlgItemMessageW(dialog,fileStyleState,CB_SETCURSEL,0,0);
}
void updateFileStyleEditor(HWND dialog,OptionsDraft &draft,bool rebuild)
{
const auto combo=GetDlgItem(dialog,fileStyleRules);
if(rebuild)
{
SendMessageW(combo,CB_RESETCONTENT,0,0);
for(const auto &style:draft.styles)
{
const auto label=style.extension==L"@system-hidden"?L"System and hidden":style.extension.c_str();
SendMessageW(combo,CB_ADDSTRING,0,reinterpret_cast<LPARAM>(label));
}
draft.styleIndex=draft.styles.empty()?-1:std::clamp(draft.styleIndex,0,static_cast<int>(draft.styles.size())-1);
SendMessageW(combo,CB_SETCURSEL,draft.styleIndex,0);
layoutFileStyleFields(dialog,true);
}
InvalidateRect(GetDlgItem(dialog,fileStylePreview),nullptr,FALSE);
const bool selected=draft.styleIndex>=0;
for(const int id:{289,293,294,295,297,298})
EnableWindow(GetDlgItem(dialog,id),selected);
if(!selected)
{
SetDlgItemTextW(dialog,296,L"");
CheckDlgButton(dialog,297,BST_UNCHECKED);
CheckDlgButton(dialog,298,BST_UNCHECKED);
return;
}
const auto &style=draft.styles[draft.styleIndex];
const auto colorLabel=[](std::optional<COLORREF> color)->std::wstring
{
if(!color) return L"Default";
wchar_t hex[8]{};
swprintf_s(hex,L"#%02X%02X%02X",unsigned(GetRValue(*color)),unsigned(GetGValue(*color)),unsigned(GetBValue(*color)));
return hex;
};
const auto state=SendDlgItemMessageW(dialog,fileStyleState,CB_GETCURSEL,0,0);
const auto fg=state>0?style.states[state-1].foreground:style.foreground;
const auto bg=state>0?style.states[state-1].background:style.background;
const auto label=L"Foreground: "+colorLabel(fg)+L" Background: "+colorLabel(bg);
SetDlgItemTextW(dialog,296,label.c_str());
CheckDlgButton(dialog,297,style.underline);
CheckDlgButton(dialog,298,style.strikeout);
}
bool addFileStyle(HWND dialog,OptionsDraft &draft)
{
wchar_t input[256]{};
GetDlgItemTextW(dialog,fileStyleSuggestion,input,std::size(input));
const auto extension=fileStyleExtension(input);
if(extension.empty())
{
MessageBoxW(dialog,L"Enter a file extension such as .txt.",L"File type styles",MB_OK|MB_ICONWARNING);
SetFocus(GetDlgItem(dialog,fileStyleSuggestion));
return false;
}
const auto existing=std::find_if(draft.styles.begin(),draft.styles.end(),
[&](const auto &style){return sameFileExtension(style.extension,extension);});
if(existing!=draft.styles.end()) draft.styleIndex=static_cast<int>(existing-draft.styles.begin());
else
{
if(draft.styles.size()>=64)
{
MessageBoxW(dialog,L"The profile can contain up to 64 file type rules.",L"File type styles",MB_OK|MB_ICONWARNING);
return false;
}
draft.styles.push_back({extension});
draft.styleIndex=static_cast<int>(draft.styles.size())-1;
}
SetDlgItemTextW(dialog,fileStyleSuggestion,L"");
updateFileStyleEditor(dialog,draft,true);
return true;
}
void paintFileStylePreview(const DRAWITEMSTRUCT &item,const OptionsDraft &draft)
{
const auto saved=SaveDC(item.hDC);
const bool selected=draft.styleIndex>=0 && static_cast<size_t>(draft.styleIndex)<draft.styles.size();
const auto extension=selected?std::wstring_view(draft.styles[draft.styleIndex].extension):std::wstring_view{};
const bool systemHidden=extension==L"@system-hidden";
SetBkColor(item.hDC,draft.palette[PanelBlue]);
ExtTextOutW(item.hDC,0,0,ETO_OPAQUE,&item.rcItem,nullptr,0,nullptr);
if(selected)
{
auto font=draft.font;
font.lfHeight=-MulDiv(draft.points,GetDpiForWindow(item.hwndItem),720);
font.lfWidth=0;
const auto fontHandle=CreateFontIndirectW(&font);
if(fontHandle) SelectObject(item.hDC,fontHandle);
TEXTMETRICW metrics{};
GetTextMetricsW(item.hDC,&metrics);
const int height=metrics.tmHeight+MulDiv(4,GetDpiForWindow(item.hwndItem),96);
if(fontHandle)
{
SelectObject(item.hDC,mono?mono:GetStockObject(SYSTEM_FONT));
DeleteObject(fontHandle);
}
IntersectClipRect(item.hDC,item.rcItem.left,item.rcItem.top,item.rcItem.right,item.rcItem.bottom);
for(int i=0;i<4;++i)
{
const auto state=static_cast<FileRowState>(i);
const auto background=draft.palette[state==FileRowState::Focused?PanelCursor:PanelBlue];
const auto style=resolveFileStyle(extension,!systemHidden,systemHidden,
state==FileRowState::Focused || state==FileRowState::Marked,
draft.palette[Text],background,draft.styles,state);
auto decorated=font;
if(style.decorations)
{
decorated.lfUnderline=(style.decorations&1)!=0;
decorated.lfStrikeOut=(style.decorations&2)!=0;
}
const auto previewFont=CreateFontIndirectW(&decorated);
const auto previous=SelectObject(item.hDC,previewFont?previewFont:mono);
auto row=item.rcItem;
row.top+=i*height;
row.bottom=row.top+height;
SetBkColor(item.hDC,style.background);
ExtTextOutW(item.hDC,0,0,ETO_OPAQUE,&row,nullptr,0,nullptr);
constexpr const wchar_t *samples[]{L"normal-sample",L"marked-sample",L"focused-cursor",L"unfocused-cursor"};
const auto label=std::wstring(samples[i])+(systemHidden?L" (hidden)":std::wstring(extension));
row.left+=5;
row.right-=5;
SetBkMode(item.hDC,TRANSPARENT);
SetTextColor(item.hDC,style.foreground);
DrawTextW(item.hDC,label.c_str(),static_cast<int>(label.size()),&row,
DT_SINGLELINE|DT_VCENTER|DT_NOPREFIX|DT_END_ELLIPSIS);
SelectObject(item.hDC,previous);
if(previewFont) DeleteObject(previewFont);
}
}
RestoreDC(item.hDC,saved);
}
+180
View File
@@ -0,0 +1,180 @@
enum class FileRowState { Normal, Marked, Focused, Unfocused };
struct FileStyleColors
{
std::optional<COLORREF> foreground,background;
bool operator==(const FileStyleColors &) const = default;
};
struct FileStyle
{
std::wstring extension;
std::optional<COLORREF> foreground,background;
bool underline{},strikeout{};
std::array<FileStyleColors,3> states{};
bool operator==(const FileStyle &) const = default;
};
const std::vector<FileStyle> defaultFileStyles{
{L"@system-hidden"},{L".exe",RGB(85,255,85)},{L".md",RGB(231,192,247)},
{L".7z",RGB(255,210,128)},{L".zip",RGB(255,210,128)},{L".rar",RGB(255,210,128)},
{L".tar",RGB(255,210,128)},{L".gz",RGB(255,210,128)},
{L".png",RGB(165,230,255)},{L".jpg",RGB(165,230,255)},{L".jpeg",RGB(165,230,255)},
{L".gif",RGB(165,230,255)},{L".webp",RGB(165,230,255)},{L".svg",RGB(165,230,255)},
{L".mp3",RGB(255,185,216)},{L".wav",RGB(255,185,216)},{L".flac",RGB(255,185,216)},
{L".mp4",RGB(255,185,216)},{L".mkv",RGB(255,185,216)},{L".mov",RGB(255,185,216)},
{L".pdf",RGB(255,229,168)},{L".docx",RGB(255,229,168)},{L".xlsx",RGB(255,229,168)},
{L".pptx",RGB(255,229,168)},{L".odt",RGB(255,229,168)},
{L".cpp",RGB(168,247,190)},{L".h",RGB(168,247,190)},{L".cs",RGB(168,247,190)},
{L".py",RGB(168,247,190)},{L".js",RGB(168,247,190)},{L".ts",RGB(168,247,190)},
{L".ps1",RGB(168,247,190)},{L".bat",RGB(168,247,190)},{L".cmd",RGB(168,247,190)}};
std::vector<FileStyle> fileStyles=defaultFileStyles;
constexpr wchar_t fileStyleKey[]=L"FileStyles";
std::wstring fileStyleExtension(std::wstring_view text)
{
const auto first=text.find_first_not_of(L" \t");
if(first==text.npos) return {};
auto extension=std::wstring(text.substr(first,text.find_last_not_of(L" \t")-first+1));
if(extension.front()!=L'.') extension.insert(0,1,L'.');
if(extension.size()<2 || extension.size()>255 ||
extension.find_first_of(L". *?\\/:<>|\";\r\n\t",1)!=extension.npos) return {};
CharLowerBuffW(extension.data(),static_cast<DWORD>(extension.size()));
return extension;
}
bool sameFileExtension(std::wstring_view left,std::wstring_view right)
{
return !left.empty() && left.size()==right.size() &&
CompareStringOrdinal(left.data(),static_cast<int>(left.size()),
right.data(),static_cast<int>(right.size()),TRUE)==CSTR_EQUAL;
}
void loadFileStyles()
{
DWORD bytes{};
if(RegGetValueW(HKEY_CURRENT_USER,schemeKey,fileStyleKey,RRF_RT_REG_BINARY,nullptr,nullptr,&bytes)!=ERROR_SUCCESS ||
!bytes || bytes>256*1024) return;
std::string data(bytes,'\0');
if(RegGetValueW(HKEY_CURRENT_USER,schemeKey,fileStyleKey,RRF_RT_REG_BINARY,nullptr,data.data(),&bytes)!=ERROR_SUCCESS)
return;
try
{
const auto rows=Json::parse(data.begin(),data.begin()+bytes);
if(!rows.is_array() || rows.size()>64) return;
std::vector<FileStyle> parsed;
for(const auto &row:rows)
{
FileStyle style{ks::wide(row.at("extension").get<std::string>())};
if(style.extension!=L"@system-hidden" && fileStyleExtension(style.extension)!=style.extension) return;
if(std::any_of(parsed.begin(),parsed.end(),[&](const auto &existing){return existing.extension==style.extension;}))
return;
const auto color=[&](const char *key)->std::optional<COLORREF>
{
if(!row.contains(key)) return {};
const auto value=row.at(key).get<DWORD>();
if(value>0xffffff) throw std::runtime_error("Invalid file style color");
return value;
};
style.foreground=color("foreground");
style.background=color("background");
style.underline=row.value("underline",false);
style.strikeout=row.value("strikeout",false);
if(row.contains("states"))
{
const auto &states=row.at("states");
if(!states.is_array() || states.size()!=style.states.size()) return;
for(size_t i=0;i<style.states.size();++i)
{
const auto read=[&](const char *key)->std::optional<COLORREF>
{
if(!states[i].contains(key)) return {};
const auto value=states[i].at(key).get<DWORD>();
if(value>0xffffff) throw std::runtime_error("Invalid state color");
return value;
};
style.states[i]={read("foreground"),read("background")};
}
}
parsed.push_back(std::move(style));
}
fileStyles=std::move(parsed);
}
catch(const std::exception &) {}
}
bool saveFileStyles(const std::vector<FileStyle> &styles)
{
auto rows=Json::array();
for(const auto &style:styles)
{
Json row{{"extension",ks::utf8(style.extension)},{"underline",style.underline},{"strikeout",style.strikeout}};
if(style.foreground) row["foreground"]=*style.foreground;
if(style.background) row["background"]=*style.background;
row["states"]=Json::array();
for(const auto &state:style.states)
{
auto entry=Json::object();
if(state.foreground) entry["foreground"]=*state.foreground;
if(state.background) entry["background"]=*state.background;
row["states"].push_back(std::move(entry));
}
rows.push_back(std::move(row));
}
const auto data=rows.dump();
return RegSetKeyValueW(HKEY_CURRENT_USER,schemeKey,fileStyleKey,REG_BINARY,data.data(),
static_cast<DWORD>(data.size()))==ERROR_SUCCESS;
}
struct FileRowStyle
{
COLORREF foreground,background;
unsigned decorations{};
};
FileRowStyle resolveFileStyle(std::wstring_view extension,bool file,bool dimmed,bool protectedRow,
COLORREF foreground,COLORREF background,const std::vector<FileStyle> &styles=fileStyles,
FileRowState state=FileRowState::Normal,bool transferred=false)
{
FileRowStyle result{foreground,background};
const FileStyle *extensionRule=nullptr,*attributeRule=nullptr;
for(const auto &style:styles)
{
if(!file || !sameFileExtension(style.extension,extension)) continue;
extensionRule=&style;
result.foreground=style.foreground.value_or(result.foreground);
if(!protectedRow) result.background=style.background.value_or(result.background);
result.decorations=(style.underline?1u:0u)|(style.strikeout?2u:0u);
break;
}
if(dimmed)
for(const auto &style:styles)
{
if(style.extension!=L"@system-hidden") continue;
attributeRule=&style;
if(style.foreground) result.foreground=*style.foreground;
else
{
const auto fg=result.foreground,bg=result.background;
result.foreground=RGB((3*GetRValue(fg)+2*GetRValue(bg))/5,
(3*GetGValue(fg)+2*GetGValue(bg))/5,(3*GetBValue(fg)+2*GetBValue(bg))/5);
}
if(!protectedRow) result.background=style.background.value_or(result.background);
result.decorations|=(style.underline?1u:0u)|(style.strikeout?2u:0u);
break;
}
if(transferred && state!=FileRowState::Marked)
return {RGB(255,223,115),background,result.decorations};
if(state==FileRowState::Marked) result.foreground=RGB(255,255,0);
if(state!=FileRowState::Normal)
{
const auto index=static_cast<size_t>(state)-1;
for(const auto *rule:{extensionRule,attributeRule})
if(rule)
{
result.foreground=rule->states[index].foreground.value_or(result.foreground);
result.background=rule->states[index].background.value_or(result.background);
}
}
if(result.foreground==result.background && state!=FileRowState::Normal)
result.foreground=GetRValue(result.background)+GetGValue(result.background)+GetBValue(result.background)>382?RGB(0,0,0):RGB(255,255,255);
return result;
}
+3
View File
@@ -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
View File
@@ -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
View File
@@ -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];
+15
View File
@@ -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");