#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "files_terminal.h" #include "files_terminal_view.h" #include "files_lan_ipc.h" #include "lan_files.h" #include "platform.h" namespace { namespace fs = std::filesystem; using ks::lan::FilesConnection; using ks::lan::FilesPeer; using ks::lan::Json; using ks::lan::fileRequest; using ks::lan::filePath; using ks::lan::checkFileReply; struct FileInfo { DWORD attributes; uint64_t size, modified; }; FileInfo fileInfo(const Json &row) { return {row.at("attributes"),row.at("size"),row.at("modified")}; } std::optional statFile(FilesConnection &host,const fs::path &path) { const auto reply=host.call(fileRequest("stat",path)); const auto error=reply.at("error").get(); if(error==ERROR_FILE_NOT_FOUND || error==ERROR_PATH_NOT_FOUND) return {}; checkFileReply(reply); return fileInfo(reply); } using FileVisitor=std::function; bool listFiles(FilesConnection &host,const fs::path &directory,const FileVisitor &visit) { auto page=host.call(fileRequest("list",directory)); for(;;) { checkFileReply(page); for(const auto &row:page.at("entries")) if(!visit(directory/filePath(row.at("name")),fileInfo(row))) return false; if(!page.at("more").get()) return true; page=host.call({{"action","listNext"}}); } } std::wstring fileError(const std::system_error &error) { wchar_t *message{}; const DWORD length=FormatMessageW(FORMAT_MESSAGE_ALLOCATE_BUFFER|FORMAT_MESSAGE_FROM_SYSTEM| FORMAT_MESSAGE_IGNORE_INSERTS,nullptr,error.code().value(),0,reinterpret_cast(&message),0,nullptr); std::wstring text=length?std::wstring(message,length):L"Windows error "+std::to_wstring(error.code().value()); LocalFree(message); while(!text.empty() && (text.back()==L'\r' || text.back()==L'\n')) text.pop_back(); return text; } std::wstring formatSize(uint64_t bytes,size_t columns=0) { constexpr const wchar_t *units[]{L"B",L"KiB",L"MiB",L"GiB",L"TiB",L"PiB",L"EiB"}; constexpr const wchar_t *compact[]{L"",L"K",L"M",L"G",L"T",L"P",L"E"}; if(columns) { for(size_t unit=0;;++unit,bytes/=1024) { auto label=std::to_wstring(bytes)+compact[unit]; if(label.size()<=columns || unit==std::size(compact)-1) return label; } } size_t unit=0; double value=static_cast(bytes); while(value>=1024 && unit+1(buffer); for(;;) { std::wstring_view name(entry->FileName,entry->FileNameLength/sizeof(wchar_t)); if(name!=L"." && name!=L"..") { const FolderEntryMetadata metadata{entry->FileAttributes, static_cast(entry->EndOfFile.QuadPart)}; std::wstring row(sizeof(metadata)/sizeof(wchar_t),L'\0'); memcpy(row.data(),&metadata,sizeof(metadata)); row.append(name); DWORD written{}; if(!WriteFile(pipe.value,row.data(),static_cast(row.size()*sizeof(wchar_t)),&written,nullptr)) return GetLastError(); } if(!entry->NextEntryOffset) break; entry=reinterpret_cast( reinterpret_cast(entry)+entry->NextEntryOffset); } } const auto error=GetLastError(); return error==ERROR_NO_MORE_FILES?ERROR_SUCCESS:error; } DWORD requestElevatedFolder(HWND owner,const fs::path &directory, std::vector &folders,std::vector &files, std::map &metadata) { GUID id{}; wchar_t token[40]{}, executable[32768]{}; auto length=GetModuleFileNameW(nullptr,executable,static_cast(std::size(executable))); if(!length || length>=std::size(executable) || FAILED(CoCreateGuid(&id)) || !StringFromGUID2(id,token,static_cast(std::size(token)))) return ERROR_GEN_FAILURE; auto name=std::wstring(folderPipePrefix)+token; Handle pipe{CreateNamedPipeW(name.c_str(),PIPE_ACCESS_DUPLEX|FILE_FLAG_FIRST_PIPE_INSTANCE, PIPE_TYPE_MESSAGE|PIPE_READMODE_MESSAGE|PIPE_NOWAIT|PIPE_REJECT_REMOTE_CLIENTS,1,0,4096,0,nullptr)}; if(pipe.value==INVALID_HANDLE_VALUE) return GetLastError(); auto parameters=L"--list-folder \""+(directory/L".").wstring()+L"\" \""+name+L"\""; SHELLEXECUTEINFOW launch{}; launch.cbSize=sizeof(launch); launch.fMask=SEE_MASK_NOCLOSEPROCESS|SEE_MASK_NOASYNC; launch.hwnd=owner; launch.lpVerb=L"runas"; launch.lpFile=executable; launch.lpParameters=parameters.c_str(); launch.nShow=SW_HIDE; if(!ShellExecuteExW(&launch)) return GetLastError(); Handle process{launch.hProcess}; bool connected=false; bool finished=false; auto deadline=GetTickCount64()+10000; while(GetTickCount64() peer; struct DirectoryWatch { HANDLE handle=INVALID_HANDLE_VALUE; ~DirectoryWatch() { reset(); } void reset() { if(handle!=INVALID_HANDLE_VALUE) FindCloseChangeNotification(handle); handle=INVALID_HANDLE_VALUE; } void start(const fs::path &path) { reset(); handle=FindFirstChangeNotificationW(path.c_str(),FALSE, FILE_NOTIFY_CHANGE_FILE_NAME|FILE_NOTIFY_CHANGE_DIR_NAME| FILE_NOTIFY_CHANGE_ATTRIBUTES|FILE_NOTIFY_CHANGE_SIZE|FILE_NOTIFY_CHANGE_LAST_WRITE); } } watch; std::map timestamps; std::vector rows; std::vector sizes; std::vector dimmed; std::vector marked; std::set transferResults; size_t fileExtensionLength{}, folderExtensionLength{}; bool wide{}; }; Panel panels[2]; FilesTerminal terminal; std::ofstream terminalTrace; unsigned terminalTraceLines{}; bool traceTerminalOutput{}; void traceTerminal(const char *event,int a=0,int b=0,int c=0,int d=0,int e=0) { if(!terminalTrace.is_open() || terminalTraceLines>=4096) return; ++terminalTraceLines; terminalTrace< terminalCommandPanels; std::wstring terminalDirectory; int terminalRows{}; struct CommandEntry { std::wstring date, command; }; std::vector terminalHistory; size_t terminalHistoryIndex{}; constexpr UINT_PTR transferHighlightTimer=1; unsigned transferHighlightPhase{}; HWND status{}, commands[6]{}; int active{}, split{}; bool dragging{}; bool rememberElevationApproval{}; HWND picker{}, pickerFrame{}, menuBar{}; HWND menuItems[5]{}; constexpr std::wstring_view menuAccessKeys=L"LFCOR"; std::vector commandAccessPositions; int menuIndex{}; bool commandPicker{}; 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 drivePaths; int driveHover=-1; std::array driveColumnStarts{}; HWND drivePeerService{}; bool drivePeerReplyReceived{}; std::wstring drivePeerLookupError; std::vector drivePeers; struct DriveQuery { FilesPeer peer; std::future pending; Json drives=Json::array(); std::wstring error; }; struct DriveChoice { FilesPeer peer; fs::path path; }; std::vector driveQueries; std::map remoteDriveRows; constexpr UINT_PTR driveQueryTimer=3; bool refreshDriveAddresses{}; ULONGLONG driveOpened{}; int drivePickerWidth{}; int pickerPanel{}; std::wstring panelSearchPrefix; HWND panelSearchPopup{}; enum Color { PanelBlue, Text, Frame, Dialog, Selection, Footer, PanelCursor, ColorCount }; constexpr std::array 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"; bool restorePlacement(HWND window) { WINDOWPLACEMENT placement{}; DWORD size=sizeof(placement); if(RegGetValueW(HKEY_CURRENT_USER,schemeKey,placementPreference,RRF_RT_REG_BINARY, nullptr,&placement,&size)!=ERROR_SUCCESS || size!=sizeof(placement) || placement.length!=sizeof(placement)) return false; const auto &rect=placement.rcNormalPosition; if(static_cast(rect.right)-rect.left<540 || static_cast(rect.bottom)-rect.top<220) return false; if(!MonitorFromRect(&rect,MONITOR_DEFAULTTONULL)) return false; placement.flags=0; placement.showCmd=placement.showCmd==SW_SHOWMAXIMIZED?SW_SHOWMAXIMIZED:SW_SHOWNORMAL; if(!SetWindowPlacement(window,&placement)) return false; ShowWindow(window,static_cast(placement.showCmd)); return true; } void savePlacement(HWND window) { WINDOWPLACEMENT placement{}; placement.length=sizeof(placement); if(!GetWindowPlacement(window,&placement)) return; const bool maximized=IsZoomed(window) || (IsIconic(window) && (placement.flags&WPF_RESTORETOMAXIMIZED)); placement.showCmd=maximized?SW_SHOWMAXIMIZED:SW_SHOWNORMAL; placement.flags=0; RegSetKeyValueW(HKEY_CURRENT_USER,schemeKey,placementPreference,REG_BINARY, &placement,sizeof(placement)); } void chooseDrive(int index, bool fromMenu = false); void openMenu(HWND window, int index); void showFileDetails(const Panel &panel); void editOptions(HWND owner); void showFolderShortcuts(Panel &panel,int digit,bool all=false); void layout(HWND window); DWORD preference(const wchar_t *name, DWORD fallback) { DWORD value{}, size=sizeof(value); return RegGetValueW(HKEY_CURRENT_USER,schemeKey,name,RRF_RT_REG_DWORD,nullptr,&value,&size) == ERROR_SUCCESS ? value : fallback; } const UINT filesPreferencesChanged=RegisterWindowMessageW(L"KSwitcher.Files.PreferencesChanged"); void notifyFilesPreferences(HWND source) { SendNotifyMessageW(HWND_BROADCAST,filesPreferencesChanged,reinterpret_cast(source),0); } void savePreference(const wchar_t *name, DWORD value) { RegSetKeyValueW(HKEY_CURRENT_USER,schemeKey,name,REG_DWORD,&value,sizeof(value)); } #include "files_styles.inl" void setStatus(const std::wstring &message); void refreshCommandHistory(); #include "files_command_history.inl" constexpr const wchar_t *panelPathKeys[]{L"LeftPath",L"RightPath"}; constexpr const wchar_t *panelCursorKeys[]{L"LeftCursor",L"RightCursor"}; fs::path savedPanelPath(const wchar_t *key) { DWORD size{}; if(RegGetValueW(HKEY_CURRENT_USER,schemeKey,key,RRF_RT_REG_SZ, nullptr,nullptr,&size)!=ERROR_SUCCESS || size(parameter); if(reinterpret_cast(SendMessageW(window,WM_GETFONT,0,0))==old) SendMessageW(window,WM_SETFONT,reinterpret_cast(mono),FALSE); return TRUE; } 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 fileStyleFonts{}; bool applyFont(HWND owner, LOGFONTW font, int points) { const auto dpi=GetDpiForWindow(owner); font.lfHeight=-MulDiv(points,dpi,720); font.lfWidth=0; auto next=CreateFontIndirectW(&font); if(!next) return false; auto dc=GetDC(owner); if(!dc){DeleteObject(next);return false;} auto previous=SelectObject(dc,next); TEXTMETRICW metrics{}; GetTextMetricsW(dc,&metrics); SelectObject(dc,previous); ReleaseDC(owner,dc); textHeight=std::max(static_cast(MulDiv(16,dpi,96)),metrics.tmHeight+MulDiv(6,dpi,96)); cellWidth=std::max(1L,metrics.tmAveCharWidth); auto old=mono; mono=next; for(unsigned i=1;i(mono),FALSE); if(panel.list) SendMessageW(panel.list,LB_SETITEMHEIGHT,0,textHeight); } if(picker) SendMessageW(picker,LB_SETITEMHEIGHT,0,textHeight); if(status) SendMessageW(status,WM_SETFONT,reinterpret_cast(mono),FALSE); for(const auto control:{terminalInput,terminalOutput,terminalPrompt}) if(control) SendMessageW(control,WM_SETFONT,reinterpret_cast(mono),FALSE); if(old) { EnumThreadWindows(GetCurrentThreadId(),[](HWND window,LPARAM parameter)->BOOL { EnumChildWindows(window,updateDialogFont,parameter); return TRUE; },reinterpret_cast(old)); DeleteObject(old); } return true; } bool chooseDisplayFont(HWND owner,LOGFONTW &font,int &points) { 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 false; points=dialog.iPointSize; return true; } void refreshColors() { if (blue) DeleteObject(blue); if (teal) DeleteObject(teal); blue = CreateSolidBrush(colors[PanelBlue]); teal = CreateSolidBrush(colors[Dialog]); } void loadColors() { for (DWORD i=0; i=text.size()) return; SIZE prefix{}; GetTextExtentPoint32W(dc,text.c_str(),static_cast(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); } struct DialogFramePadding { DWORD outer,inner,rowText{}; bool operator==(const DialogFramePadding &) const=default; }; // Delete at 96 DPI: outer line at x=8, inner line at x=10, content at x=25. constexpr DialogFramePadding defaultDialogFramePadding{8,14}; constexpr DWORD maxDialogFramePadding=24; constexpr DWORD maxDialogRowTextPadding=4; constexpr wchar_t dialogFrameOuterKey[]=L"DialogFrameOuterPadding"; constexpr wchar_t dialogFrameInnerKey[]=L"DialogFrameInnerPadding"; constexpr wchar_t dialogRowTextKey[]=L"DialogRowTextPadding"; auto dialogFramePadding=defaultDialogFramePadding; struct DialogFrameMetrics { int outer,gap,inner; int innerLine() const {return outer+gap;} int content() const {return innerLine()+1+inner;} }; DialogFrameMetrics dialogFrameMetrics(UINT dpi,DialogFramePadding padding=dialogFramePadding) { return {MulDiv(padding.outer,dpi,96),std::max(2,MulDiv(2,dpi,96)),MulDiv(padding.inner,dpi,96)}; } void paintDialogCaption(HDC dc,RECT row,const wchar_t *caption,HBRUSH surface,COLORREF ink,HFONT font) { const int saved=SaveDC(dc); SelectObject(dc,font); SIZE extent{}; GetTextExtentPoint32W(dc,caption,lstrlenW(caption),&extent); TEXTMETRICW metrics{}; GetTextMetricsW(dc,&metrics); const LONG width=std::min(row.right-row.left,extent.cx+2*metrics.tmAveCharWidth); row.left+=(row.right-row.left-width)/2; row.right=row.left+width; FillRect(dc,&row,surface); SetBkMode(dc,TRANSPARENT); SetTextColor(dc,ink); DrawTextW(dc,caption,-1,&row,DT_CENTER|DT_VCENTER|DT_SINGLELINE|DT_END_ELLIPSIS|DT_NOPREFIX); if(saved) RestoreDC(dc,saved); } RECT paintDialogFrame(HDC dc,RECT bounds,DialogFrameMetrics metrics,HBRUSH surface,HBRUSH border) { FillRect(dc,&bounds,surface); InflateRect(&bounds,-metrics.outer,-metrics.outer); FrameRect(dc,&bounds,border); InflateRect(&bounds,-metrics.gap,-metrics.gap); FrameRect(dc,&bounds,border); return bounds; } void paintDialogFrameCaptions(HDC dc,RECT client,RECT frame,const wchar_t *title, HBRUSH surface,COLORREF ink,HFONT font,const wchar_t *hints=nullptr) { const auto previous=SelectObject(dc,font); TEXTMETRICW text{}; GetTextMetricsW(dc,&text); SelectObject(dc,previous); const auto top=std::max(client.top,frame.top-text.tmHeight/2); if(title) paintDialogCaption(dc,{frame.left,top,frame.right,top+text.tmHeight},title,surface,ink,font); const auto bottom=std::min(client.bottom,frame.bottom+(text.tmHeight+1)/2); if(hints) paintDialogCaption(dc,{frame.left,bottom-text.tmHeight,frame.right,bottom},hints,surface,ink,font); } int dialogListInset(DialogFrameMetrics metrics,int fontHeight) { return std::max(metrics.content(),metrics.innerLine()+(fontHeight+1)/2); } int dialogRowTextInset(int characterWidth,DialogFramePadding padding=dialogFramePadding) { return characterWidth*static_cast(padding.rowText); } void paintDialogListRow(HDC dc,RECT bounds,std::wstring_view label,HFONT font, COLORREF foreground,COLORREF background,int inset,UINT ellipsis=DT_PATH_ELLIPSIS) { const auto saved=SaveDC(dc); SetBkColor(dc,background); ExtTextOutW(dc,0,0,ETO_OPAQUE,&bounds,nullptr,0,nullptr); SelectObject(dc,font); SetTextColor(dc,foreground); SetBkMode(dc,TRANSPARENT); InflateRect(&bounds,-inset,0); if(bounds.right>bounds.left) DrawTextW(dc,label.data(),static_cast(label.size()),&bounds, DT_SINGLELINE|DT_VCENTER|DT_NOPREFIX|ellipsis); if(saved) RestoreDC(dc,saved); } void showMenu(bool show); void hidePicker() { if (!picker || !IsWindowVisible(picker)) return; KillTimer(GetParent(picker),driveQueryTimer); driveQueries.clear(); ShowWindow(picker,SW_HIDE); ShowWindow(pickerFrame,SW_HIDE); InvalidateRect(GetParent(picker),nullptr,FALSE); SetFocus(panels[commandPicker ? active : pickerPanel].list); if (IsWindowVisible(menuBar)) showMenu(true); } void selectDrive(); void showMenu(bool show) { if (show) SetWindowPos(menuBar,HWND_TOP,0,0,0,0,SWP_NOMOVE|SWP_NOSIZE|SWP_SHOWWINDOW); for (auto item : menuItems) { if (show) SetWindowPos(item,HWND_TOP,0,0,0,0,SWP_NOMOVE|SWP_NOSIZE|SWP_SHOWWINDOW); else ShowWindow(item,SW_HIDE); if (show) InvalidateRect(item,nullptr,FALSE); } if (!show) ShowWindow(menuBar,SW_HIDE); RedrawWindow(GetParent(menuBar),nullptr,nullptr,RDW_INVALIDATE|RDW_ALLCHILDREN); } LRESULT scrollList(HWND list, UINT message, WPARAM wp, LPARAM lp) { if (message == WM_MOUSEWHEEL) { int delta = GET_WHEEL_DELTA_WPARAM(wp); int top = static_cast(SendMessageW(list, LB_GETTOPINDEX, 0, 0)); int count = static_cast(SendMessageW(list, LB_GETCOUNT, 0, 0)); RECT rect{}; GetClientRect(list, &rect); int rowHeight = static_cast(SendMessageW(list, LB_GETITEMHEIGHT, 0, 0)); int visible = std::max(1, static_cast(rect.bottom) / std::max(1, rowHeight)); SendMessageW(list, LB_SETTOPINDEX, std::clamp(top - (delta / WHEEL_DELTA) * 3, 0, std::max(0, count - visible)), 0); return 0; } return DefSubclassProc(list, message, wp, lp); } void paintScrollPosition(HWND parent, HDC dc, HWND list, COLORREF background) { RECT track{}; GetWindowRect(list, &track); MapWindowPoints(nullptr, parent, reinterpret_cast(&track), 2); track.left = track.right; track.right += 5; SetDCBrushColor(dc, background); FillRect(dc, &track, static_cast(GetStockObject(DC_BRUSH))); int count = static_cast(SendMessageW(list, LB_GETCOUNT, 0, 0)); int rowHeight = static_cast(SendMessageW(list, LB_GETITEMHEIGHT, 0, 0)); int height = track.bottom - track.top; int visible = std::max(1, height / std::max(1, rowHeight)); if (height <= 0 || count <= visible) return; int thumb = std::min(height, std::max(5, MulDiv(height, visible, count))); int top = static_cast(SendMessageW(list, LB_GETTOPINDEX, 0, 0)); track.top += MulDiv(std::clamp(top, 0, count - visible), height - thumb, count - visible); track.bottom = track.top + thumb; SetDCBrushColor(dc, colors[Frame]); FillRect(dc, &track, static_cast(GetStockObject(DC_BRUSH))); } void placePanelSearch(HWND window) { RECT panel{}; GetWindowRect(panels[active].list,&panel); MapWindowPoints(nullptr,window,reinterpret_cast(&panel),2); const auto frame=dialogFrameMetrics(GetDpiForWindow(window)); const int width=std::min(static_cast(panel.right-panel.left),24*cellWidth+2*frame.content()); const int height=textHeight+2*frame.content(); SetWindowPos(panelSearchPopup,HWND_TOP,(panel.left+panel.right-width)/2, std::max(panel.top,panel.bottom+textHeight-height),width,height,SWP_NOACTIVATE); } void closePanelSearch() { panelSearchPrefix.clear(); if(IsWindowVisible(panelSearchPopup)) ShowWindow(panelSearchPopup,SW_HIDE); } void layout(HWND window) { RECT rect{}; GetClientRect(window, &rect); int width = rect.right, height = rect.bottom; const int inputTop=height-2*textHeight; const int terminalBottom=std::max(4,inputTop); const int detailsHeight=terminalVisible?0:textHeight+12; const int terminalTop=terminalVisible?4:std::max(4,terminalBottom- std::min(terminalRows,std::max(0,(terminalBottom-detailsHeight)/textHeight-4))*textHeight); const int panelBottom=terminalTop-detailsHeight; const bool showOutput=terminalTop"); SetWindowTextW(terminalPrompt,prompt.c_str()); SIZE promptExtent{static_cast(prompt.size())*cellWidth,0}; if(const auto dc=GetDC(window)) { const auto previous=SelectObject(dc,mono); GetTextExtentPoint32W(dc,prompt.c_str(),static_cast(prompt.size()),&promptExtent); SelectObject(dc,previous); ReleaseDC(window,dc); } const int promptWidth=std::min(width/2,static_cast(promptExtent.cx)); place(terminalPrompt,4,inputTop,promptWidth,textHeight); place(terminalInput,4+promptWidth,inputTop,std::max(0,width-8-promptWidth),textHeight); // Keep the WebView in place while the panels reveal more or less of it. place(terminalOutput,4,4,std::max(0,width-8),terminalBottom-4); if(showOutput) { const auto region=CreateRectRgn(0,terminalTop-4,std::max(0,width-8),terminalBottom-4); if(region && !SetWindowRgn(terminalOutput,region,FALSE)) DeleteObject(region); } ShowWindow(terminalOutput,showOutput?SW_SHOWNA:SW_HIDE); ShowWindow(status,terminalVisible?SW_HIDE:SW_SHOWNA); terminalView.layout(terminalOutput,showOutput,terminalBottom-4); for (int i = 0; i < static_cast(std::size(commands)); ++i) { const int count=static_cast(std::size(commands)); place(commands[i],i*width/count,height-textHeight,(i+1)*width/count-i*width/count,textHeight); } if(IsWindowVisible(panelSearchPopup)) placePanelSearch(window); RedrawWindow(window,nullptr,nullptr,RDW_INVALIDATE|RDW_ERASE|RDW_ALLCHILDREN); } void setStatus(const std::wstring &message) { SetWindowTextW(status, message.c_str()); } void showTerminal(HWND owner,bool show) { terminalVisible=show; for(const auto &panel:panels) { ShowWindow(panel.path,show?SW_HIDE:SW_SHOWNA); ShowWindow(panel.list,show?SW_HIDE:SW_SHOWNA); } layout(owner); SetFocus(terminalInput); } void open(Panel &panel,const fs::path &directory,bool offerElevation); void activatePanel(int index) { const auto previous=active; active=index; showFileDetails(panels[index]); if(previous==active) return; for(const auto &panel:panels) { InvalidateRect(panel.path,nullptr,FALSE); InvalidateRect(panel.list,nullptr,FALSE); } layout(GetParent(panels[index].list)); } std::wstring terminalInputText() { std::wstring value(GetWindowTextLengthW(terminalInput)+1,L'\0'); value.resize(GetWindowTextW(terminalInput,value.data(),static_cast(value.size()))); return value; } void copyTerminalInput(HWND owner) { const auto value=terminalInputText(); if(value.empty()) return; try { ks::writeClipboard(owner,ks::textClip(value)); } catch(const std::exception &) { setStatus(L"Could not copy the command line."); } } LRESULT currentPanelRow(const Panel &panel) { const auto row=SendMessageW(panel.list,LB_GETCURSEL,0,0); return row>=0 && static_cast(row)(quoted.empty()?path.c_str():quoted.c_str())); } bool startTerminal(HWND owner) { if(terminal.alive()) return true; if(!terminalViewRequested) { terminalViewRequested=terminalView.create(terminalOutput,[owner](const std::wstring &message) { if(message==L"ready") { terminalView.setFont(displayFont,fontPoints); if(terminalTrace.is_open()) terminalView.enableTrace(traceTerminalOutput); } else if(traceTerminalOutput && terminalTrace.is_open() && message.starts_with(L"output:")) terminalTrace<<"output "<=0 && phase<=3) { const char *events[]{"web-before-fit","web-after-fit","web-anchor","web-scroll"}; traceTerminal(events[phase],height,rows,viewport,base,offset); } } else if(message.starts_with(L"directory:")) { const auto directory=message.substr(10); if(!terminalCommandPanels.empty()) { const int panel=terminalCommandPanels.front(); terminalCommandPanels.pop_front(); std::error_code error; if(panel>=0 && !panels[panel].peer && directory!=terminalDirectory && !directory.empty() && fs::is_directory(directory,error)) open(panels[panel],directory,false); } terminalDirectory=directory; } else if(message==L"focus-input") { if(GetForegroundWindow()==owner) SetFocus(terminalInput); } else if(message==L"insert-path") insertCurrentPanelPath(); else if(message==L"copy-command-line") SendMessageW(terminalInput,WM_COPY,0,0); else if(message==L"toggle") showTerminal(owner,!terminalVisible); else if(!terminalVisible && (message==L"grow" || message==L"shrink")) { terminalRows=std::max(0,terminalRows+(message==L"grow"?1:-1)); layout(owner); } else if(message.starts_with(L"size:")) { int columns{},rows{}; if(swscanf_s(message.c_str()+5,L"%d,%d",&columns,&rows)==2 && columns>=2 && columns<=32767 && rows>=2 && rows<=32767) { terminalSize={static_cast(columns),static_cast(rows)}; terminal.resize(terminalSize); } } },[owner] { showTerminal(owner,false); setStatus(L"Could not initialize the terminal view."); }); if(!terminalViewRequested) { setStatus(L"WebView2 and the local terminal assets are required."); return false; } } if(panels[active].peer) { setStatus(L"Select a local panel to start the terminal."); return false; } if(terminal.start(panels[active].directory.c_str(),terminalSize)) { terminalCommandPanels={-1}; terminalDirectory.clear(); return true; } setStatus(L"Could not start PowerShell."); return false; } void updateTerminalOutput() { if(!terminalView.initialized()) return; const auto bytes=terminal.takeOutput(); if(!bytes.empty()) terminalView.send(bytes); } bool submitTerminalCommand(const Panel &panel,const std::wstring &value) { if(!startTerminal(GetParent(panel.list))) return false; if(!terminal.write(value+L"\r")) { setStatus(L"Could not send PowerShell input."); return false; } terminalCommandPanels.push_back(static_cast(&panel-panels)); return true; } LRESULT CALLBACK terminalInputKeys(HWND input,UINT message,WPARAM wp,LPARAM lp,UINT_PTR,DWORD_PTR) { if(message==WM_COPY || (message==WM_CHAR && wp==3)) { copyTerminalInput(GetParent(input)); return 0; } if(message==WM_CHAR && wp==VK_TAB && !terminalVisible) return 0; if(message==WM_CHAR && wp==VK_ESCAPE) { recallCommand(terminalHistory.size()); return 0; } if((message==WM_CHAR && wp==22) || (message==WM_KEYDOWN && wp==VK_INSERT && (GetKeyState(VK_SHIFT)&0x8000))) { SendMessageW(input,WM_PASTE,0,0); return 0; } // The edit control receives Ctrl+Backspace as DEL. if(message==WM_CHAR && wp==127) { DWORD first{},last{}; SendMessageW(input,EM_GETSEL,reinterpret_cast(&first),reinterpret_cast(&last)); if(first==last && first) { std::wstring text(GetWindowTextLengthW(input)+1,L'\0'); GetWindowTextW(input,text.data(),static_cast(text.size())); while(first && iswspace(text[first-1])) --first; while(first && !iswspace(text[first-1])) --first; SendMessageW(input,EM_SETSEL,first,last); } SendMessageW(input,EM_REPLACESEL,TRUE,reinterpret_cast(L"")); return 0; } if(message==WM_CHAR && wp==VK_RETURN) { if(!terminalVisible && !GetWindowTextLengthW(input)) { SendMessageW(panels[active].list,WM_KEYDOWN,VK_RETURN,lp); return 0; } const auto value=terminalInputText(); if(!submitTerminalCommand(panels[active],value)) return 0; if(value.find_first_not_of(L" \t\r\n")!=std::wstring::npos) rememberCommand(value); terminalHistoryIndex=terminalHistory.size(); SetWindowTextW(input,L""); return 0; } if(message==WM_KEYDOWN && !terminalVisible && wp==VK_TAB) { activatePanel(1-active); return 0; } if(message==WM_KEYDOWN && !terminalVisible && ((wp>=VK_F1 && wp<=VK_F12) || wp==VK_UP || wp==VK_DOWN || wp==VK_PRIOR || wp==VK_NEXT || (!GetWindowTextLengthW(input) && (wp==VK_HOME || wp==VK_END || wp==VK_LEFT || wp==VK_RIGHT || wp==VK_INSERT || wp==VK_DELETE)))) { SendMessageW(panels[active].list,WM_KEYDOWN,wp,lp); return 0; } if(message==WM_KEYDOWN && (wp==VK_UP || wp==VK_DOWN)) { if(wp==VK_UP && terminalHistoryIndex) recallCommand(terminalHistoryIndex-1); if(wp==VK_DOWN && terminalHistoryIndex(row)>=panel.rows.size()) { setStatus(L""); return; } const auto &path=panel.rows[row]; WIN32_FILE_ATTRIBUTE_DATA data{}; SYSTEMTIME modified{}; wchar_t date[64]{}; bool haveTime=false; if(panel.peer) { if(const auto found=panel.timestamps.find(path); found!=panel.timestamps.end()) { data.ftLastWriteTime={static_cast(found->second),static_cast(found->second>>32)}; haveTime=true; } } else haveTime=GetFileAttributesExW(path.c_str(),GetFileExInfoStandard,&data)!=FALSE; if(haveTime && FileTimeToSystemTime(&data.ftLastWriteTime,&modified)) { SYSTEMTIME local{}; if(SystemTimeToTzSpecificLocalTime(nullptr,&modified,&local)) swprintf_s(date,L" %04u-%02u-%02u %02u:%02u",unsigned(local.wYear), unsigned(local.wMonth),unsigned(local.wDay),unsigned(local.wHour),unsigned(local.wMinute)); } std::wstring details; if(statusFields&1) details=path.filename().wstring(); if(statusFields&2) { const auto &size=panel.sizes[row]; details+=L" "+(size==L"Folder" || size==L"Up"?size:formatSize(std::stoull(size))); } std::wstring timestamp=date; if((statusFields&4) && timestamp.size()>=12) details+=timestamp.substr(0,12); if((statusFields&8) && timestamp.size()>=18) details+=timestamp.substr(12); setStatus(details); } void openOn(Panel &panel,const fs::path &directory,bool offerElevation,std::optional peer) { const auto previousDirectory=(panel.peer?panel.peer->id:std::string{})== (peer?peer->id:std::string{})?panel.directory:fs::path{}; const bool sameDirectory=previousDirectory==directory; // Start watching before enumeration so changes during a scan trigger another refresh. Panel::DirectoryWatch nextWatch; if(!peer && (!sameDirectory || panel.watch.handle==INVALID_HANDLE_VALUE)) nextWatch.start(directory); WIN32_FIND_DATAW data{}; const auto search=peer?INVALID_HANDLE_VALUE:FindFirstFileW((directory/L"*").c_str(),&data); auto error=search==INVALID_HANDLE_VALUE ? GetLastError() : ERROR_SUCCESS; std::vector folders, files; std::map metadata; std::map timestamps; if(peer) { try { FilesConnection host(&*peer); listFiles(host,directory,[&](const fs::path &path,const FileInfo &info) { (info.attributes&FILE_ATTRIBUTE_DIRECTORY?folders:files).push_back(path); metadata.emplace(path,FolderEntryMetadata{info.attributes,info.size}); timestamps.emplace(path,info.modified); return true; }); error=ERROR_SUCCESS; } catch(const std::system_error &e){setStatus(peer->name+L" — "+fileError(e));return;} catch(const std::exception &){setStatus(L"Cannot open folder on "+peer->name);return;} } if (error!=ERROR_SUCCESS && error!=ERROR_FILE_NOT_FOUND) { const bool denied=error==ERROR_ACCESS_DENIED; setStatus(denied ? L"Access denied: " + directory.wstring() : L"Cannot open folder: " + directory.wstring()); if(offerElevation && denied && !IsUserAnAdmin()) { auto owner=GetParent(panel.list); auto path=directory.wstring(); const TASKDIALOG_BUTTON buttons[]{{IDYES,L"Continue as administrator"},{IDCANCEL,L"Skip"}}; TASKDIALOGCONFIG dialog{}; dialog.cbSize=sizeof(dialog); dialog.hwndParent=owner; dialog.dwFlags=TDF_ALLOW_DIALOG_CANCELLATION|TDF_POSITION_RELATIVE_TO_WINDOW; dialog.pszWindowTitle=L"KSwitcher Files"; dialog.pszMainInstruction=L"Windows denied access to this folder"; dialog.pszContent=path.c_str(); dialog.pszVerificationText=L"Remember Continue for this session (Windows may still ask for UAC)"; dialog.cButtons=static_cast(std::size(buttons)); dialog.pButtons=buttons; dialog.nDefaultButton=IDCANCEL; BOOL skip{}; int choice=IDYES; if(!rememberElevationApproval && FAILED(TaskDialogIndirect(&dialog,&choice,nullptr,&skip))) choice=MessageBoxW(owner,(path+L"\n\nTry opening as administrator?").c_str(), L"KSwitcher Files",MB_YESNO|MB_DEFBUTTON2|MB_ICONQUESTION); if(choice==IDYES) { const auto result=requestElevatedFolder(owner,directory,folders,files,metadata); SetForegroundWindow(owner); SetFocus(panel.list); if(result!=ERROR_SUCCESS) { setStatus(result==ERROR_ACCESS_DENIED ? L"Windows denied access after elevation: "+path : L"Could not read folder as administrator ("+std::to_wstring(result)+L"): "+path); return; } rememberElevationApproval=rememberElevationApproval || skip!=FALSE; error=ERROR_SUCCESS; } } if(error) return; } if(search!=INVALID_HANDLE_VALUE) { do { const std::wstring_view name(data.cFileName); if(name==L"." || name==L"..") continue; const auto path=directory/name; const auto size=(static_cast(data.nFileSizeHigh)<<32)|data.nFileSizeLow; (data.dwFileAttributes&FILE_ATTRIBUTE_DIRECTORY?folders:files).push_back(path); metadata.emplace(path,FolderEntryMetadata{data.dwFileAttributes,size}); } while(FindNextFileW(search,&data)); error=GetLastError(); FindClose(search); } std::sort(folders.begin(), folders.end()); std::sort(files.begin(), files.end()); std::set markedPaths; fs::path previousSelection; LRESULT previousRow=0, previousTop=0; if(sameDirectory) { previousRow=SendMessageW(panel.list,LB_GETCURSEL,0,0); previousTop=SendMessageW(panel.list,LB_GETTOPINDEX,0,0); if(previousRow>=0 && static_cast(previousRow)(label.c_str())) >= 0) { panel.rows.push_back(path); if(label!=L"..") { const auto extension=path.extension().native(); auto &longest=folder?panel.folderExtensionLength:panel.fileExtensionLength; if(extension.size()>1) longest=std::max(longest,extension.size()-1); } panel.marked.push_back(label!=L".." && markedPaths.contains(path)); const auto entry=label==L".." ? FolderEntryMetadata{} : metadata.at(path); panel.dimmed.push_back((entry.attributes&(FILE_ATTRIBUTE_SYSTEM|FILE_ATTRIBUTE_HIDDEN))!=0); panel.sizes.push_back(folder ? (label==L".." ? L"Up" : L"Folder") : std::to_wstring(entry.size)); } }; if (directory.has_relative_path()) add(directory.parent_path(), L"..", true); for (const auto &folder : folders) add(folder, L"[" + folder.filename().wstring() + L"]", true); for (const auto &file : files) add(file, file.filename().wstring(), false); const auto label=(panel.peer?panel.peer->name+L" · ":std::wstring{})+directory.native(); SetWindowTextW(panel.path,label.c_str()); if(&panel==&panels[active] && !sameDirectory) layout(GetParent(panel.list)); auto selected=std::find(panel.rows.begin(),panel.rows.end(),previousSelection); if(selected==panel.rows.end() && !panel.rows.empty()) selected=panel.rows.begin()+std::clamp(previousRow,0,panel.rows.size()-1); if(!previousDirectory.empty() && previousDirectory.parent_path()==directory) { const auto child=std::find(panel.rows.begin(),panel.rows.end(),previousDirectory); if(child!=panel.rows.end()) selected=child; } if(selected!=panel.rows.end()) { if(sameDirectory) SendMessageW(panel.list,LB_SETTOPINDEX,previousTop,0); SendMessageW(panel.list,LB_SETCURSEL,selected-panel.rows.begin(),0); } InvalidateRect(panel.list,nullptr,FALSE); if(error!=ERROR_SUCCESS && error!=ERROR_NO_MORE_FILES && error!=ERROR_FILE_NOT_FOUND) setStatus(L"Some entries could not be read."); else if(&panel==&panels[active]) showFileDetails(panel); } void open(Panel &panel,const fs::path &directory,bool offerElevation=false) { openOn(panel,directory,offerElevation,panel.peer); } void refreshChangedPanels() { for(auto &panel:panels) { const auto watch=panel.watch.handle; if(watch==INVALID_HANDLE_VALUE || WaitForSingleObject(watch,0)!=WAIT_OBJECT_0) continue; if(!FindNextChangeNotification(watch)) panel.watch.reset(); // Pass a copy because opening the folder rebuilds the panel. const auto directory=panel.directory; open(panel,directory); } } void selectDrive() { auto row=SendMessageW(picker,LB_GETCURSEL,0,0); if(!commandPicker && (row<0 || static_cast(row)==drivePaths.size())) return; const auto remote=remoteDriveRows.find(row); if(commandPicker || row(drivePaths.size()) || remote!=remoteDriveRows.end()) { hidePicker(); showMenu(false); } if(commandPicker) { if(row<0) return; auto owner=GetParent(picker); if(menuIndex==0 || menuIndex==4) { int index=menuIndex==4; if(row==2) chooseDrive(index); else setMode(index,row==1); } else if(menuIndex==2 && row==3) showFolderShortcuts(panels[active],-1); else { const int command=menuIndex==1?113+static_cast(row):110+static_cast(row); SendMessageW(owner,WM_COMMAND,MAKEWPARAM(command,BN_CLICKED),0); } return; } if(row>=0 && static_cast(row)second.path,false,remote->second.peer); } LRESULT CALLBACK pickerKeys(HWND window, UINT msg, WPARAM wp, LPARAM lp, UINT_PTR, DWORD_PTR) { if(!commandPicker && msg==WM_CHAR) { const auto key=towupper(static_cast(wp)); LRESULT row=LB_ERR; if(key>=L'A' && key<=L'Z') { const auto disk=std::find_if(drivePaths.begin(),drivePaths.end(),[key](const fs::path &path) { return path.native().front()==key; }); if(disk!=drivePaths.end()) row=disk-drivePaths.begin(); } if(row!=LB_ERR) { SendMessageW(window,LB_SETCURSEL,row,0); selectDrive(); return 0; } } if(!commandPicker && (msg==WM_MOUSEMOVE || msg==WM_MOUSELEAVE)) { int row=-1; if(msg==WM_MOUSEMOVE) { TRACKMOUSEEVENT tracking{sizeof(tracking),TME_LEAVE,window,0}; TrackMouseEvent(&tracking); const auto hit=SendMessageW(window,LB_ITEMFROMPOINT,0,lp); if(!HIWORD(hit) && LOWORD(hit)(GetParent(picker)),reinterpret_cast(&request), SMTO_ABORTIFHUNG|SMTO_ERRORONEXIT,350,&result); const auto error=GetLastError(); if(!delivered || result!=1 || !drivePeerReplyReceived) { drivePeers.clear(); if(delivered && result!=1) drivePeerLookupError=L"KSwitcher rejected the peer request"; else if(delivered) drivePeerLookupError=L"Invalid peer list from KSwitcher"; else if(error==ERROR_ACCESS_DENIED) drivePeerLookupError=L"Access denied by Windows"; else if(error==ERROR_TIMEOUT) drivePeerLookupError=L"KSwitcher did not respond in time"; else drivePeerLookupError=L"Peer request failed (Windows code "+std::to_wstring(error)+L")"; ks::log(L"LAN Files peer lookup failed: delivered="+std::to_wstring(delivered!=0)+ L", reply="+std::to_wstring(result)+L", Windows error="+std::to_wstring(error)); } } else { drivePeerLookupError=L"KSwitcher is not running"; ks::log(L"LAN Files peer lookup failed: local KSwitcher window not found"); } drivePeerService=nullptr; } void startDriveQuery(DriveQuery &query) { if(query.peer.address.empty()) return; std::packaged_task task([peer=query.peer] { FilesConnection host(&peer); auto reply=host.call({{"action","drives"}}); checkFileReply(reply); return reply.at("drives"); }); query.pending=task.get_future(); try {std::thread(std::move(task)).detach();} catch(const std::system_error &e){query.pending={};query.error=fileError(e);} } std::array driveLabels(wchar_t letter,UINT type,uint64_t total,uint64_t free) { const wchar_t *types[]{L"unknown",L"unavailable",L"removable",L"fixed",L"network",L"CD-ROM",L"RAM disk"}; return {std::wstring(1,letter)+L": "+types[type(&panel),2); const int panelWidth=panel.right-panel.left; const auto frame=dialogFrameMetrics(GetDpiForWindow(owner)); const int inset=frame.content(),vertical=dialogListInset(frame,textHeight); const int width=std::clamp(drivePickerWidth,90,std::max(90,panelWidth*4/5-2*inset)); const int height=std::min(std::max(1,(static_cast(panel.bottom-panel.top)-2*vertical)/textHeight), static_cast(SendMessageW(picker,LB_GETCOUNT,0,0)))*textHeight; const int x=panel.left+(panel.right-panel.left-width)/2; const int y=panel.top+(panel.bottom-panel.top-height)/2; constexpr UINT flags=SWP_SHOWWINDOW|SWP_NOACTIVATE|SWP_NOCOPYBITS; auto layout=BeginDeferWindowPos(2); if(layout) layout=DeferWindowPos(layout,pickerFrame,HWND_TOP, x-inset,y-vertical,width+2*inset,height+2*vertical,flags); if(layout) layout=DeferWindowPos(layout,picker,HWND_TOP,x,y,width,height,flags); if(!layout || !EndDeferWindowPos(layout)) { SetWindowPos(pickerFrame,HWND_TOP,x-inset,y-vertical,width+2*inset,height+2*vertical,flags); SetWindowPos(picker,HWND_TOP,x,y,width,height,flags); } RedrawWindow(pickerFrame,nullptr,nullptr,RDW_INVALIDATE|RDW_UPDATENOW); SendMessageW(picker,LB_SETCURSEL,selected,0); if(focus) SetFocus(picker); } void showRemoteDrives(bool focus=false,int initialRow=0) { auto selection=SendMessageW(picker,LB_GETCURSEL,0,0); std::optional previous; if(auto row=remoteDriveRows.find(selection);row!=remoteDriveRows.end()) previous=row->second; if(selection>static_cast(drivePaths.size())) selection=-1; if(focus) selection=initialRow; const auto top=SendMessageW(picker,LB_GETTOPINDEX,0,0); for(auto count=SendMessageW(picker,LB_GETCOUNT,0,0);count>static_cast(drivePaths.size());--count) SendMessageW(picker,LB_DELETESTRING,count-1,0); remoteDriveRows.clear(); const auto add=[](const std::wstring &label) { drivePickerWidth=std::max(drivePickerWidth,static_cast(label.size()+2)*cellWidth); return SendMessageW(picker,LB_ADDSTRING,0,reinterpret_cast(label.c_str())); }; add(L"KSwitcher LAN"); if(!drivePeerLookupError.empty()) add(L" "+drivePeerLookupError); else if(driveQueries.empty()) add(L" No paired computers"); for(const auto &query:driveQueries) { add(query.peer.name); if(query.pending.valid()) add(L" Loading drives..."); else if(!query.error.empty()) add(L" "+query.error); else if(query.peer.address.empty()) add(refreshDriveAddresses?L" Looking for PC...":L" Offline"); else if(query.drives.empty()) add(L" No drives"); for(const auto &entry:query.drives) { const auto root=filePath(entry.at("path")); if(root.native().size()!=3 || root.native().front()L'Z') continue; const auto labels=driveLabels(root.native().front(),entry.at("type"),entry.at("total"),entry.at("free")); const auto row=add(L" "+labels[0]+L" "+labels[1]+L" "+labels[2]); if(row<0) continue; remoteDriveRows.emplace(row,DriveChoice{query.peer,root}); if(previous && previous->peer.id==query.peer.id && previous->path==root) selection=row; } } layoutDrivePicker(static_cast(selection),focus); InvalidateRect(picker,nullptr,FALSE); if(!focus) SendMessageW(picker,LB_SETTOPINDEX,top,0); } void pollDriveQueries(HWND owner) { if(commandPicker || !IsWindowVisible(picker)) { KillTimer(owner,driveQueryTimer); driveQueries.clear(); return; } bool changed=false, pending=false; if(refreshDriveAddresses && GetTickCount64()-driveOpened>=500) { refreshDriveAddresses=false; queryDrivePeers(); for(const auto &peer:drivePeers) if(std::none_of(driveQueries.begin(),driveQueries.end(),[&](const DriveQuery &query){return query.peer.id==peer.id;})) { auto &query=driveQueries.emplace_back(); query.peer=peer; startDriveQuery(query); } for(auto &query:driveQueries) if(query.peer.address.empty()) for(const auto &peer:drivePeers) if(peer.id==query.peer.id && !peer.address.empty()) { query.peer=peer; startDriveQuery(query); break; } changed=true; } for(auto &query:driveQueries) { if(!query.pending.valid()) continue; if(query.pending.wait_for(std::chrono::seconds(0))!=std::future_status::ready) { pending=true; continue; } try {query.drives=query.pending.get();} catch(const std::system_error &e) { ks::log(L"LAN Files drive query failed: Windows error "+std::to_wstring(e.code().value())); if(e.code()==std::error_code(WSAETIMEDOUT,std::system_category())) query.error=L"Connection timed out"; else query.error=query.peer.filesBrowse?fileError(e):L"Check build / TCP 45885"; } catch(const std::exception &) { ks::log(L"LAN Files drive query failed: unexpected error"); query.error=query.peer.filesBrowse?L"Cannot reach TCP 45885":L"Check build / TCP 45885"; } changed=true; } if(changed) showRemoteDrives(); if(!pending && !refreshDriveAddresses) KillTimer(owner,driveQueryTimer); } void chooseDrive(int index,bool fromMenu) { commandPicker=false; if (!fromMenu) showMenu(false); pickerPanel=index; drivePaths.clear(); driveHover=-1; SendMessageW(picker,LB_RESETCONTENT,0,0); queryDrivePeers(); remoteDriveRows.clear(); driveQueries.clear(); for(const auto &peer:drivePeers) { auto &query=driveQueries.emplace_back(); query.peer=peer; startDriveQuery(query); } refreshDriveAddresses=true; driveOpened=GetTickCount64(); const DWORD disks=GetLogicalDrives(); int selected=0; int textWidth=0; auto dc=GetDC(picker); auto oldFont=dc ? SelectObject(dc,mono) : nullptr; std::vector> driveColumns; std::array columnWidths{}; SIZE space{cellWidth,0}; if(dc) GetTextExtentPoint32W(dc,L" ",1,&space); for(int letter=0;letter<26;++letter) { if(!(disks & (1UL<(L'A'+letter),L':',L'\\',0}; const auto type=GetDriveTypeW(drive); ULARGE_INTEGER total{},free{}; if(type==DRIVE_FIXED || type==DRIVE_RAMDISK) GetDiskFreeSpaceExW(drive,nullptr,&total,&free); driveColumns.push_back(driveLabels(drive[0],type,total.QuadPart,free.QuadPart)); for(size_t i=0;i(value.size())*cellWidth,0}; if(dc) GetTextExtentPoint32W(dc,value.c_str(),static_cast(value.size()),&extent); columnWidths[i]=std::max(columnWidths[i],extent.cx); } if(fs::path(drive)==panels[index].directory.root_path()) selected=static_cast(drivePaths.size()); drivePaths.emplace_back(drive); } LONG edge=0; for(size_t i=0;i(label.c_str())); } if(dc) { if(oldFont) SelectObject(dc,oldFont); ReleaseDC(picker,dc); } HWND owner=GetParent(picker); drivePickerWidth=textWidth; showRemoteDrives(true,selected); SetTimer(owner,driveQueryTimer,100,nullptr); if (fromMenu) showMenu(true); RedrawWindow(owner,nullptr,nullptr,RDW_INVALIDATE|RDW_ALLCHILDREN|RDW_UPDATENOW); } void openPanelFile(const Panel &panel, const fs::path &file, const wchar_t *verb, const wchar_t *failureMessage) { if(panel.peer) { setStatus(L"Copy the remote file to a local panel before opening it."); return; } std::error_code error; if (!fs::is_regular_file(file, error)) { if(!verb) setStatus(failureMessage); return; } const auto extension=file.extension().wstring(); const auto isExtension=[&](const wchar_t *expected) { return CompareStringOrdinal(extension.c_str(),-1,expected,-1,TRUE)==CSTR_EQUAL; }; if(!verb && (isExtension(L".bat") || isExtension(L".cmd"))) { const auto command=L"& "+powerShellPath(file); if(submitTerminalCommand(panel,command) && terminalRows<5) { terminalRows=5; layout(GetParent(panel.list)); } return; } if(!verb && isExtension(L".exe")) verb=L"open"; if (reinterpret_cast(ShellExecuteW(GetParent(panel.list), verb, file.c_str(), nullptr, panel.directory.c_str(), SW_SHOWNORMAL)) <= 32) setStatus(failureMessage); } void enter(Panel &panel) { const auto row=currentPanelRow(panel); if(row!=LB_ERR) { auto target = panel.rows[row]; if(panel.sizes[row]==L"Folder" || panel.sizes[row]==L"Up") open(panel,target,true); else openPanelFile(panel, target, nullptr, L"Cannot open the selected file."); } } enum class ConflictMode { ask, overwrite, skip, rename, append, newer }; struct CopyRequest { std::vector sources; fs::path destination; fs::path newName; fs::path otherDirectory; ConflictMode conflictMode{}; bool askReadOnly=true; bool move{}; const FilesPeer *sourcePeer{}, *destinationPeer{}, *otherPeer{}; }; bool directoryExists(const FilesPeer *peer,const fs::path &path) { if(!peer) { std::error_code error; return fs::is_directory(path,error); } FilesConnection host(peer); const auto info=statFile(host,path); return info && (info->attributes&FILE_ATTRIBUTE_DIRECTORY); } HBRUSH warningBrush{}; RECT previousOwnerBounds{}; #include "files_dialog_input.inl" LRESULT CALLBACK dialogButtonEnter(HWND button,UINT message,WPARAM wp,LPARAM lp,UINT_PTR,DWORD_PTR); void installDialogButtonKeys(HWND dialog) { EnumChildWindows(dialog,[](HWND control,LPARAM)->BOOL { wchar_t name[16]{}; GetClassNameW(control,name,static_cast(std::size(name))); if(lstrcmpiW(name,L"Button")==0 && (GetWindowLongPtrW(control,GWL_STYLE)&WS_TABSTOP)) SetWindowSubclass(control,dialogButtonEnter,1,0); return TRUE; },0); } bool isDialogEdit(HWND control) { wchar_t name[16]{}; GetClassNameW(control,name,static_cast(std::size(name))); return lstrcmpiW(name,L"Edit")==0; } void centerDialogEditText(HWND field) { RECT edit{}; GetClientRect(field,&edit); if(const auto dc=GetDC(field)) { const auto oldFont=SelectObject(dc,mono); TEXTMETRICW metrics{}; if(GetTextMetricsW(dc,&metrics)) edit.top=std::max(0L,(edit.bottom-metrics.tmHeight)/2); SelectObject(dc,oldFont); ReleaseDC(field,dc); } SendMessageW(field,EM_SETRECTNP,0,reinterpret_cast(&edit)); } LRESULT CALLBACK dialogEditLayout(HWND field,UINT message,WPARAM wp,LPARAM lp,UINT_PTR,DWORD_PTR) { const auto result=DefSubclassProc(field,message,wp,lp); if(message==WM_SIZE || message==WM_SETFONT) centerDialogEditText(field); return result; } void prepareDialogEdit(HWND field) { SendMessageW(field,WM_SETFONT,reinterpret_cast(mono),FALSE); const auto margin=MulDiv(2,GetDpiForWindow(field),96); SendMessageW(field,EM_SETMARGINS,EC_LEFTMARGIN|EC_RIGHTMARGIN,MAKELPARAM(margin,margin)); if(GetWindowLongPtrW(field,GWL_STYLE)&ES_MULTILINE) { SetWindowSubclass(field,dialogEditLayout,1,0); centerDialogEditText(field); } } void prepareCopyDialog(HWND dialog) { installDialogButtonKeys(dialog); EnumChildWindows(dialog,[](HWND control,LPARAM)->BOOL { if(isDialogEdit(control)) prepareDialogEdit(control); return TRUE; },0); const auto style=GetWindowLongPtrW(dialog,GWL_STYLE); SetWindowLongPtrW(dialog,GWL_STYLE,style&~(WS_CAPTION|WS_SYSMENU|WS_THICKFRAME)); const auto extended=GetWindowLongPtrW(dialog,GWL_EXSTYLE); SetWindowLongPtrW(dialog,GWL_EXSTYLE,extended&~WS_EX_DLGMODALFRAME); SetWindowPos(dialog,nullptr,0,0,0,0, SWP_NOMOVE|SWP_NOSIZE|SWP_NOZORDER|SWP_NOACTIVATE|SWP_FRAMECHANGED); } RECT dialogControlBounds(HWND dialog,int id) { RECT bounds{}; GetWindowRect(GetDlgItem(dialog,id),&bounds); MapWindowPoints(nullptr,dialog,reinterpret_cast(&bounds),2); return bounds; } void layoutDialogContent(HWND dialog,int firstControl) { const auto dpi=GetDpiForWindow(dialog); const auto frame=dialogFrameMetrics(dpi); const auto first=dialogControlBounds(dialog,firstControl); const int dx=frame.content()-first.left; const int dy=dialogListInset(frame,textHeight)+MulDiv(1,dpi,96)-first.top; for(auto control=GetWindow(dialog,GW_CHILD);control;control=GetWindow(control,GW_HWNDNEXT)) { RECT bounds{}; GetWindowRect(control,&bounds); MapWindowPoints(nullptr,dialog,reinterpret_cast(&bounds),2); SetWindowPos(control,nullptr,bounds.left+dx,bounds.top+dy,0,0, SWP_NOSIZE|SWP_NOZORDER|SWP_NOACTIVATE); } RECT outer{}; GetWindowRect(dialog,&outer); SetWindowPos(dialog,nullptr,0,0,outer.right-outer.left+2*dx,outer.bottom-outer.top+dy, SWP_NOMOVE|SWP_NOZORDER|SWP_NOACTIVATE); } int dialogFooterInset(HWND dialog) { const auto dpi=GetDpiForWindow(dialog); const auto metrics=dialogFrameMetrics(dpi); return metrics.innerLine(); } LONG dialogDividerY(HWND dialog,int id) { const auto bounds=dialogControlBounds(dialog,id); if(id==214) { const auto combo=dialogControlBounds(dialog,213); const auto validation=dialogControlBounds(dialog,328); return bounds.bottom+std::max(0L,(combo.top-validation.bottom-1)/2); } if(id==213) return (dialogControlBounds(dialog,328).bottom+bounds.top-1)/2; return bounds.top-4; } void centerDialogRow(HWND dialog,int upperDivider,int lowerDivider,std::initializer_list controls) { RECT client{}; GetClientRect(dialog,&client); const auto top=dialogDividerY(dialog,upperDivider)+1; const auto bottom=lowerDivider?dialogDividerY(dialog,lowerDivider):client.bottom-dialogFooterInset(dialog); for(const auto id:controls) { const auto bounds=dialogControlBounds(dialog,id); SetWindowPos(GetDlgItem(dialog,id),nullptr,bounds.left,(top+bottom-(bounds.bottom-bounds.top))/2, 0,0,SWP_NOSIZE|SWP_NOZORDER|SWP_NOACTIVATE); } } void layoutDialogButtons(HWND dialog,int divider,std::initializer_list controls) { RECT client{},window{}; GetClientRect(dialog,&client); GetWindowRect(dialog,&window); const auto dpi=GetDpiForWindow(dialog); const auto frame=dialogFrameMetrics(dpi); const auto extra=frame.inner-dialogFrameMetrics(dpi,defaultDialogFramePadding).inner; LONG tallest{}; for(const auto id:controls) { const auto bounds=dialogControlBounds(dialog,id); tallest=std::max(tallest,bounds.bottom-bounds.top); } const auto footerHeight=std::max(tallest+2,2*static_cast(textHeight)+2*extra); const auto height=dialogDividerY(dialog,divider)+1+footerHeight+dialogFooterInset(dialog); const auto width=window.right-window.left; const auto outerHeight=window.bottom-window.top+height-client.bottom; RECT owner{}; const bool centered=GetWindowRect(GetWindow(dialog,GW_OWNER),&owner)!=FALSE; SetWindowPos(dialog,nullptr,(owner.left+owner.right-width)/2, (owner.top+owner.bottom-outerHeight)/2,width,outerHeight, (centered?0:SWP_NOMOVE)|SWP_NOZORDER|SWP_NOACTIVATE); centerDialogRow(dialog,divider,0,controls); } void paintDialogButton(const DRAWITEMSTRUCT &item,COLORREF surface,COLORREF foreground) { wchar_t label[32]{}; GetWindowTextW(item.hwndItem,label,static_cast(std::size(label))); const bool focused=(item.itemState&(ODS_FOCUS|ODS_SELECTED))!=0; SetDCBrushColor(item.hDC,focused?colors[Dialog]:surface); FillRect(item.hDC,&item.rcItem,static_cast(GetStockObject(DC_BRUSH))); SelectObject(item.hDC,mono); SetBkMode(item.hDC,TRANSPARENT); SetTextColor(item.hDC,focused?colors[Text]:foreground); auto rect=item.rcItem; const auto defaultButton=SendMessageW(GetParent(item.hwndItem),DM_GETDEFID,0,0); const bool isDefault=HIWORD(defaultButton)==DC_HASDEFID && LOWORD(defaultButton)==item.CtlID; const auto caption=(isDefault?L"{ ":L"[ ")+std::wstring(label)+(isDefault?L" }":L" ]"); DrawTextW(item.hDC,caption.c_str(),-1,&rect,DT_CENTER|DT_VCENTER|DT_SINGLELINE); } INT_PTR paintCopyDialog(HWND dialog, UINT message, WPARAM wp, LPARAM lp, const wchar_t *caption, bool warning, std::initializer_list dividers, bool useDialogPalette=false, HWND scrollListControl=nullptr) { const auto foreground=useDialogPalette?colors[Text]:warning?RGB(255,255,255):RGB(0,0,0); const auto surface=useDialogPalette?colors[Dialog]:warning?RGB(128,0,0):RGB(192,192,192); const auto brush=useDialogPalette?teal:warning?warningBrush:static_cast(GetStockObject(LTGRAY_BRUSH)); if(message==WM_CTLCOLORDLG) return reinterpret_cast(brush); if(message==WM_CTLCOLORSTATIC || message==WM_CTLCOLOREDIT || message==WM_CTLCOLORBTN) { const auto dc=reinterpret_cast(wp); const bool edit=message==WM_CTLCOLOREDIT || (message==WM_CTLCOLORSTATIC && isDialogEdit(reinterpret_cast(lp))); SetTextColor(dc,edit?colors[Text]:foreground); SetBkColor(dc,edit?colors[Dialog]:surface); return reinterpret_cast(edit?teal:brush); } if(message==WM_PAINT) { PAINTSTRUCT paint{}; const auto dc=BeginPaint(dialog,&paint); RECT bounds{}; GetClientRect(dialog,&bounds); const auto client=bounds; const auto black=static_cast(GetStockObject(useDialogPalette?DC_BRUSH:warning?WHITE_BRUSH:BLACK_BRUSH)); if(useDialogPalette) SetDCBrushColor(dc,foreground); bounds=paintDialogFrame(dc,client,dialogFrameMetrics(GetDpiForWindow(dialog)),brush,black); for(const auto control:dividers) { const auto y=dialogDividerY(dialog,control); RECT divider{bounds.left+1,y,bounds.right-1,y+1}; FillRect(dc,÷r,black); } paintDialogFrameCaptions(dc,client,bounds,caption,brush,foreground,mono); if(scrollListControl) paintScrollPosition(dialog,dc,scrollListControl,surface); EndPaint(dialog,&paint); return TRUE; } if(message==WM_DRAWITEM) { const auto &item=*reinterpret_cast(lp); paintDialogButton(item,surface,foreground); return TRUE; } return FALSE; } LRESULT CALLBACK paintConflictCombo(HWND control,UINT message,WPARAM wp,LPARAM lp,UINT_PTR,DWORD_PTR) { if(message!=WM_PAINT) return DefSubclassProc(control,message,wp,lp); PAINTSTRUCT paint{}; const auto dc=BeginPaint(control,&paint); RECT bounds{}; GetClientRect(control,&bounds); wchar_t label[64]{}; const auto selected=SendMessageW(control,CB_GETCURSEL,0,0); if(selected!=CB_ERR) SendMessageW(control,CB_GETLBTEXT,selected,reinterpret_cast(label)); auto arrow=bounds; arrow.left=std::max(bounds.left,bounds.right-2*static_cast(cellWidth)); bounds.right=arrow.left; SetDCBrushColor(dc,colors[Dialog]); FillRect(dc,&bounds,static_cast(GetStockObject(DC_BRUSH))); FillRect(dc,&arrow,static_cast(GetStockObject(DC_BRUSH))); SelectObject(dc,mono); SetBkMode(dc,TRANSPARENT); SetTextColor(dc,colors[Text]); bounds.left+=5; DrawTextW(dc,label,-1,&bounds,DT_SINGLELINE|DT_VCENTER|DT_END_ELLIPSIS|DT_NOPREFIX); DrawTextW(dc,L"↓",-1,&arrow,DT_SINGLELINE|DT_VCENTER|DT_CENTER); if(GetFocus()==control) DrawFocusRect(dc,&bounds); EndPaint(control,&paint); return 0; } INT_PTR CALLBACK copyDialog(HWND dialog, UINT message, WPARAM wp, LPARAM lp) { constexpr int summary=210, pathField=211, conflicts=212; if(message==WM_DRAWITEM && wp==214) { const auto &item=*reinterpret_cast(lp); const auto request=reinterpret_cast(GetWindowLongPtrW(dialog,DWLP_USER)); const bool checked=request && request->askReadOnly; paintText(item.hDC,item.rcItem,checked?L"[x] Also ask on read-only files": L"[ ] Also ask on read-only files",RGB(0,0,0),RGB(192,192,192)); if(item.itemState&ODS_FOCUS) DrawFocusRect(item.hDC,&item.rcItem); return TRUE; } if(message==WM_DRAWITEM && wp==213) { const auto &item=*reinterpret_cast(lp); wchar_t label[64]{}; if(item.itemID!=UINT(-1)) SendMessageW(item.hwndItem,CB_GETLBTEXT,item.itemID,reinterpret_cast(label)); const bool selected=(item.itemState&ODS_SELECTED)!=0 && (item.itemState&ODS_COMBOBOXEDIT)==0; paintText(item.hDC,item.rcItem,label,colors[Text],colors[selected?Selection:Dialog]); return TRUE; } if(message==WM_CTLCOLORLISTBOX) { const auto dc=reinterpret_cast(wp); SetTextColor(dc,colors[Text]); SetBkColor(dc,colors[Dialog]); return reinterpret_cast(teal); } const auto prompt=reinterpret_cast(GetWindowLongPtrW(dialog,DWLP_USER)); if(const auto painted=paintCopyDialog(dialog,message,wp,lp, prompt && prompt->move?L" Rename/Move ":L" Copy ",false,{213,214})) return painted; if(message==WM_INITDIALOG) { auto &request=*reinterpret_cast(lp); prepareCopyDialog(dialog); layoutDialogContent(dialog,summary); SetWindowLongPtrW(dialog,DWLP_USER,lp); for(const auto label:{L"Ask",L"Overwrite",L"Skip",L"Rename",L"Append",L"Only newer file(s)"}) if(!request.move || std::wstring_view(label)!=L"Append") SendDlgItemMessageW(dialog,213,CB_ADDSTRING,0,reinterpret_cast(label)); SendDlgItemMessageW(dialog,213,CB_SETCURSEL,0,0); const std::wstring action=request.move?L"Rename or move ":L"Copy "; const auto label=action+(request.sources.size()==1?request.sources.front().filename().wstring(): std::to_wstring(request.sources.size())+L" items")+L" to:"; SetDlgItemTextW(dialog,summary,label.c_str()); const bool single=request.sources.size()==1; auto fieldText=single && !request.move?request.sources.front().filename():request.destination; if(request.move && fieldText.has_filename()) fieldText/=L""; SetDlgItemTextW(dialog,pathField,fieldText.c_str()); SendDlgItemMessageW(dialog,pathField,EM_SETREADONLY,!single && !request.move,0); if(request.move) SetDlgItemTextW(dialog,IDOK,L"&Rename"); layoutDialogButtons(dialog,214,{IDOK,IDCANCEL}); SendDlgItemMessageW(dialog,328,WM_SETFONT,reinterpret_cast(mono),FALSE); SendDlgItemMessageW(dialog,summary,WM_SETFONT,reinterpret_cast(mono),FALSE); SendDlgItemMessageW(dialog,conflicts,WM_SETFONT,reinterpret_cast(mono),FALSE); for(const auto control:{213,214}) SendDlgItemMessageW(dialog,control,WM_SETFONT,reinterpret_cast(mono),FALSE); SendDlgItemMessageW(dialog,213,CB_SETITEMHEIGHT,0,textHeight); SendDlgItemMessageW(dialog,213,CB_SETITEMHEIGHT,static_cast(-1),textHeight+2); RECT combo{}, caption{}; const auto captionControl=GetDlgItem(dialog,conflicts); GetWindowRect(GetDlgItem(dialog,213),&combo); GetWindowRect(captionControl,&caption); MapWindowPoints(nullptr,dialog,reinterpret_cast(&combo),2); MapWindowPoints(nullptr,dialog,reinterpret_cast(&caption),2); SetWindowPos(captionControl,nullptr,caption.left,combo.top, caption.right-caption.left,combo.bottom-combo.top,SWP_NOZORDER|SWP_NOACTIVATE); SetWindowSubclass(GetDlgItem(dialog,213),paintConflictCombo,1,0); SendMessageW(dialog,DM_SETDEFID,IDOK,0); SendDlgItemMessageW(dialog,pathField,EM_SETSEL,0,-1); return TRUE; } if(message==WM_CLOSE || (message==WM_COMMAND && LOWORD(wp)==IDCANCEL)) { finishFilesDialog(dialog,IDCANCEL); return TRUE; } if(message!=WM_COMMAND) return FALSE; auto &request=*reinterpret_cast(GetWindowLongPtrW(dialog,DWLP_USER)); if(LOWORD(wp)==214 && HIWORD(wp)==BN_CLICKED) { request.askReadOnly=!request.askReadOnly; InvalidateRect(GetDlgItem(dialog,214),nullptr,FALSE); return TRUE; } if(LOWORD(wp)==213) { InvalidateRect(GetDlgItem(dialog,213),nullptr,FALSE); return FALSE; } if(LOWORD(wp)==pathField && HIWORD(wp)==EN_CHANGE) { SetDlgItemTextW(dialog,328,L""); return TRUE; } if(LOWORD(wp)!=IDOK) return FALSE; const auto showError=[&](const wchar_t *message) { SetDlgItemTextW(dialog,328,message); SetFocus(GetDlgItem(dialog,pathField)); }; try { if(request.move) { const auto field=GetDlgItem(dialog,pathField); std::wstring value(GetWindowTextLengthW(field)+1,L'\0'); value.resize(GetWindowTextW(field,value.data(),static_cast(value.size()))); const fs::path entered(value); const bool bareName=request.sources.size()==1 && entered==entered.filename() && entered!=L"." && entered!=L".."; const auto initial=request.otherDirectory; const bool usesOtherPanel=entered==initial || entered==initial/L"" || (request.otherPeer && entered.is_absolute()); const auto target=usesOtherPanel?entered:request.sources.front().parent_path()/entered; const auto peer=usesOtherPanel?request.otherPeer:request.sourcePeer; if(value.empty() || value.find_first_of(L"<>|?*")!=value.npos || (!directoryExists(peer,target) && (request.sources.size()!=1 || !target.has_filename() || !directoryExists(peer,target.parent_path())))) { showError(L"Enter an existing destination folder, or a new name for one item."); return TRUE; } request.destinationPeer=peer; request.newName.clear(); if(!bareName && directoryExists(peer,target)) { request.destination=target.lexically_normal(); if(!request.destination.has_filename() && request.destination.has_relative_path()) request.destination=request.destination.parent_path(); } else { request.destination=target.parent_path(); request.newName=target.filename(); } } else if(request.sources.size()==1) { const auto field=GetDlgItem(dialog,pathField); std::wstring name(GetWindowTextLengthW(field)+1,L'\0'); name.resize(GetWindowTextW(field,name.data(),static_cast(name.size()))); if(name.empty() || name.back()==L'.' || name.back()==L' ' || name.find_first_of(L"<>:\"/\\|?*")!=name.npos || std::any_of(name.begin(),name.end(),[](wchar_t ch){return ch<32;})) { showError(L"Enter a valid name for the copy."); return TRUE; } if(CompareStringOrdinal(name.c_str(),-1,request.sources.front().filename().c_str(),-1,TRUE)!=CSTR_EQUAL) request.newName=name; else request.newName.clear(); } const auto sourceDirectory=request.sources.front().parent_path(); const auto destination=request.move || request.newName.empty()?request.destination:sourceDirectory; std::error_code error; const auto peer=request.move || request.newName.empty()?request.destinationPeer:request.sourcePeer; if(!directoryExists(peer,destination)) { showError(L"The destination folder is unavailable."); return TRUE; } const bool sameHost=(!peer && !request.sourcePeer) || (peer && request.sourcePeer && peer->id==request.sourcePeer->id); const bool sameFolder=sameHost && (peer? CompareStringOrdinal(destination.c_str(),-1,sourceDirectory.c_str(),-1,TRUE)==CSTR_EQUAL: fs::equivalent(destination,sourceDirectory,error)); if(request.newName.empty() && sameFolder) { showError(L"Open a different folder in the other panel, or enter a new name for a single item."); return TRUE; } const auto mode=SendDlgItemMessageW(dialog,213,CB_GETCURSEL,0,0); if(mode!=CB_ERR) request.conflictMode=static_cast( request.move && mode==static_cast(ConflictMode::append)?static_cast(ConflictMode::newer):mode); finishFilesDialog(dialog,IDOK); } catch(const std::system_error &e) { showError(fileError(e).c_str()); } catch(const std::exception &) { showError(L"The remote destination is unavailable."); } return TRUE; } struct CopyProgress { HWND window{}; bool canceled{}; bool closeOwner{}; bool move{}; int conflictChoice{}; ConflictMode conflictMode{}; bool askReadOnly=true; uint64_t files{}, totalFiles{}, bytes{}, totalBytes{}; uint64_t skippedFiles{}, skippedBytes{}; const wchar_t *lastSkipReason=L""; ULONGLONG lastPaint{}; fs::path from,to,renamedTarget; }; bool copyInProgress{}; CopyProgress *activeCopy{}; struct OwnedWindowMove { HWND owner; LONG dx,dy; }; BOOL CALLBACK moveOwnedDialog(HWND window,LPARAM parameter) { const auto &move=*reinterpret_cast(parameter); if(window==move.owner || !IsWindowVisible(window) || GetAncestor(window,GA_ROOTOWNER)!=move.owner) return TRUE; RECT bounds{}; if(GetWindowRect(window,&bounds)) SetWindowPos(window,nullptr,bounds.left+move.dx,bounds.top+move.dy,0,0, SWP_NOSIZE|SWP_NOZORDER|SWP_NOACTIVATE); return TRUE; } LRESULT CALLBACK paintCopyProgressBar(HWND bar,UINT message,WPARAM wp,LPARAM lp,UINT_PTR,DWORD_PTR) { if(message!=WM_PAINT) return DefSubclassProc(bar,message,wp,lp); PAINTSTRUCT paint{}; const auto dc=BeginPaint(bar,&paint); RECT bounds{}; GetClientRect(bar,&bounds); const auto percent=std::clamp(SendMessageW(bar,PBM_GETPOS,0,0),LRESULT{0},LRESULT{100}); auto filled=bounds; filled.right=static_cast(bounds.right*percent/100); FillRect(dc,&bounds,static_cast(GetStockObject(WHITE_BRUSH))); FillRect(dc,&filled,static_cast(GetStockObject(BLACK_BRUSH))); const auto label=std::to_wstring(percent)+L"%"; SelectObject(dc,mono); SetBkMode(dc,TRANSPARENT); constexpr auto flags=DT_CENTER|DT_VCENTER|DT_SINGLELINE|DT_NOPREFIX; SetTextColor(dc,RGB(0,0,0)); auto labelBounds=bounds; DrawTextW(dc,label.c_str(),-1,&labelBounds,flags); const auto saved=SaveDC(dc); if(saved) { IntersectClipRect(dc,filled.left,filled.top,filled.right,filled.bottom); SetTextColor(dc,RGB(255,255,255)); labelBounds=bounds; DrawTextW(dc,label.c_str(),-1,&labelBounds,flags); RestoreDC(dc,saved); } EndPaint(bar,&paint); return 0; } void updateCopyProgress(CopyProgress &progress,uint64_t current,uint64_t fileSize,bool force=false) { constexpr ULONGLONG refreshIntervalMs=200; const auto now=GetTickCount64(); if(!force && now-progress.lastPaint(100.0*std::min(done,total)/total):0; }; const auto updateBar=[&](int bar,int value) { const auto control=GetDlgItem(progress.window,bar); SendMessageW(control,PBM_SETPOS,value,0); InvalidateRect(control,nullptr,FALSE); }; const auto bytes=progress.bytes+current; updateBar(222,fileSize?percent(current,fileSize):0); updateBar(226,progress.totalBytes? percent(bytes+progress.skippedBytes,progress.totalBytes): percent(progress.files+progress.skippedFiles,progress.totalFiles)); SetDlgItemTextW(progress.window,224,(L"Files: "+std::to_wstring(progress.files)+L" / "+ std::to_wstring(progress.totalFiles)+(progress.skippedFiles? L" Skipped: "+std::to_wstring(progress.skippedFiles)+L" ("+progress.lastSkipReason+L")":L"")).c_str()); SetDlgItemTextW(progress.window,225,(L"Size: "+formatSize(bytes)+L" / "+ formatSize(progress.totalBytes)).c_str()); } LRESULT CALLBACK dialogButtonEnter(HWND button,UINT message,WPARAM wp,LPARAM lp,UINT_PTR,DWORD_PTR) { if(message==WM_GETDLGCODE) { const auto key=reinterpret_cast(lp); if(key && key->message==WM_KEYDOWN && key->wParam==VK_RETURN) return DefSubclassProc(button,message,wp,lp)|DLGC_WANTMESSAGE; } if(message==WM_KEYDOWN && wp==VK_RETURN) { SendMessageW(button,BM_CLICK,0,0); return 0; } return DefSubclassProc(button,message,wp,lp); } INT_PTR CALLBACK cancelOperationDialog(HWND window,UINT message,WPARAM wp,LPARAM lp) { if(const auto painted=paintCopyDialog(window,message,wp,lp,L" Operation interrupted ",true,{236})) return painted; if(message==WM_INITDIALOG) { prepareCopyDialog(window); layoutDialogContent(window,235); SetDlgItemTextW(window,235,reinterpret_cast(lp)); SendDlgItemMessageW(window,235,WM_SETFONT,reinterpret_cast(mono),FALSE); layoutDialogButtons(window,236,{IDYES,IDNO}); SendMessageW(window,DM_SETDEFID,IDNO,0); SetFocus(GetDlgItem(window,IDNO)); return FALSE; } if(message==WM_CLOSE || (message==WM_COMMAND && (LOWORD(wp)==IDYES || LOWORD(wp)==IDNO || LOWORD(wp)==IDCANCEL))) { finishFilesDialog(window,message==WM_COMMAND && LOWORD(wp)==IDYES?IDYES:IDNO); return TRUE; } return FALSE; } bool confirmOperationCancellation(HWND owner,const wchar_t *question) { const auto previous=warningBrush; if(!previous) warningBrush=CreateSolidBrush(RGB(128,0,0)); const auto answer=runFilesDialog(GetModuleHandleW(nullptr),MAKEINTRESOURCEW(105),owner, cancelOperationDialog,reinterpret_cast(question)); if(!previous) { DeleteObject(warningBrush); warningBrush=nullptr; } return answer==IDYES; } INT_PTR CALLBACK copyProgressDialog(HWND window,UINT message,WPARAM wp,LPARAM lp) { if(message==WM_DRAWITEM && (wp==220 || wp==221)) { const auto &item=*reinterpret_cast(lp); std::wstring path(GetWindowTextLengthW(item.hwndItem)+1,L'\0'); GetWindowTextW(item.hwndItem,path.data(),static_cast(path.size())); auto bounds=item.rcItem; FillRect(item.hDC,&bounds,static_cast(GetStockObject(LTGRAY_BRUSH))); SelectObject(item.hDC,mono); SetBkMode(item.hDC,TRANSPARENT); SetTextColor(item.hDC,RGB(0,0,0)); constexpr auto flags=DT_SINGLELINE|DT_VCENTER|DT_NOPREFIX; const int prefix=wp==220?6:4; if(GetWindowTextLengthW(item.hwndItem)>prefix && path[prefix-2]==L':') { SIZE width{}; GetTextExtentPoint32W(item.hDC,path.data(),prefix,&width); DrawTextW(item.hDC,path.data(),prefix,&bounds,flags); bounds.left+=width.cx; DrawTextW(item.hDC,path.data()+prefix,-1,&bounds,flags|DT_PATH_ELLIPSIS|DT_END_ELLIPSIS); } else DrawTextW(item.hDC,path.data(),-1,&bounds,flags|DT_END_ELLIPSIS); return TRUE; } const auto operation=reinterpret_cast(GetWindowLongPtrW(window,DWLP_USER)); if(const auto painted=paintCopyDialog(window,message,wp,lp, operation && operation->move?L" Move ":L" Copy ",false,{228,229})) return painted; if(message==WM_DPICHANGED) { PostMessageW(window,refreshProgressLayout,0,0); return FALSE; } if(message==WM_INITDIALOG || message==refreshProgressLayout) { if(message==WM_INITDIALOG) { prepareCopyDialog(window); layoutDialogContent(window,220); SetWindowLongPtrW(window,DWLP_USER,lp); } for(const auto control:{220,221,224,225}) { SendDlgItemMessageW(window,control,WM_SETFONT,reinterpret_cast(mono),FALSE); const auto bounds=dialogControlBounds(window,control); SetWindowPos(GetDlgItem(window,control),nullptr,0,0,bounds.right-bounds.left,textHeight, SWP_NOMOVE|SWP_NOZORDER|SWP_NOACTIVATE); } for(const auto id:{222,226}) { const auto bar=GetDlgItem(window,id); const auto bounds=dialogControlBounds(window,id); SetWindowPos(bar,nullptr,0,0,bounds.right-bounds.left,textHeight+8, SWP_NOMOVE|SWP_NOZORDER|SWP_NOACTIVATE); SetWindowSubclass(bar,paintCopyProgressBar,1,0); } const auto moveRow=[&](int id,LONG top) { const auto bounds=dialogControlBounds(window,id); SetWindowPos(GetDlgItem(window,id),nullptr,bounds.left,top,0,0, SWP_NOSIZE|SWP_NOZORDER|SWP_NOACTIVATE); }; const auto frame=dialogFrameMetrics(GetDpiForWindow(window)); const auto padding=std::max(0,frame.inner); moveRow(221,std::max(dialogControlBounds(window,221).top,dialogControlBounds(window,220).bottom)); moveRow(222,std::max(dialogControlBounds(window,222).top,dialogControlBounds(window,221).bottom+4)); const auto firstBottom=dialogControlBounds(window,222).bottom; moveRow(228,firstBottom+padding+4); const auto offset=dialogDividerY(window,228)+1+padding-dialogControlBounds(window,224).top; for(const auto id:{224,225,226}) moveRow(id,dialogControlBounds(window,id).top+offset); moveRow(225,std::max(dialogControlBounds(window,225).top,dialogControlBounds(window,224).bottom)); moveRow(226,std::max(dialogControlBounds(window,226).top,dialogControlBounds(window,225).bottom+4)); const auto secondBottom=dialogControlBounds(window,226).bottom; moveRow(229,secondBottom+padding+4); layoutDialogButtons(window,229,{IDCANCEL}); RedrawWindow(window,nullptr,nullptr,RDW_INVALIDATE|RDW_ALLCHILDREN); return TRUE; } if(message==WM_NCHITTEST) { RECT bounds{}; GetWindowRect(window,&bounds); if(GET_Y_LPARAM(lp)(GetWindowLongPtrW(window,DWLP_USER)); if(!progress.canceled) progress.canceled=confirmOperationCancellation(window,progress.move? L"Do you really want to cancel moving?":L"Do you really want to cancel copying?"); return TRUE; } struct WarningPrompt { const wchar_t *message; const wchar_t *details; const wchar_t *accept; const wchar_t *decline; FileInfo incoming,existing; bool remember{}; }; std::wstring conflictFileDetails(const FileInfo &file) { const FILETIME stamp{static_cast(file.modified),static_cast(file.modified>>32)}; SYSTEMTIME utc{},local{}; wchar_t date[32]=L"—"; if(file.modified && FileTimeToSystemTime(&stamp,&utc) && SystemTimeToTzSpecificLocalTime(nullptr,&utc,&local)) swprintf_s(date,L"%04u-%02u-%02u %02u:%02u:%02u",unsigned(local.wYear), unsigned(local.wMonth),unsigned(local.wDay),unsigned(local.wHour), unsigned(local.wMinute),unsigned(local.wSecond)); auto bytes=std::to_wstring(file.size); for(auto position=bytes.size();position>3;) { position-=3; bytes.insert(position,1,L'\u202F'); } return bytes+L" B "+date; } LRESULT CALLBACK warningButtonKeys(HWND button,UINT message,WPARAM wp,LPARAM lp, UINT_PTR,DWORD_PTR index) { if(message==WM_GETDLGCODE) return DefSubclassProc(button,message,wp,lp)|DLGC_WANTARROWS; if(message==WM_KEYDOWN && (wp==VK_UP || wp==VK_DOWN)) { if(index<3 && wp==VK_UP) SetFocus(GetDlgItem(GetParent(button),232)); if(index==3 && wp==VK_DOWN) SetFocus(GetDlgItem(GetParent(button),IDYES)); return 0; } if(message==WM_KEYDOWN && index<3 && (wp==VK_LEFT || wp==VK_RIGHT)) { constexpr int buttons[]{IDYES,IDNO,IDCANCEL}; SetFocus(GetDlgItem(GetParent(button),buttons[(index+(wp==VK_LEFT?2:1))%3])); return 0; } return DefSubclassProc(button,message,wp,lp); } INT_PTR CALLBACK warningDialog(HWND window,UINT message,WPARAM wp,LPARAM lp) { if(message==WM_DRAWITEM && wp==232) { const auto &item=*reinterpret_cast(lp); const auto warning=reinterpret_cast(GetWindowLongPtrW(window,DWLP_USER)); auto bounds=item.rcItem; FillRect(item.hDC,&bounds,warningBrush); SelectObject(item.hDC,mono); SetBkMode(item.hDC,TRANSPARENT); SetTextColor(item.hDC,RGB(255,255,255)); bounds.left+=5; DrawTextW(item.hDC,warning && warning->remember?L"[x] Remember choice":L"[ ] Remember choice", -1,&bounds,DT_SINGLELINE|DT_VCENTER|DT_NOPREFIX); if(item.itemState&ODS_FOCUS) DrawFocusRect(item.hDC,&item.rcItem); return TRUE; } if(const auto painted=paintCopyDialog(window,message,wp,lp,L" Warning ",true,{233,333,234})) return painted; if(message==WM_INITDIALOG) { prepareCopyDialog(window); layoutDialogContent(window,230); SetWindowLongPtrW(window,DWLP_USER,lp); const auto &warning=*reinterpret_cast(lp); SetDlgItemTextW(window,230,warning.message); SetDlgItemTextW(window,231,warning.details); SetDlgItemTextW(window,330,conflictFileDetails(warning.incoming).c_str()); SetDlgItemTextW(window,332,conflictFileDetails(warning.existing).c_str()); SetDlgItemTextW(window,IDYES,warning.accept); SetDlgItemTextW(window,IDNO,warning.decline); for(const auto control:{230,231,232,329,330,331,332}) SendDlgItemMessageW(window,control,WM_SETFONT,reinterpret_cast(mono),FALSE); centerDialogRow(window,333,234,{232}); layoutDialogButtons(window,234,{IDYES,IDNO,IDCANCEL}); SendMessageW(window,DM_SETDEFID,IDNO,0); constexpr int buttons[]{IDYES,IDNO,IDCANCEL}; for(size_t i=0;i(GetWindowLongPtrW(window,DWLP_USER)); warning.remember=!warning.remember; InvalidateRect(GetDlgItem(window,232),nullptr,FALSE); return TRUE; } if(message==WM_COMMAND && (LOWORD(wp)==IDYES || LOWORD(wp)==IDNO || LOWORD(wp)==IDCANCEL)) { const auto choice=LOWORD(wp); finishFilesDialog(window,choice); return TRUE; } return FALSE; } bool pumpCopy(CopyProgress &progress) { MSG message{}; while(!progress.canceled && PeekMessageW(&message,nullptr,0,0,PM_REMOVE)) { if(message.message==WM_QUIT) { PostQuitMessage(static_cast(message.wParam)); progress.canceled=true; break; } const auto owner=GetWindow(progress.window,GW_OWNER); const bool inMainWindow=message.hwnd==owner || IsChild(owner,message.hwnd); if(inMainWindow && ((message.message>=WM_KEYFIRST && message.message<=WM_KEYLAST) || (message.message>=WM_MOUSEFIRST && message.message<=WM_MOUSELAST))) { if(filesMouseDown(message.message) || (message.message==WM_KEYDOWN && message.wParam==VK_ESCAPE)) SendMessageW(progress.window,WM_COMMAND,IDCANCEL,0); else if(message.message==WM_SYSKEYDOWN && message.wParam==VK_F4) SendMessageW(owner,WM_CLOSE,0,0); continue; } const bool inProgressWindow=message.hwnd==progress.window || IsChild(progress.window,message.hwnd); if(!inProgressWindow || !IsDialogMessageW(progress.window,&message)) { TranslateMessage(&message); DispatchMessageW(&message); } } return !progress.canceled; } using FileEntries=std::vector>; FileEntries childrenOf(FilesConnection &host,const fs::path &path) { FileEntries children; listFiles(host,path,[&](const fs::path &child,const FileInfo &info) { children.emplace_back(child,info); return true; }); return children; } bool countCopy(FilesConnection &host,const fs::path &path,CopyProgress &progress) { if(!pumpCopy(progress)) return false; const auto info=statFile(host,path); if(!info || (info->attributes&FILE_ATTRIBUTE_REPARSE_POINT)) throw std::system_error(info?ERROR_NOT_SUPPORTED:ERROR_FILE_NOT_FOUND,std::system_category()); if(!(info->attributes&FILE_ATTRIBUTE_DIRECTORY)) { ++progress.totalFiles; progress.totalBytes+=info->size; return true; } for(const auto &[child,entry]:childrenOf(host,path)) if(!countCopy(host,child,progress)) return false; return true; } enum class CopyResult { complete, skipped, failed }; struct CopyEndpoints { FilesConnection &source, ⌖ bool sameHost; std::string sourceId; }; void transferFile(const CopyEndpoints &hosts,const fs::path &from,const fs::path &to, const FileInfo &info,bool replace,bool append,CopyProgress &progress) { const auto update=[&](uint64_t done,uint64_t total) { updateCopyProgress(progress,done,total); return pumpCopy(progress); }; if(hosts.sameHost) { auto request=fileRequest("copy",from); request["target"]=ks::utf8(to.native()); request["replace"]=replace; request["append"]=append; request["size"]=info.size; request["modified"]=info.modified; checkFileReply(hosts.source.call(request,update)); return; } ks::lan::streamFile(hosts.source,hosts.target,hosts.sourceId,from,to, info.size,info.modified,replace,append,update); } CopyResult copyItem(const CopyEndpoints &hosts,const fs::path &from,const fs::path &to,CopyProgress &progress, bool topLevel=false,bool singleItem=false) { if(!pumpCopy(progress)) return CopyResult::failed; const auto incoming=statFile(hosts.source,from); if(!incoming || (incoming->attributes&FILE_ATTRIBUTE_REPARSE_POINT)) throw std::system_error(incoming?ERROR_NOT_SUPPORTED:ERROR_FILE_NOT_FOUND,std::system_category()); const auto moveOnHost=[&](const fs::path &target,bool replace) { if(!progress.move || !hosts.sameHost) return false; auto request=fileRequest("move",from); request["target"]=ks::utf8(target.native()); request["replace"]=replace; const auto reply=hosts.source.call(request); if(reply.at("error")==ERROR_NOT_SAME_DEVICE) return false; checkFileReply(reply); return true; }; const auto existing=statFile(hosts.target,to); if(incoming->attributes&FILE_ATTRIBUTE_DIRECTORY) { if(existing && (!(existing->attributes&FILE_ATTRIBUTE_DIRECTORY) || (existing->attributes&FILE_ATTRIBUTE_REPARSE_POINT))) throw std::system_error(ERROR_ALREADY_EXISTS,std::system_category()); if(!existing && singleItem && moveOnHost(to,false)) { progress.files=progress.totalFiles; progress.bytes=progress.totalBytes; return CopyResult::complete; } if(!existing) checkFileReply(hosts.target.call(fileRequest("mkdir",to))); auto result=CopyResult::complete; for(const auto &[child,entry]:childrenOf(hosts.source,from)) { const auto outcome=copyItem(hosts,child,to/child.filename(),progress); if(outcome==CopyResult::failed) return outcome; if(outcome==CopyResult::skipped) result=outcome; } if(progress.move && result==CopyResult::complete) checkFileReply(hosts.source.call(fileRequest("rmdir",from))); return result; } progress.from=from; progress.to=to; const auto fileSize=incoming->size; const auto transfer=[&](const fs::path &target,bool replace=false,bool append=false) { if(!moveOnHost(target,replace)) { transferFile(hosts,from,target,*incoming,replace,append,progress); if(progress.move) { auto remove=fileRequest("remove",from); remove["size"]=incoming->size; remove["modified"]=incoming->modified; checkFileReply(hosts.source.call(remove)); } } progress.bytes+=fileSize; ++progress.files; updateCopyProgress(progress,0,0); return CopyResult::complete; }; const auto skip=[&](const wchar_t *reason) { progress.skippedBytes+=fileSize; ++progress.skippedFiles; progress.lastSkipReason=reason; updateCopyProgress(progress,0,0); return CopyResult::skipped; }; updateCopyProgress(progress,0,fileSize); if(!existing) return transfer(to); if(existing->attributes&(FILE_ATTRIBUTE_DIRECTORY|FILE_ATTRIBUTE_REPARSE_POINT)) throw std::system_error(ERROR_ALREADY_EXISTS,std::system_category()); const bool readOnly=(existing->attributes&FILE_ATTRIBUTE_READONLY)!=0; auto mode=progress.conflictMode; if(readOnly && progress.askReadOnly) mode=ConflictMode::ask; if(mode==ConflictMode::skip) return skip(L"existing file"); if(mode==ConflictMode::newer && incoming->modified<=existing->modified) return skip(L"source is not newer"); if(mode==ConflictMode::rename) { for(unsigned index=2;index<=10000;++index) { if(!pumpCopy(progress)) return CopyResult::failed; const auto name=to.stem().wstring()+L" ("+std::to_wstring(index)+L")"+to.extension().wstring(); const auto candidate=to.parent_path()/name; if(statFile(hosts.target,candidate)) continue; const auto result=transfer(candidate); if(topLevel) progress.renamedTarget=candidate; return result; } throw std::system_error(ERROR_ALREADY_EXISTS,std::system_category()); } const bool forceAsk=readOnly && progress.askReadOnly && progress.conflictMode!=ConflictMode::ask; auto answer=mode==ConflictMode::ask?(forceAsk?0:static_cast(progress.conflictChoice)):IDYES; if(!answer) { WarningPrompt warning{readOnly?L"Existing file is read-only":L"File already exists", to.c_str(),L"&Overwrite",L"&Skip",*incoming,*existing}; warningBrush=CreateSolidBrush(RGB(128,0,0)); answer=runFilesDialog(GetModuleHandleW(nullptr),MAKEINTRESOURCEW(104),progress.window, warningDialog,reinterpret_cast(&warning)); if(warning.remember && (answer==IDYES || answer==IDNO)) progress.conflictChoice=static_cast(answer); DeleteObject(warningBrush); warningBrush=nullptr; } if(answer==IDNO) return skip(readOnly?L"read-only conflict":L"conflict choice"); if(answer!=IDYES) { progress.canceled=answer==IDCANCEL; return CopyResult::failed; } return transfer(to,true,mode==ConflictMode::append); } void copySelection(HWND owner,Panel &source,Panel &destination,bool move) { if(copyInProgress) { setStatus(L"A copy is already in progress."); return; } CopyRequest request{{},destination.directory}; request.move=move; request.otherDirectory=destination.directory; request.sourcePeer=source.peer?&*source.peer:nullptr; request.otherPeer=request.destinationPeer=destination.peer?&*destination.peer:nullptr; for(size_t i=0;i(row)>=source.rows.size() || source.sizes[row]==L"Up") return; request.sources.push_back(source.rows[row]); } const auto result=runFilesDialog(GetModuleHandleW(nullptr),MAKEINTRESOURCEW(102),owner,copyDialog, reinterpret_cast(&request)); if(result==-1) setStatus(L"Could not open the Copy dialog."); if(result!=IDOK) return; const bool renamed=!request.newName.empty(); const auto targetDirectory=renamed && !move?request.sources.front().parent_path():request.destination; const auto *targetPeer=renamed && !move?request.sourcePeer:request.destinationPeer; const auto peerId=[](const FilesPeer *peer){return peer?peer->id:std::string{};}; const bool sameHost=peerId(request.sourcePeer)==peerId(targetPeer); CopyProgress progress; progress.move=move; progress.conflictMode=request.conflictMode; progress.askReadOnly=request.askReadOnly; progress.window=CreateDialogParamW(GetModuleHandleW(nullptr),MAKEINTRESOURCEW(103),owner, copyProgressDialog,reinterpret_cast(&progress)); if(!progress.window) { setStatus(L"Could not open copy progress."); return; } FilesDialogInput progressInput(progress.window); copyInProgress=true; activeCopy=&progress; for(auto &panel:panels) EnableWindow(panel.list,FALSE); for(const auto command:commands) EnableWindow(command,FALSE); GetWindowRect(owner,&previousOwnerBounds); ShowWindow(progress.window,SW_SHOW); bool failed=false, skipped=false; size_t copied=0; std::set results; std::wstring failure; try { FilesConnection sourceHost(request.sourcePeer); std::unique_ptr otherHost; if(!sameHost) otherHost=std::make_unique(targetPeer); const CopyEndpoints hosts{sourceHost,otherHost?*otherHost:sourceHost,sameHost, request.sourcePeer?request.sourcePeer->id:"local"}; for(const auto &path:request.sources) if(!countCopy(sourceHost,path,progress)) { failed=true; break; } updateCopyProgress(progress,0,0,true); for(const auto &path:request.sources) { if(failed) break; const auto to=targetDirectory/(renamed?request.newName:path.filename()); if(sameHost) { const auto canonical=[&](const fs::path &name) { const auto reply=sourceHost.call(fileRequest("canonical",name)); checkFileReply(reply); return filePath(reply.at("path")); }; const auto canonicalSource=canonical(path); const auto canonicalTarget=canonical(to); for(auto ancestor=canonicalTarget;;ancestor=ancestor.parent_path()) { if(CompareStringOrdinal(ancestor.c_str(),-1,canonicalSource.c_str(),-1,TRUE)==CSTR_EQUAL) { throw std::system_error(ERROR_INVALID_PARAMETER,std::system_category()); } if(!ancestor.has_relative_path()) break; } } if(failed) break; progress.renamedTarget.clear(); const auto outcome=copyItem(hosts,path,to,progress,true,request.sources.size()==1); if(outcome==CopyResult::failed) { failed=true; break; } if(outcome==CopyResult::skipped) { skipped=true; continue; } ++copied; results.insert(progress.renamedTarget.empty()?to:progress.renamedTarget); const auto row=std::find(source.rows.begin(),source.rows.end(),path); if(row!=source.rows.end()) source.marked[row-source.rows.begin()]=false; InvalidateRect(source.list,nullptr,FALSE); UpdateWindow(source.list); } } catch(const std::system_error &e){failed=true;failure=fileError(e);} catch(const std::exception &){failed=true;failure=L"File connection interrupted.";} const auto targetId=peerId(targetPeer); const bool restoreFocus=GetForegroundWindow()==progress.window; updateCopyProgress(progress,0,0,true); RedrawWindow(progress.window,nullptr,nullptr,RDW_UPDATENOW|RDW_ALLCHILDREN); DestroyWindow(progress.window); activeCopy=nullptr; copyInProgress=false; for(auto &panel:panels) EnableWindow(panel.list,TRUE); for(const auto command:commands) EnableWindow(command,TRUE); if(progress.closeOwner) { PostMessageW(owner,WM_CLOSE,0,0); return; } if(move) { open(source,source.directory); open(destination,destination.directory); if(renamed && copied && sameHost) { const auto renamedPath=progress.renamedTarget.empty()? targetDirectory/request.newName:progress.renamedTarget; const auto row=std::find(source.rows.begin(),source.rows.end(),renamedPath); if(row!=source.rows.end()) { SendMessageW(source.list,LB_SETCURSEL,row-source.rows.begin(),0); InvalidateRect(source.list,nullptr,FALSE); showFileDetails(source); } } } else { auto &updated=renamed?source:destination; if(updated.directory==targetDirectory) open(updated,updated.directory); } if(!results.empty()) { for(auto &panel:panels) { panel.transferResults.clear(); std::error_code error; const bool hostMatches=peerId(panel.peer?&*panel.peer:nullptr)==targetId; if(hostMatches && (panel.peer?CompareStringOrdinal(panel.directory.c_str(),-1, targetDirectory.c_str(),-1,TRUE)==CSTR_EQUAL:fs::equivalent(panel.directory,targetDirectory,error))) for(const auto &path:results) panel.transferResults.insert(panel.directory/path.filename()); InvalidateRect(panel.list,nullptr,FALSE); } transferHighlightPhase=0; if(!SetTimer(owner,transferHighlightTimer,450,nullptr)) for(auto &panel:panels) panel.transferResults.clear(); } if(restoreFocus) { SetForegroundWindow(owner); SetFocus(panels[active].list); } if(failed && !progress.canceled && !failure.empty()) setStatus((move?L"Move failed — ":L"Copy failed — ")+failure); else if(move) { if(progress.canceled) setStatus(L"Move canceled."); else if(failed) setStatus(L"Could not complete the move."); else if(skipped) setStatus(L"Move complete with skipped items."); else setStatus(L"Move complete."); } else if(progress.canceled) setStatus(L"Copy canceled."); else if(failed) setStatus(L"Could not complete the copy."); else if(skipped) setStatus(L"Copy complete with skipped items."); else setStatus(L"Copy complete."); } struct DeleteConfirmation { const wchar_t *message; std::wstring entries; int lines; bool permanent; bool folderPrompt; bool readOnlyPrompt{}; }; INT_PTR CALLBACK deleteConfirmationDialog(HWND window,UINT message,WPARAM wp,LPARAM lp) { if(message==WM_INITDIALOG) SetWindowLongPtrW(window,DWLP_USER,lp); const auto data=reinterpret_cast(GetWindowLongPtrW(window,DWLP_USER)); if(!data) return FALSE; if(const auto painted=paintCopyDialog(window,message,wp,lp, data->readOnlyPrompt?L" Warning ":data->folderPrompt?L" Delete folder ":L" Delete ", data->permanent,{243})) return painted; if(message==WM_INITDIALOG) { prepareCopyDialog(window); layoutDialogContent(window,240); SetDlgItemTextW(window,240,data->message); SetDlgItemTextW(window,241,data->entries.c_str()); if(data->permanent) SetDlgItemTextW(window,IDYES,L"&Delete"); for(const auto id:{240,241}) SendDlgItemMessageW(window,id,WM_SETFONT,reinterpret_cast(mono),FALSE); const auto entries=dialogControlBounds(window,241); const auto question=dialogControlBounds(window,240); const std::vector buttons=data->readOnlyPrompt? std::vector{IDYES,244,IDNO,249,IDCANCEL}:data->folderPrompt? std::vector{IDYES,244,IDNO,IDCANCEL}:std::vector{IDYES,IDCANCEL}; if(!data->readOnlyPrompt) ShowWindow(GetDlgItem(window,249),SW_HIDE); if(!data->folderPrompt && !data->readOnlyPrompt) for(const auto id:{244,IDNO}) ShowWindow(GetDlgItem(window,id),SW_HIDE); LONG buttonWidth=cellWidth*static_cast(buttons.size()-1); for(const auto id:buttons) { const auto rect=dialogControlBounds(window,id); buttonWidth+=rect.right-rect.left; } LONG contentWidth=buttonWidth; if(const auto dc=GetDC(window)) { const auto previous=SelectObject(dc,mono); const auto measure=[&](const wchar_t *text) { RECT rect{}; DrawTextW(dc,text,-1,&rect,DT_CALCRECT|DT_SINGLELINE|DT_NOPREFIX); contentWidth=std::max(contentWidth,rect.right); }; measure(data->message); std::wistringstream names(data->entries); for(std::wstring name;std::getline(names,name);) { if(!name.empty() && name.back()==L'\r') name.pop_back(); measure(name.c_str()); } SelectObject(dc,previous); ReleaseDC(window,dc); } MONITORINFO monitor{sizeof(monitor)}; if(GetMonitorInfoW(MonitorFromWindow(GetWindow(window,GW_OWNER),MONITOR_DEFAULTTONEAREST),&monitor)) contentWidth=std::min(contentWidth,std::max(buttonWidth,monitor.rcWork.right-monitor.rcWork.left-2*entries.left)); RECT outer{}; GetWindowRect(window,&outer); const auto width=contentWidth+2*entries.left; SetWindowPos(window,nullptr,0,0,width,outer.bottom-outer.top,SWP_NOMOVE|SWP_NOZORDER|SWP_NOACTIVATE); for(const auto id:{240,241}) { const auto control=GetDlgItem(window,id); const bool centered=id==240 || data->lines==1; SetWindowLongPtrW(control,GWL_STYLE, (GetWindowLongPtrW(control,GWL_STYLE)&~SS_TYPEMASK)|(centered?SS_CENTER:SS_LEFT)|SS_NOPREFIX); SetWindowPos(control,nullptr,entries.left,id==240?question.top:entries.top, contentWidth,(id==240?1:data->lines)*textHeight,SWP_NOZORDER|SWP_NOACTIVATE); } SetWindowPos(GetDlgItem(window,243),nullptr,entries.left,entries.top+data->lines*textHeight+10,0,0, SWP_NOSIZE|SWP_NOZORDER|SWP_NOACTIVATE); auto x=(width-buttonWidth)/2; for(const auto id:buttons) { const auto rect=dialogControlBounds(window,id); SetWindowPos(GetDlgItem(window,id),nullptr,x,rect.top,0,0, SWP_NOSIZE|SWP_NOZORDER|SWP_NOACTIVATE); x+=rect.right-rect.left+cellWidth; } if(data->readOnlyPrompt) layoutDialogButtons(window,243,{IDYES,244,IDNO,249,IDCANCEL}); else if(data->folderPrompt) layoutDialogButtons(window,243,{IDYES,244,IDNO,IDCANCEL}); else layoutDialogButtons(window,243,{IDYES,IDCANCEL}); SendMessageW(window,DM_SETDEFID,IDCANCEL,0); SetFocus(GetDlgItem(window,IDCANCEL)); return FALSE; } if(message==WM_CLOSE || (message==WM_COMMAND && LOWORD(wp)==IDCANCEL)) { finishFilesDialog(window,IDCANCEL); return TRUE; } if(message==WM_COMMAND && (LOWORD(wp)==IDYES || ((data->folderPrompt || data->readOnlyPrompt) && (LOWORD(wp)==244 || LOWORD(wp)==IDNO)) || (data->readOnlyPrompt && LOWORD(wp)==249))) { finishFilesDialog(window,LOWORD(wp)); return TRUE; } return FALSE; } INT_PTR CALLBACK deleteProgressDialog(HWND window,UINT message,WPARAM wp,LPARAM lp) { if(const auto painted=paintCopyDialog(window,message,wp,lp,L" Delete ",false,{248})) return painted; if(message==WM_INITDIALOG) { prepareCopyDialog(window); layoutDialogContent(window,245); SetWindowLongPtrW(window,DWLP_USER,lp); for(const auto id:{245,246}) SendDlgItemMessageW(window,id,WM_SETFONT,reinterpret_cast(mono),FALSE); SetWindowSubclass(GetDlgItem(window,247),paintCopyProgressBar,1,0); layoutDialogButtons(window,248,{IDCANCEL}); return TRUE; } if(message==WM_CLOSE || (message==WM_COMMAND && LOWORD(wp)==IDCANCEL)) { auto &canceled=*reinterpret_cast(GetWindowLongPtrW(window,DWLP_USER)); if(!canceled) canceled=confirmOperationCancellation(window,L"Do you really want to cancel deletion?"); return TRUE; } return FALSE; } bool pumpDeletion(HWND window,bool &canceled) { MSG message{}; while(!canceled && PeekMessageW(&message,nullptr,0,0,PM_REMOVE)) { if(message.message==WM_QUIT) { PostQuitMessage(static_cast(message.wParam)); canceled=true; break; } if(!filesDialogHandlesMessage(window,message)) { TranslateMessage(&message); DispatchMessageW(&message); } } return !canceled; } void deleteSelection(HWND owner,Panel &panel,bool permanent) { if(copyInProgress) return; std::vector paths; for(size_t i=0;i(row)>=panel.rows.size() || panel.sizes[row]==L"Up") return; paths.push_back(panel.rows[row]); } const auto shown=std::min(paths.size(),10); std::wstring entries; for(size_t i=0;iattributes; } catch(const std::system_error &e){setStatus(fileError(e));DeleteObject(warningBrush);warningBrush=nullptr;return;} catch(const std::exception &){setStatus(L"File connection interrupted.");DeleteObject(warningBrush);warningBrush=nullptr;return;} if(attributes!=INVALID_FILE_ATTRIBUTES) message=attributes&FILE_ATTRIBUTE_DIRECTORY ? (permanent?L"Delete this folder permanently?":L"Move this folder to the Recycle Bin?") : (permanent?L"Delete this file permanently?":L"Move this file to the Recycle Bin?"); } DeleteConfirmation confirmation{message,entries,static_cast(shown+(shown(&confirmation))!=IDYES) { DeleteObject(warningBrush); warningBrush=nullptr; // The dialog owns focus until DialogBoxParamW returns. Restore the invoking list. SetFocus(panel.list); return; } bool canceled=false; const auto window=CreateDialogParamW(instance,MAKEINTRESOURCEW(107),owner,deleteProgressDialog, reinterpret_cast(&canceled)); if(window) { GetWindowRect(owner,&previousOwnerBounds); FilesDialogInput progressInput(window); ShowWindow(window,SW_SHOW); size_t done=0, skipped=0, failed=0, total=0; bool all=false; int readOnlyChoice=0; std::vector> groups; std::wstring failure; try { FilesConnection host(panel.peer?&*panel.peer:nullptr); const auto collect=[&](auto &&self,const fs::path &path,std::vector &group)->void { for(const auto &[child,info]:childrenOf(host,path)) { if(!pumpDeletion(window,canceled)) return; group.push_back(child); if((info.attributes&FILE_ATTRIBUTE_DIRECTORY) && !(info.attributes&FILE_ATTRIBUTE_REPARSE_POINT)) self(self,child,group); } }; for(const auto &path:paths) { if(!pumpDeletion(window,canceled)) break; auto &group=groups.emplace_back(1,path); if(permanent) { try { const auto info=statFile(host,path); if(info && (info->attributes&FILE_ATTRIBUTE_DIRECTORY) && !(info->attributes&FILE_ATTRIBUTE_REPARSE_POINT)) collect(collect,path,group); } catch(const std::system_error &e) { failure=fileError(e); ++failed; ++total; group.clear(); } } total+=group.size(); } ULONGLONG lastPaint=0; fs::path current; const auto update=[&](const fs::path &path,bool force) { const auto now=GetTickCount64(); if(!force && now-lastPaint<200) return; lastPaint=now; SetDlgItemTextW(window,245,path.c_str()); const auto status=(permanent?L"Deleted: ":L"Moved: ")+std::to_wstring(done)+L" / "+std::to_wstring(total)+ L" Skipped: "+std::to_wstring(skipped)+L" Failed: "+std::to_wstring(failed); SetDlgItemTextW(window,246,status.c_str()); const auto bar=GetDlgItem(window,247); SendMessageW(bar,PBM_SETPOS,total?static_cast(100.0*(done+skipped+failed)/total):0,0); InvalidateRect(bar,nullptr,FALSE); }; for(const auto &group:groups) { if(!pumpDeletion(window,canceled)) break; if(group.empty()) continue; if(group.size()>1 && !all) { DeleteConfirmation folder{L"The following folder will be deleted:",group.front().wstring(),1,true,true}; const auto answer=runFilesDialog(instance,MAKEINTRESOURCEW(106),window,deleteConfirmationDialog, reinterpret_cast(&folder)); if(answer==244) all=true; else if(answer==IDNO) { skipped+=group.size(); update(group.front(),false); continue; } else if(answer!=IDYES) { canceled=true; break; } } for(auto it=group.rbegin();it!=group.rend() && pumpDeletion(window,canceled);++it) { current=*it; update(current,false); bool readOnly=false; try { const auto info=permanent?statFile(host,current):std::optional{}; const auto attributes=info?info->attributes:INVALID_FILE_ATTRIBUTES; if(attributes!=INVALID_FILE_ATTRIBUTES && (attributes&FILE_ATTRIBUTE_READONLY) && !(attributes&FILE_ATTRIBUTE_DIRECTORY)) { auto answer=static_cast(readOnlyChoice); if(!answer) { DeleteConfirmation warning{L"Delete this read-only file?",current.wstring(),1,true,false,true}; answer=runFilesDialog(instance,MAKEINTRESOURCEW(106),window,deleteConfirmationDialog, reinterpret_cast(&warning)); if(answer==244 || answer==249) readOnlyChoice=static_cast(answer); } if(answer==IDNO || answer==249) { ++skipped; continue; } if(answer!=IDYES && answer!=244) { canceled=true; break; } if(attributes&FILE_ATTRIBUTE_REPARSE_POINT) throw std::system_error(ERROR_ACCESS_DENIED,std::system_category()); readOnly=true; } if(!permanent && !panel.peer) { if(!ks::lan::recycleFile(window,current)) throw std::system_error(ERROR_ACCESS_DENIED,std::system_category()); } else { auto request=fileRequest(permanent?"remove":"recycle",current); request["readOnly"]=readOnly; checkFileReply(host.call(request)); } ++done; } catch(const std::system_error &e) { ++failed; failure=fileError(e); if(e.code().value()>=10000) break; } } } update(current,true); } catch(const std::system_error &e){++failed;failure=fileError(e);} catch(const std::exception &){++failed;failure=L"File connection interrupted.";} RedrawWindow(window,nullptr,nullptr,RDW_UPDATENOW|RDW_ALLCHILDREN); DestroyWindow(window); for(auto &visible:panels) if(!visible.directory.empty()) open(visible,visible.directory); SetFocus(panel.list); setStatus((canceled?L"Deletion canceled. ":L"Deletion finished. ")+ std::to_wstring(done)+(permanent?L" deleted, ":L" moved to Recycle Bin, ")+ std::to_wstring(skipped)+L" skipped, "+std::to_wstring(failed)+L" failed."+(failure.empty()?L"":L" "+failure)); } else setStatus(L"Could not open deletion progress."); DeleteObject(warningBrush); warningBrush=nullptr; } void selectPanelEdge(Panel &panel,bool first) { activatePanel(static_cast(&panel-panels)); const auto count=SendMessageW(panel.list,LB_GETCOUNT,0,0); if(count<=0) return; SendMessageW(panel.list,LB_SETCURSEL,first?0:count-1,0); InvalidateRect(panel.list,nullptr,FALSE); showFileDetails(panel); } LRESULT CALLBACK panelKeys(HWND window, UINT message, WPARAM wp, LPARAM lp, UINT_PTR, DWORD_PTR index) { if(message==WM_LBUTTONDOWN) { const auto count=SendMessageW(window,LB_GETCOUNT,0,0); RECT last{}; if(count>0 && SendMessageW(window,LB_GETITEMRECT,count-1,reinterpret_cast(&last))!=LB_ERR && GET_Y_LPARAM(lp)>=last.bottom) { selectPanelEdge(panels[index],false); return 0; } } const bool rangeKey=wp==VK_UP || wp==VK_DOWN || wp==VK_LEFT || wp==VK_RIGHT; if(message==WM_KEYDOWN && (wp==VK_INSERT || (rangeKey && (GetKeyState(VK_SHIFT)&0x8000)))) { auto &panel=panels[index]; const auto row=SendMessageW(window,LB_GETCURSEL,0,0); if(row<0 || static_cast(row)>=panel.marked.size()) return 0; const auto last=static_cast(panel.marked.size()-1); auto target=row; switch(wp) { case VK_UP: target=std::max(0,row-1); break; case VK_LEFT: target=0; break; case VK_RIGHT: target=last; break; default: target=std::min(last,row+1); break; } const bool mark=!panel.marked[row]; const bool toBoundary=wp==VK_LEFT || wp==VK_RIGHT; const auto first=toBoundary ? std::min(row,target) : row; const auto end=toBoundary ? std::max(row,target) : row; for(auto i=first;i<=end;++i) if(panel.sizes[i]!=L"Up") panel.marked[i]=mark; SendMessageW(window,LB_SETCURSEL,target,0); InvalidateRect(window,nullptr,FALSE); showFileDetails(panel); return 0; } 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_SETFOCUS) activatePanel(static_cast(index)); if(message==WM_CHAR && wp>=32 && wp!=127) { SetFocus(terminalInput); return SendMessageW(terminalInput,message,wp,lp); } if (message == WM_KEYDOWN) { if(wp==VK_TAB) { activatePanel(static_cast(1-index)); SetFocus(terminalInput); return 0; } if (wp == VK_RETURN) { enter(panels[index]); return 0; } if (wp == VK_F2) { open(panels[index], panels[index].directory); return 0; } if (wp == VK_F10) { SendMessageW(GetParent(window), WM_CLOSE, 0, 0); return 0; } if (wp == VK_F3 || wp == VK_F4) { const auto &panel = panels[index]; const auto row=currentPanelRow(panel); if(row==LB_ERR) return 0; const bool edit = wp == VK_F4; openPanelFile(panel, panel.rows[row], edit ? L"edit" : L"open", edit ? L"No editor is registered for this file." : L"Cannot open the selected file."); return 0; } if (wp == VK_F5 || wp == VK_F6) { copySelection(GetParent(window),panels[index],panels[1-index],wp==VK_F6); return 0; } if(wp==VK_F8 || (wp==VK_DELETE && (GetKeyState(VK_SHIFT)&0x8000))) { deleteSelection(GetParent(window),panels[index],wp==VK_DELETE); return 0; } } auto result=scrollList(window, message, wp, lp); if(message==WM_PAINT && panels[index].wide) { auto dc=GetDC(window); if(dc) { RECT line{}; GetClientRect(window,&line); line.left=std::max(0L,line.right-(sizeWidth+2)*static_cast(cellWidth)); line.right=line.left+1; SetDCBrushColor(dc,colors[Frame]); FillRect(dc,&line,static_cast(GetStockObject(DC_BRUSH))); ReleaseDC(window,dc); } } return result; } #include "files_folder_shortcuts.inl" #include "files_command_history_dialog.inl" bool loadFilesPreferences(HWND window) { LOGFONTW font{}; DWORD bytes=sizeof(font); if(RegGetValueW(HKEY_CURRENT_USER,schemeKey,L"DisplayFont",RRF_RT_REG_BINARY,nullptr,&font,&bytes) !=ERROR_SUCCESS || bytes!=sizeof(font)) { font=defaultFilesFont(); } font.lfFaceName[LF_FACESIZE-1]=0; if(!applyFont(window,font,static_cast(std::clamp(preference(L"FontPoints",defaultFontPoints),80UL,240UL)))) return false; loadColors(); dialogFramePadding={ std::min(preference(dialogFrameOuterKey,defaultDialogFramePadding.outer),maxDialogFramePadding), std::min(preference(dialogFrameInnerKey,defaultDialogFramePadding.inner),maxDialogFramePadding), std::min(preference(dialogRowTextKey,defaultDialogFramePadding.rowText),maxDialogRowTextPadding)}; loadFileStyles(); loadFolderShortcuts(); panels[0].wide=preference(L"LeftWide",defaultWide)!=0; panels[1].wide=preference(L"RightWide",defaultWide)!=0; sizeWidth=static_cast(std::clamp(preference(L"SizeWidth",defaultSizeWidth),6UL,16UL)); alignExtensions=preference(L"AlignExtensions",defaultAlignExtensions)!=0; showDetails=preference(L"ShowDetails",defaultShowDetails)!=0; alignFolders=preference(L"AlignFolders",defaultAlignFolders)!=0; statusFields=preference(L"StatusFields",defaultStatusFields)&15; loadCommandHistoryKeys(); return true; } #include "files_options.inl" void openMenu(HWND window, int index) { if(index==3) { hidePicker(); showMenu(false); editOptions(window); return; } menuIndex=index; commandPicker=true; SendMessageW(picker,LB_RESETCONTENT,0,0); std::vector rows; if(index==0 || index==4) { const bool wide=panels[index==4].wide; rows={wide?L" Brief":L"✓ Brief", wide?L"✓ Wide":L" Wide", index==0?L" Change drive\tAlt+F1":L" Change drive\tAlt+F2"}; } 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",L"Folder shortcuts"}; std::set 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=L'A' && key<=L'Z' && reserve(i,static_cast(key))) break; } SendMessageW(picker,LB_ADDSTRING,0,reinterpret_cast(text.c_str())); } RECT anchor{}, client{}; GetWindowRect(menuItems[index],&anchor); MapWindowPoints(nullptr,window,reinterpret_cast(&anchor),2); GetClientRect(window,&client); const int inset=dialogFrameMetrics(GetDpiForWindow(window)).content(); const int width=std::max(1,std::min(static_cast(client.right)-2*inset,26*cellWidth+16)); const int x=std::clamp(static_cast(anchor.left)+inset,inset, std::max(inset,static_cast(client.right)-width-inset)); const int y=anchor.bottom+5; const int height=static_cast(rows.size())*static_cast(SendMessageW(picker,LB_GETITEMHEIGHT,0,0)); SetWindowPos(pickerFrame,HWND_TOP,x-inset,y-inset,width+2*inset,height+2*inset,SWP_SHOWWINDOW); SetWindowPos(picker,HWND_TOP,x,y,width,height,SWP_SHOWWINDOW); SendMessageW(picker,LB_SETCURSEL,0,0); SetFocus(picker); 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) { return !modified; } if((message==WM_SYSKEYUP || message==WM_KEYUP) && key==VK_MENU) return !modified; if(message!=WM_KEYDOWN && message!=WM_SYSKEYDOWN) return false; if(modified) return false; if(message==WM_KEYDOWN && key==VK_F9) { if(!repeated) toggleFilesMenu(); return true; } if(IsWindowVisible(menuBar) && message==WM_SYSKEYDOWN && (flags & (1LL << 29)) && key>=L'A' && key<=L'Z') { const auto category=menuAccessKeys.find(static_cast(key)); if(category!=menuAccessKeys.npos) { if(!repeated) openMenu(window,static_cast(category)); return true; } } return false; } void searchPanelByPrefix(HWND window,wchar_t character) { if(character==L'\b') { if(!panelSearchPrefix.empty()) panelSearchPrefix.pop_back(); } else panelSearchPrefix+=character; auto &panel=panels[active]; const auto length=static_cast(panelSearchPrefix.size()); for(size_t row=0;length && row=32 && message.wParam!=127 && (!alt || message.wParam!=L' ')))) { searchPanelByPrefix(window,static_cast(message.wParam)); return true; } return false; } if(message.message!=WM_KEYDOWN && message.message!=WM_SYSKEYDOWN) return false; if(!modified && message.wParam!=VK_SPACE && message.message==WM_SYSKEYDOWN && alt && (message.wParam==VK_BACK || MapVirtualKeyW(static_cast(message.wParam),MAPVK_VK_TO_CHAR))) { TranslateMessage(&message); return true; } if(!searching || message.wParam==VK_MENU || message.wParam==VK_SHIFT) return false; if(!modified && !alt && (message.wParam==VK_BACK || MapVirtualKeyW(static_cast(message.wParam),MAPVK_VK_TO_CHAR)) && message.wParam!=VK_RETURN && message.wParam!=VK_ESCAPE && message.wParam!=VK_TAB) { TranslateMessage(&message); return true; } closePanelSearch(); if(message.wParam==VK_RETURN && !modified && !alt) { SendMessageW(panels[active].list,message.message,message.wParam,message.lParam); return true; } return message.wParam==VK_ESCAPE && !modified && !alt; } void restoreCommandFocus() { if(GetFocus()!=terminalInput && !GetCapture() && !copyInProgress && !activeFilesDialog && !commandHistoryWindow && !IsWindowVisible(menuBar) && !IsWindowVisible(picker)) SetFocus(terminalInput); } LRESULT CALLBACK filesWindow(HWND window, UINT message, WPARAM wp, LPARAM lp) { if(message==filesPreferencesChanged) { if(reinterpret_cast(wp)!=window) { loadFilesPreferences(window); layout(window); layoutOpenCommandHistory(); showFileDetails(panels[active]); RedrawWindow(window,nullptr,nullptr,RDW_INVALIDATE|RDW_ALLCHILDREN); } return 0; } switch (message) { case WM_SETFOCUS: restoreCommandFocus(); return 0; case WM_ACTIVATEAPP: if(!wp) closePanelSearch(); break; case WM_COPYDATA: { const auto *reply=reinterpret_cast(lp); if(!drivePeerService || reinterpret_cast(wp)!=drivePeerService || !reply || reply->dwData!=FilesLanPeersReply || reply->cbData>FilesLanPeersLimit || (!reply->cbData || !reply->lpData)) return FALSE; try { const auto *begin=static_cast(reply->lpData); const auto rows=Json::parse(begin,begin+reply->cbData); if(!rows.is_array()) return FALSE; for(const auto &row:rows) drivePeers.push_back({row.at("id"),row.at("address"),ks::wide(row.at("name")),ks::lan::FilesPort,row.value("filesBrowse",false)}); } catch(const std::exception &){drivePeers.clear();return FALSE;} drivePeerReplyReceived=true; return TRUE; } case WM_TIMER: if(wp==driveQueryTimer) {pollDriveQueries(window);return 0;} if(wp==terminalTimer) { updateTerminalOutput(); return 0; } if(wp==directoryWatchTimer) { refreshChangedPanels(); return 0; } if(wp!=transferHighlightTimer) break; ++transferHighlightPhase; for(auto &panel:panels) { if(panel.transferResults.empty()) continue; if(transferHighlightPhase==4) panel.transferResults.clear(); InvalidateRect(panel.list,nullptr,FALSE); } if(transferHighlightPhase==4) KillTimer(window,transferHighlightTimer); return 0; case WM_CREATE: { auto instance = GetModuleHandleW(nullptr); if(!loadFilesPreferences(window)) return -1; menuBar=CreateWindowW(L"BUTTON",L"",WS_CHILD|WS_CLIPSIBLINGS|BS_OWNERDRAW, 0,0,0,0,window,reinterpret_cast(120),instance,nullptr); const wchar_t *menus[]{L"Left",L"Files",L"Commands",L"Options",L"Right"}; for(int i=0;i<5;++i) { menuItems[i]=CreateWindowW(L"BUTTON",menus[i],WS_CHILD|WS_CLIPSIBLINGS|BS_OWNERDRAW, 0,0,0,0,window,reinterpret_cast(130+static_cast(i)),instance,nullptr); if(!menuItems[i]) return -1; } picker=CreateWindowExW(0,L"LISTBOX",L"Change drive", WS_CHILD|WS_CLIPSIBLINGS|LBS_NOTIFY|LBS_OWNERDRAWFIXED|LBS_HASSTRINGS|LBS_NOINTEGRALHEIGHT, 0,0,0,0,window,reinterpret_cast(121),instance,nullptr); pickerFrame=CreateWindowW(L"STATIC",L"",WS_CHILD|WS_CLIPSIBLINGS|SS_OWNERDRAW, 0,0,0,0,window,reinterpret_cast(122),instance,nullptr); SetWindowSubclass(picker,pickerKeys,1,0); SendMessageW(picker,LB_SETITEMHEIGHT,0,textHeight); status = CreateWindowW(L"STATIC", L"", WS_CHILD | WS_VISIBLE | SS_PATHELLIPSIS, 0, 0, 0, 0, window, reinterpret_cast(144), instance, nullptr); panelSearchPopup=CreateWindowW(L"STATIC",L"Search",WS_CHILD|WS_CLIPSIBLINGS|SS_OWNERDRAW|SS_NOTIFY, 0,0,0,0,window,reinterpret_cast(148),instance,nullptr); terminalInput=CreateWindowW(L"EDIT",L"",WS_CHILD|WS_VISIBLE|ES_AUTOHSCROLL, 0,0,0,0,window,nullptr,instance,nullptr); terminalPrompt=CreateWindowW(L"STATIC",L"",WS_CHILD|WS_VISIBLE|SS_PATHELLIPSIS, 0,0,0,0,window,nullptr,instance,nullptr); terminalOutput=CreateWindowW(L"STATIC",L"",WS_CHILD|WS_CLIPCHILDREN, 0,0,0,0,window,reinterpret_cast(145),instance,nullptr); terminalBackground=CreateSolidBrush(RGB(24,24,24)); if(!terminalInput || !terminalOutput || !terminalPrompt || !terminalBackground) return -1; for(const auto control:{terminalInput,terminalOutput,terminalPrompt}) SendMessageW(control,WM_SETFONT,reinterpret_cast(mono),FALSE); SendMessageW(terminalInput,EM_SETLIMITTEXT,2048,0); SendMessageW(terminalInput,EM_SETMARGINS,EC_LEFTMARGIN|EC_RIGHTMARGIN,0); SetWindowSubclass(terminalInput,terminalInputKeys,1,0); SetTimer(window,terminalTimer,50,nullptr); SetTimer(window,directoryWatchTimer,200,nullptr); if (!mono || !blue || !teal || !menuBar || !picker || !pickerFrame || !status || !panelSearchPopup) return -1; SendMessageW(status, WM_SETFONT, reinterpret_cast(mono), FALSE); const wchar_t *labels[]{L"F2 Refresh", L"F3 View", L"F4 Edit", L"F5 Copy", L"F6 Move", L"F10 Exit"}; for (int i = 0; i < static_cast(std::size(commands)); ++i) commands[i] = CreateWindowW(L"BUTTON", labels[i], WS_CHILD | WS_VISIBLE | BS_OWNERDRAW, 0, 0, 0, 0, window, reinterpret_cast(110 + static_cast(i)), instance, nullptr); for (int i = 0; i < 2; ++i) { auto &panel = panels[i]; panel.path = CreateWindowW(L"STATIC", L"", WS_CHILD | WS_VISIBLE | WS_CLIPSIBLINGS | SS_OWNERDRAW, 0, 0, 0, 0, window, reinterpret_cast(142 + static_cast(i)), instance, nullptr); panel.list = CreateWindowExW(0, L"LISTBOX", L"", WS_CHILD | WS_VISIBLE | WS_CLIPSIBLINGS | LBS_NOTIFY | LBS_NOINTEGRALHEIGHT | LBS_OWNERDRAWFIXED | LBS_HASSTRINGS, 0, 0, 0, 0, window, reinterpret_cast(102 + static_cast(i)), instance, nullptr); panel.heading=CreateWindowW(L"STATIC",L"",WS_CHILD|WS_CLIPSIBLINGS|SS_OWNERDRAW, 0,0,0,0,window,reinterpret_cast(140+static_cast(i)),instance,nullptr); if (!panel.path || !panel.list || !panel.heading) return -1; for (auto child : {panel.path, panel.list}) SendMessageW(child, WM_SETFONT, reinterpret_cast(mono), FALSE); SendMessageW(panel.list,LB_SETITEMHEIGHT,0,textHeight); wchar_t system[MAX_PATH]{}; GetWindowsDirectoryW(system, MAX_PATH); const auto saved=savedPanelPath(panelPathKeys[i]); if(saved.is_absolute()) open(panel,saved); if(!panel.directory.empty()) { const auto cursor=savedPanelPath(panelCursorKeys[i]); const auto row=std::find(panel.rows.begin(),panel.rows.end(),cursor); if(row!=panel.rows.end()) SendMessageW(panel.list,LB_SETCURSEL,row-panel.rows.begin(),0); } if(panel.directory.empty()) open(panel,fs::path(system).root_path()); SetWindowSubclass(panel.list, panelKeys, 1, i); } SetFocus(panels[0].list); return 0; } case WM_DPICHANGED: { hidePicker(); const auto &bounds=*reinterpret_cast(lp); applyFont(window,displayFont,fontPoints); SetWindowPos(window,nullptr,bounds.left,bounds.top,bounds.right-bounds.left, bounds.bottom-bounds.top,SWP_NOZORDER|SWP_NOACTIVATE); layout(window); if(activeCopy) PostMessageW(activeCopy->window,refreshProgressLayout,0,0); RedrawWindow(window,nullptr,nullptr,RDW_INVALIDATE|RDW_ALLCHILDREN); return 0; } case WM_MOVE: layoutOpenCommandHistory(true); if((copyInProgress || activeFilesDialog) && !IsIconic(window)) { RECT bounds{}; if(GetWindowRect(window,&bounds)) { const OwnedWindowMove move{window,bounds.left-previousOwnerBounds.left, bounds.top-previousOwnerBounds.top}; previousOwnerBounds=bounds; if(move.dx || move.dy) EnumThreadWindows(GetCurrentThreadId(),moveOwnedDialog,reinterpret_cast(&move)); } } return 0; case WM_SIZE: hidePicker(); layout(window); layoutOpenCommandHistory(); return 0; case WM_GETMINMAXINFO: reinterpret_cast(lp)->ptMinTrackSize = {540, 220}; return 0; case WM_SETCURSOR: case WM_LBUTTONDOWN: { POINT point{}; GetCursorPos(&point); ScreenToClient(window, &point); RECT rect{}; GetClientRect(window, &rect); const int width = rect.right; int middle = std::clamp(split ? split : width / 2, 120, std::max(120, width - 120)); if (point.y >= 0 && point.y < rect.bottom-2*textHeight-14 && abs(point.x - middle) <= 4) { SetCursor(LoadCursorW(nullptr, IDC_SIZEWE)); if (message == WM_LBUTTONDOWN) { dragging = true; SetCapture(window); } return TRUE; } if(message==WM_LBUTTONDOWN && !terminalVisible) { for(auto &panel:panels) { RECT list{}; if(!GetWindowRect(panel.list,&list)) continue; MapWindowPoints(nullptr,window,reinterpret_cast(&list),2); if(point.x>=list.left && point.x=0 && point.y(&box),2); const LONG columnX=std::max(box.left,box.right-(sizeWidth+2)*static_cast(cellWidth)); box.right+=5; InflateRect(&box,2,2); box.left-=2; RECT pathBounds{}; GetWindowRect(panel.path,&pathBounds); MapWindowPoints(nullptr,window,reinterpret_cast(&pathBounds),2); box.top=pathBounds.top+(pathBounds.bottom-pathBounds.top)/2-2; RECT details{}; GetWindowRect(status,&details); MapWindowPoints(nullptr,window,reinterpret_cast(&details),2); const RECT divider{box.left+1,box.bottom-2,box.right-1,box.bottom-1}; box.bottom=std::max(box.bottom,details.bottom+6); FrameRect(dc,&box,border); paintScrollPosition(window,dc,panel.list,colors[PanelBlue]); InflateRect(&box,-3,-3); FrameRect(dc,&box,border); FillRect(dc,÷r,border); if(panel.wide) { RECT column{columnX,box.top+1,columnX+1,pathBounds.bottom+6}; FillRect(dc,&column,border); } } DeleteObject(border); EndPaint(window,&paint); return 0; } case WM_CTLCOLOREDIT: case WM_CTLCOLORSTATIC: { auto dc=reinterpret_cast(wp); if(reinterpret_cast(lp)==terminalInput || reinterpret_cast(lp)==terminalOutput || reinterpret_cast(lp)==terminalPrompt) { SetTextColor(dc,RGB(232,232,232)); SetBkColor(dc,RGB(24,24,24)); return reinterpret_cast(terminalBackground); } SetTextColor(dc,colors[Frame]); SetBkColor(dc,colors[PanelBlue]); return reinterpret_cast(blue); } case WM_CTLCOLORLISTBOX: return reinterpret_cast(reinterpret_cast(lp)==picker?teal:blue); case WM_DRAWITEM: { const auto &item=*reinterpret_cast(lp); if(item.hwndItem==panelSearchPopup) { const auto frame=dialogFrameMetrics(GetDpiForWindow(window)); const auto surface=GetSysColorBrush(COLOR_BTNFACE); const auto border=GetSysColorBrush(COLOR_BTNTEXT); const auto ink=GetSysColor(COLOR_BTNTEXT); const auto box=paintDialogFrame(item.hDC,item.rcItem,frame,surface,border); paintDialogFrameCaptions(item.hDC,item.rcItem,box,L"Search",surface,ink,mono); auto field=item.rcItem; InflateRect(&field,-frame.content(),-frame.content()); const auto cells=static_cast(std::max(0L,field.right-field.left-10)/std::max(1,cellWidth)); const auto start=panelSearchPrefix.size()>cells?panelSearchPrefix.size()-cells:0; paintText(item.hDC,field,panelSearchPrefix.c_str()+start,RGB(0,0,0),RGB(0,128,128)); return TRUE; } if(item.CtlID==142 || item.CtlID==143) { auto bounds=item.rcItem; FillRect(item.hDC,&bounds,blue); const auto &panel=panels[item.CtlID-142]; const auto path=(panel.peer?panel.peer->name+L" · ":std::wstring{})+panel.directory.native(); const auto middle=bounds.top+(bounds.bottom-bounds.top)/2; SetDCBrushColor(item.hDC,colors[Frame]); const auto lineBrush=static_cast(GetStockObject(DC_BRUSH)); RECT upper{bounds.left,middle-2,bounds.right,middle-1}; RECT lower{bounds.left,middle+1,bounds.right,middle+2}; FillRect(item.hDC,&upper,lineBrush); FillRect(item.hDC,&lower,lineBrush); if(panels[item.CtlID-142].wide) { const auto index=item.CtlID-142; RECT list{}; GetWindowRect(panels[index].list,&list); MapWindowPoints(nullptr,item.hwndItem,reinterpret_cast(&list),2); const auto x=std::max(list.left,list.right-(sizeWidth+2)*static_cast(cellWidth)); RECT column{x,middle,x+1,bounds.bottom}; FillRect(item.hDC,&column,lineBrush); } if(path.empty()) return TRUE; const int saved=SaveDC(item.hDC); SelectObject(item.hDC,mono); SIZE text{}; GetTextExtentPoint32W(item.hDC,path.c_str(),static_cast(path.size()),&text); const auto width=std::min(bounds.right-bounds.left,text.cx+2L*cellWidth); bounds.left+=(bounds.right-bounds.left-width)/2; bounds.right=bounds.left+width; const bool highlighted=static_cast(item.CtlID)-142==active; FillRect(item.hDC,&bounds,highlighted?teal:blue); const auto padding=std::min(static_cast(cellWidth),width/4); InflateRect(&bounds,-padding,0); SetTextColor(item.hDC,colors[highlighted?Text:Frame]); SetBkMode(item.hDC,TRANSPARENT); DrawTextW(item.hDC,path.c_str(),-1,&bounds, DT_CENTER|DT_VCENTER|DT_SINGLELINE|DT_PATH_ELLIPSIS|DT_NOPREFIX); if(saved) RestoreDC(item.hDC,saved); return TRUE; } if(item.CtlID==140 || item.CtlID==141) { auto name=item.rcItem, size=item.rcItem; size.left=std::max(size.left,size.right-(sizeWidth+2)*static_cast(cellWidth)); name.right=size.left; paintText(item.hDC,name,L"Name",RGB(255,255,0),colors[PanelBlue]); paintText(item.hDC,size,L"Size",RGB(255,255,0),colors[PanelBlue],true); RECT line{size.left,size.top,size.left+1,size.bottom}; SetDCBrushColor(item.hDC,colors[Frame]); FillRect(item.hDC,&line,static_cast(GetStockObject(DC_BRUSH))); return TRUE; } if(item.hwndItem==pickerFrame) { auto border=CreateSolidBrush(commandPicker?colors[Frame]:RGB(255,255,255)); const auto box=paintDialogFrame(item.hDC,item.rcItem, dialogFrameMetrics(GetDpiForWindow(window)),teal,border); if(!commandPicker) { if(!drivePaths.empty()) { RECT list{}; GetWindowRect(picker,&list); MapWindowPoints(nullptr,pickerFrame,reinterpret_cast(&list),2); for(size_t i=1;i=0) { text.resize(length+1); SendMessageW(item.hwndItem,LB_GETTEXT,item.itemID,reinterpret_cast(text.data())); } } if(item.CtlType==ODT_BUTTON) { wchar_t label[64]{}; GetWindowTextW(item.hwndItem,label,64); text=label; } auto background=item.CtlType==ODT_BUTTON?Footer:item.hwndItem==picker?Dialog:PanelBlue; auto foreground=colors[Text]; if(item.hwndItem==picker && !commandPicker && item.itemID==drivePaths.size()) { auto row=item.rcItem; FillRect(item.hDC,&row,teal); const RECT line{row.left,row.top,row.right,row.top+1}; SetDCBrushColor(item.hDC,RGB(255,255,255)); FillRect(item.hDC,&line,static_cast(GetStockObject(DC_BRUSH))); ++row.top; paintDialogCaption(item.hDC,row,L"KSwitcher LAN",teal,colors[Text],mono); const RECT sectionLine{row.left,row.bottom-1,row.right,row.bottom}; FillRect(item.hDC,§ionLine,static_cast(GetStockObject(DC_BRUSH))); return TRUE; } if(item.CtlID>=130 && item.CtlID<=134 && static_cast(item.CtlID)-130==menuIndex) background=Selection; const bool panelRow=item.CtlID==102 || item.CtlID==103; const bool activePanelRow=panelRow && item.hwndItem==panels[active].list; if(panelRow) { const auto cursor=SendMessageW(item.hwndItem,LB_GETCURSEL,0,0); if(activePanelRow && cursor!=LB_ERR && static_cast(cursor)==item.itemID) background=PanelCursor; } else if(item.itemState&(ODS_SELECTED|ODS_FOCUS)) background=Selection; COLORREF rowBackground=colors[background]; HFONT rowFont{}; if(panelRow) { const auto &panel=panels[item.CtlID-102]; if(item.itemID(item.itemID)==driveHover? hover:colors[background]); ExtTextOutW(item.hDC,0,0,ETO_OPAQUE,&item.rcItem,L"",0,nullptr); SetBkMode(item.hDC,TRANSPARENT); SetDCBrushColor(item.hDC,RGB(255,255,255)); const auto brush=static_cast(GetStockObject(DC_BRUSH)); size_t start=0; for(size_t i=0;i(length),nullptr); if(i==0 && length) { SetTextColor(item.hDC,RGB(255,255,0)); ExtTextOutW(item.hDC,x,item.rcItem.top+3,ETO_CLIPPED,&item.rcItem,text.c_str(),1,nullptr); } if(end==text.npos) break; start=end+1; } SetBkMode(item.hDC,OPAQUE); } else if(item.hwndItem==picker && !commandPicker && item.itemID>drivePaths.size()) { auto bounds=item.rcItem; SelectObject(item.hDC,mono); SetTextColor(item.hDC,foreground); SetBkColor(item.hDC,colors[background]); ExtTextOutW(item.hDC,bounds.left+driveColumnStarts[0],bounds.top+3, ETO_OPAQUE|ETO_CLIPPED,&bounds,text.c_str(),lstrlenW(text.c_str()),nullptr); } else if(item.CtlID>=130 && item.CtlID<=134) paintMenuLabel(item.hDC,item.rcItem,text,0,foreground,colors[background]); else paintText(item.hDC,item.rcItem,text.c_str(),foreground,rowBackground,false,rowFont); if(item.CtlID==102 || item.CtlID==103) { const auto &panel=panels[item.CtlID-102]; if(panel.wide && item.itemID(cellWidth)); name.right=size.left; bool folder=panel.sizes[item.itemID]==L"Folder"; if(folder) text=panel.rows[item.itemID].filename().wstring(); const auto extensionLength=std::max(alignFolders?panel.folderExtensionLength:0, alignExtensions?panel.fileExtensionLength:0); if(panel.sizes[item.itemID]!=L"Up" && extensionLength && (folder?alignFolders:alignExtensions)) { auto extension=panel.rows[item.itemID].extension().wstring(); if(extension.size()>1) { auto end=name; const auto width=static_cast(extensionLength)*cellWidth+5; end.left=std::max(end.left+static_cast(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,rowBackground,true,rowFont); text=panel.rows[item.itemID].stem().wstring(); } } 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(sizeWidth)); 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(GetStockObject(DC_BRUSH))); } } if (item.CtlType==ODT_LISTBOX && item.hwndItem!=picker) { RECT track{}; GetWindowRect(item.hwndItem,&track); MapWindowPoints(nullptr,window,reinterpret_cast(&track),2); track.left=track.right; track.right+=5; InvalidateRect(window,&track,FALSE); } return TRUE; } case WM_COMMAND: if(copyInProgress || commandHistoryWindow || activeFilesDialog) return 0; if(LOWORD(wp)==121 && HIWORD(wp)==LBN_DBLCLK) selectDrive(); if(LOWORD(wp)>=130 && LOWORD(wp)<=134 && HIWORD(wp)==BN_CLICKED) { openMenu(window,LOWORD(wp)-130); return 0; } if (LOWORD(wp) >= 110 && LOWORD(wp) <= 115 && HIWORD(wp) == BN_CLICKED) { const WPARAM keys[]{VK_F2, VK_F3, VK_F4, VK_F5, VK_F6, VK_F10}; SendMessageW(panels[active].list, WM_KEYDOWN, keys[LOWORD(wp) - 110], 0); } if (LOWORD(wp) >= 102 && LOWORD(wp) <= 103 && HIWORD(wp) == LBN_DBLCLK) enter(panels[LOWORD(wp) - 102]); if (LOWORD(wp) >= 102 && LOWORD(wp) <= 103 && HIWORD(wp) == LBN_SELCHANGE) { const auto &panel=panels[LOWORD(wp) - 102]; InvalidateRect(panel.list,nullptr,FALSE); showFileDetails(panel); } return 0; case WM_CLOSE: if(activeFilesDialog && !activeCopy) { PostMessageW(activeFilesDialog,WM_CLOSE,0,0); return 0; } if(activeCopy) { if(activeFilesDialog!=activeCopy->window) { setStatus(L"Resolve the copy prompt before closing."); return 0; } SendMessageW(activeCopy->window,WM_COMMAND,IDCANCEL,0); activeCopy->closeOwner=activeCopy->canceled; return 0; } savePlacement(window); for(int i=0;i<2;++i) { const auto &path=panels[i].directory.native(); if(!panels[i].peer && !path.empty()) RegSetKeyValueW(HKEY_CURRENT_USER,schemeKey,panelPathKeys[i],REG_SZ,path.c_str(), static_cast((path.size()+1)*sizeof(wchar_t))); const auto row=SendMessageW(panels[i].list,LB_GETCURSEL,0,0); const auto cursor=row>=0 && static_cast(row)((cursor.size()+1)*sizeof(wchar_t))); } DestroyWindow(window); return 0; case WM_DESTROY: KillTimer(window,terminalTimer); KillTimer(window,directoryWatchTimer); for(auto &panel:panels) panel.watch.reset(); terminalView.close(); terminal.close(); PostQuitMessage(0); return 0; } return DefWindowProcW(window, message, wp, lp); } } int WINAPI wWinMain(HINSTANCE instance, HINSTANCE, PWSTR, int show) { bool writeLog=true; try { const auto settings=ks::dataDirectory()/L"settings.json"; if(std::filesystem::exists(settings)) writeLog=Json::parse(ks::readFile(settings,1024*1024)).value("writeLog",true); } catch(const std::exception &) {} ks::setLogFilename(L"kswitcher-files.log"); ks::setLoggingEnabled(writeLog); int count{}; if(auto args=CommandLineToArgvW(GetCommandLineW(),&count)) { if(count==4 && std::wstring_view(args[1])==L"--list-folder") { auto result=listElevatedFolder(fs::path(args[2]).parent_path(),args[3]); LocalFree(args); return static_cast(result); } for(int i=1;i=WM_KEYFIRST && message.message<=WM_KEYLAST && GetFocus()==terminalInput) message.hwnd=terminalInput; if(commandHistoryWindow) { if(message.message==WM_KEYDOWN && (message.wParam==VK_RETURN || message.wParam==VK_ESCAPE) && (message.hwnd==commandHistoryWindow || IsChild(commandHistoryWindow,message.hwnd))) { SendMessageW(commandHistoryWindow,WM_COMMAND,message.wParam==VK_RETURN?IDOK:IDCANCEL,0); continue; } if(IsDialogMessageW(commandHistoryWindow,&message)) continue; const bool ownerClient=message.hwnd==window || IsChild(window,message.hwnd); const bool clientInput=(message.message>=WM_KEYFIRST && message.message<=WM_KEYLAST) || (message.message>=WM_MOUSEFIRST && message.message<=WM_MOUSELAST) || message.message==WM_CONTEXTMENU; if(ownerClient && clientInput) continue; } if(!copyInProgress && handlePanelSearchKey(window,message)) continue; if(message.message==WM_KEYDOWN && (message.wParam==VK_UP || message.wParam==VK_DOWN) && (GetKeyState(VK_CONTROL)&0x8000) && !(GetKeyState(VK_MENU)&0x8000) && (message.hwnd==window || IsChild(window,message.hwnd)) && !terminalVisible && !copyInProgress) { RECT bounds{}; GetClientRect(window,&bounds); const auto maximum=std::max(0,static_cast(bounds.bottom)/textHeight-8); const int rows=std::clamp(terminalRows+(message.wParam==VK_UP?1:-1),0,maximum); traceTerminal(message.wParam==VK_UP?"key-grow":"key-shrink",terminalRows,rows); if(rows && !startTerminal(window)) continue; terminalRows=rows; layout(window); continue; } if(message.message==WM_KEYDOWN && message.wParam=='O' && (GetKeyState(VK_CONTROL)&0x8000) && !(GetKeyState(VK_MENU)&0x8000) && (message.hwnd==window || IsChild(window,message.hwnd)) && !copyInProgress) { if(terminalVisible || startTerminal(window)) showTerminal(window,!terminalVisible); continue; } const bool panelFocused=message.hwnd==panels[0].list || message.hwnd==panels[1].list; const bool mainInput=panelFocused || message.hwnd==terminalInput; if(message.message==WM_KEYDOWN && mainInput && !terminalVisible && (GetKeyState(VK_CONTROL)&0x8000) && !(GetKeyState(VK_MENU)&0x8000) && !(GetKeyState(VK_LWIN)&0x8000) && !(GetKeyState(VK_RWIN)&0x8000) && message.wParam>='0' && message.wParam<='9' && !copyInProgress && (message.wParam!='8' || (GetKeyState(VK_SHIFT)&0x8000))) { if(!(message.lParam & (1LL<<30))) { hidePicker(); showMenu(false); auto &panel=panels[active]; const auto digit=static_cast(message.wParam-'0'); if(GetKeyState(VK_SHIFT)&0x8000) addFolderShortcut(panel,digit); else showFolderShortcuts(panel,digit); } continue; } const bool filesMessage=message.hwnd==window || IsChild(window,message.hwnd); if(filesMessage && !terminalVisible && !copyInProgress && !commandHistoryWindow && message.message==WM_KEYDOWN && message.wParam=='8' && (GetKeyState(VK_CONTROL)&0x8000) && !(GetKeyState(VK_MENU)&0x8000) && !(GetKeyState(VK_SHIFT)&0x8000) && !(GetKeyState(VK_LWIN)&0x8000) && !(GetKeyState(VK_RWIN)&0x8000)) { if(!(message.lParam & (1LL<<30))) { hidePicker(); showMenu(false); showFolderShortcuts(panels[active],-1,true); } continue; } if(mainInput && !copyInProgress && message.message==WM_KEYDOWN && !(GetKeyState(VK_MENU)&0x8000) && !(GetKeyState(VK_LWIN)&0x8000) && !(GetKeyState(VK_RWIN)&0x8000)) { const bool ctrl=(GetKeyState(VK_CONTROL)&0x8000)!=0; const bool shift=(GetKeyState(VK_SHIFT)&0x8000)!=0; if(ctrl && !shift && (message.wParam=='F' || message.wParam=='C')) { if(!(message.lParam & (1LL<<30))) { if(message.wParam=='F') insertCurrentPanelPath(); else SendMessageW(terminalInput,WM_COPY,0,0); } continue; } if(panelFocused && ((message.wParam=='V' && ctrl && !shift) || (message.wParam==VK_INSERT && shift && !ctrl))) { SetFocus(terminalInput); SendMessageW(terminalInput,WM_PASTE,0,0); continue; } } if(filesMessage && !copyInProgress && handleCommandHistoryKey(window,message)) continue; const bool modified=(GetKeyState(VK_CONTROL)&0x8000) || (GetKeyState(VK_SHIFT)&0x8000) || (GetKeyState(VK_LWIN)&0x8000) || (GetKeyState(VK_RWIN)&0x8000); if(filesMessage && !copyInProgress && handleFilesMenuKey(window,message.message,message.wParam,message.lParam,modified)) continue; 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)) { showMenu(false); if(message.hwnd!=picker) hidePicker(); } if((message.message==WM_CHAR || message.message==WM_SYSCHAR) && IsWindowVisible(menuBar)) { const auto letter=towupper(static_cast(message.wParam)); if(letter>=L'A' && letter<=L'Z') { if(!IsWindowVisible(picker)) { const auto category=menuAccessKeys.find(static_cast(letter)); if(category!=menuAccessKeys.npos) openMenu(window,static_cast(category)); } else if(commandPicker) { for(size_t row=0;row=static_cast(length)) continue; std::wstring label(static_cast(length)+1,L'\0'); SendMessageW(picker,LB_GETTEXT,row,reinterpret_cast(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;} if(message.wParam==VK_LEFT || message.wParam==VK_RIGHT) { menuIndex=(menuIndex+(message.wParam==VK_LEFT?4:1))%5; showMenu(true); continue; } if(message.wParam==VK_RETURN || message.wParam==VK_DOWN) { SendMessageW(window,WM_COMMAND,MAKEWPARAM(130+menuIndex,BN_CLICKED),0); continue; } } if (message.message == WM_SYSKEYDOWN && message.wParam == VK_F10 && !(GetKeyState(VK_MENU) & 0x8000) && !(GetKeyState(VK_SHIFT) & 0x8000)) { SendMessageW(window, WM_CLOSE, 0, 0); continue; } if (message.message == WM_SYSKEYDOWN && (message.wParam == VK_F1 || message.wParam == VK_F2)) { chooseDrive(static_cast(message.wParam - VK_F1)); continue; } TranslateMessage(&message); DispatchMessageW(&message); } for(auto font:fileStyleFonts) if(font) DeleteObject(font); DeleteObject(mono); DeleteObject(blue); DeleteObject(teal); DeleteObject(terminalBackground); if(SUCCEEDED(com)) CoUninitialize(); return 0; }