Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d5137887b9 | ||
|
|
c75cc989f4 | ||
|
|
f580892ca9 | ||
|
|
372c4cb8bd |
No files matched your search
+21
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
+230
-168
@@ -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);
|
||||
|
||||
@@ -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.");
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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