4474 lines
184 KiB
C++
4474 lines
184 KiB
C++
#include <windows.h>
|
|
#include <commctrl.h>
|
|
#include <windowsx.h>
|
|
#include <shellapi.h>
|
|
#include <shlobj.h>
|
|
#include <commdlg.h>
|
|
#include <algorithm>
|
|
#include <array>
|
|
#include <deque>
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
#include <future>
|
|
#include <thread>
|
|
#include <iomanip>
|
|
#include <map>
|
|
#include <set>
|
|
#include <sstream>
|
|
#include <string>
|
|
#include <vector>
|
|
#include <objbase.h>
|
|
#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<FileInfo> statFile(FilesConnection &host,const fs::path &path)
|
|
{
|
|
const auto reply=host.call(fileRequest("stat",path));
|
|
const auto error=reply.at("error").get<DWORD>();
|
|
if(error==ERROR_FILE_NOT_FOUND || error==ERROR_PATH_NOT_FOUND) return {};
|
|
checkFileReply(reply);
|
|
return fileInfo(reply);
|
|
}
|
|
using FileVisitor=std::function<bool(const fs::path &,const FileInfo &)>;
|
|
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<bool>()) 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<wchar_t*>(&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<double>(bytes);
|
|
while(value>=1024 && unit+1<std::size(units))
|
|
{
|
|
value/=1024;
|
|
++unit;
|
|
}
|
|
if(!unit) return std::to_wstring(bytes)+L" B";
|
|
std::wostringstream label;
|
|
label<<std::fixed<<std::setprecision(2)<<value<<L' '<<units[unit];
|
|
return label.str();
|
|
}
|
|
constexpr wchar_t folderPipePrefix[]=L"\\\\.\\pipe\\KSwitcher.Files.Folder.";
|
|
struct Handle
|
|
{
|
|
HANDLE value;
|
|
~Handle(){if(value && value!=INVALID_HANDLE_VALUE) CloseHandle(value);}
|
|
};
|
|
struct FolderEntryMetadata
|
|
{
|
|
DWORD attributes;
|
|
ULONGLONG size;
|
|
};
|
|
|
|
DWORD enableBackupPrivilege()
|
|
{
|
|
Handle token{};
|
|
if(!OpenProcessToken(GetCurrentProcess(),TOKEN_ADJUST_PRIVILEGES|TOKEN_QUERY,&token.value))
|
|
return GetLastError();
|
|
|
|
TOKEN_PRIVILEGES privileges{};
|
|
privileges.PrivilegeCount=1;
|
|
privileges.Privileges[0].Attributes=SE_PRIVILEGE_ENABLED;
|
|
if(!LookupPrivilegeValueW(nullptr,SE_BACKUP_NAME,&privileges.Privileges[0].Luid))
|
|
return GetLastError();
|
|
|
|
AdjustTokenPrivileges(token.value,FALSE,&privileges,0,nullptr,nullptr);
|
|
return GetLastError();
|
|
}
|
|
|
|
DWORD listElevatedFolder(const fs::path &directory, const std::wstring &pipeName)
|
|
{
|
|
if(!IsUserAnAdmin() || !directory.is_absolute() || !pipeName.starts_with(folderPipePrefix))
|
|
return ERROR_ACCESS_DENIED;
|
|
Handle pipe{CreateFileW(pipeName.c_str(),GENERIC_READ|GENERIC_WRITE,0,nullptr,OPEN_EXISTING,
|
|
SECURITY_SQOS_PRESENT|SECURITY_IDENTIFICATION,nullptr)};
|
|
if(pipe.value==INVALID_HANDLE_VALUE) return GetLastError();
|
|
// Keep the connection open until the parent verifies the helper's process ID.
|
|
char ready{};
|
|
DWORD received{};
|
|
if(!ReadFile(pipe.value,&ready,sizeof(ready),&received,nullptr)) return GetLastError();
|
|
if(received!=sizeof(ready) || ready!=1) return ERROR_INVALID_DATA;
|
|
if(const auto error=enableBackupPrivilege();error!=ERROR_SUCCESS)
|
|
return error;
|
|
Handle folder{CreateFileW(directory.c_str(),FILE_LIST_DIRECTORY,
|
|
FILE_SHARE_READ|FILE_SHARE_WRITE|FILE_SHARE_DELETE,nullptr,OPEN_EXISTING,
|
|
FILE_FLAG_BACKUP_SEMANTICS,nullptr)};
|
|
if(folder.value==INVALID_HANDLE_VALUE) return GetLastError();
|
|
|
|
alignas(FILE_ID_BOTH_DIR_INFO) unsigned char buffer[16384];
|
|
while(GetFileInformationByHandleEx(folder.value,FileIdBothDirectoryInfo,buffer,sizeof(buffer)))
|
|
{
|
|
auto entry=reinterpret_cast<const FILE_ID_BOTH_DIR_INFO*>(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<ULONGLONG>(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<DWORD>(row.size()*sizeof(wchar_t)),&written,nullptr))
|
|
return GetLastError();
|
|
}
|
|
if(!entry->NextEntryOffset) break;
|
|
entry=reinterpret_cast<const FILE_ID_BOTH_DIR_INFO*>(
|
|
reinterpret_cast<const unsigned char*>(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<fs::path> &folders,std::vector<fs::path> &files,
|
|
std::map<fs::path,FolderEntryMetadata> &metadata)
|
|
{
|
|
GUID id{};
|
|
wchar_t token[40]{}, executable[32768]{};
|
|
auto length=GetModuleFileNameW(nullptr,executable,static_cast<DWORD>(std::size(executable)));
|
|
if(!length || length>=std::size(executable) || FAILED(CoCreateGuid(&id)) ||
|
|
!StringFromGUID2(id,token,static_cast<int>(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()<deadline)
|
|
{
|
|
if(!connected)
|
|
{
|
|
connected=ConnectNamedPipe(pipe.value,nullptr) || GetLastError()==ERROR_PIPE_CONNECTED;
|
|
if(connected)
|
|
{
|
|
ULONG client{};
|
|
if(!GetNamedPipeClientProcessId(pipe.value,&client) || client!=GetProcessId(process.value))
|
|
return ERROR_ACCESS_DENIED;
|
|
const char ready=1;
|
|
DWORD written{};
|
|
if(!WriteFile(pipe.value,&ready,sizeof(ready),&written,nullptr)) return GetLastError();
|
|
if(written!=sizeof(ready)) return ERROR_WRITE_FAULT;
|
|
}
|
|
}
|
|
if(connected)
|
|
{
|
|
wchar_t row[512]{};
|
|
DWORD bytes{};
|
|
if(ReadFile(pipe.value,row,sizeof(row),&bytes,nullptr))
|
|
{
|
|
FolderEntryMetadata entry{};
|
|
if(bytes<=sizeof(entry) || bytes%sizeof(wchar_t)) return ERROR_INVALID_DATA;
|
|
memcpy(&entry,row,sizeof(entry));
|
|
const std::wstring_view name(row+sizeof(entry)/sizeof(wchar_t),(bytes-sizeof(entry))/sizeof(wchar_t));
|
|
if(name.find(L'\0')!=name.npos) return ERROR_INVALID_DATA;
|
|
const fs::path child(name);
|
|
if(child!=child.filename() || child==L"." || child==L"..") return ERROR_INVALID_DATA;
|
|
const auto path=directory/child;
|
|
(entry.attributes&FILE_ATTRIBUTE_DIRECTORY?folders:files).push_back(path);
|
|
metadata.emplace(path,entry);
|
|
continue;
|
|
}
|
|
}
|
|
if(finished)
|
|
{
|
|
DWORD result{};
|
|
if(!GetExitCodeProcess(process.value,&result)) return GetLastError();
|
|
return !connected && result==ERROR_SUCCESS ? ERROR_PIPE_NOT_CONNECTED : result;
|
|
}
|
|
finished=WaitForSingleObject(process.value,20)==WAIT_OBJECT_0;
|
|
}
|
|
return ERROR_TIMEOUT;
|
|
}
|
|
struct Panel
|
|
{
|
|
HWND path{}, list{}, heading{};
|
|
fs::path directory;
|
|
std::optional<FilesPeer> 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<fs::path,uint64_t> timestamps;
|
|
std::vector<fs::path> rows;
|
|
std::vector<std::wstring> sizes;
|
|
std::vector<bool> dimmed;
|
|
std::vector<bool> marked;
|
|
std::set<fs::path> 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<<GetTickCount64()<<' '<<event<<' '<<a<<' '<<b<<' '<<c<<' '<<d<<' '<<e<<std::endl;
|
|
}
|
|
FilesTerminalView terminalView;
|
|
COORD terminalSize{80,25};
|
|
bool terminalViewRequested{};
|
|
HWND terminalInput{},terminalOutput{},terminalPrompt{};
|
|
constexpr UINT_PTR terminalTimer=2;
|
|
constexpr UINT_PTR directoryWatchTimer=4;
|
|
bool terminalVisible{};
|
|
std::deque<int> terminalCommandPanels;
|
|
std::wstring terminalDirectory;
|
|
int terminalRows{};
|
|
struct CommandEntry { std::wstring date, command; };
|
|
std::vector<CommandEntry> 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<size_t> 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<fs::path> drivePaths;
|
|
int driveHover=-1;
|
|
std::array<LONG,3> driveColumnStarts{};
|
|
HWND drivePeerService{};
|
|
bool drivePeerReplyReceived{};
|
|
std::wstring drivePeerLookupError;
|
|
std::vector<FilesPeer> drivePeers;
|
|
struct DriveQuery
|
|
{
|
|
FilesPeer peer;
|
|
std::future<Json> pending;
|
|
Json drives=Json::array();
|
|
std::wstring error;
|
|
};
|
|
struct DriveChoice { FilesPeer peer; fs::path path; };
|
|
std::vector<DriveQuery> driveQueries;
|
|
std::map<LRESULT,DriveChoice> 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<COLORREF,ColorCount> defaultColors{RGB(0,0,128), RGB(230,255,255), RGB(0,255,255),
|
|
RGB(0,128,128), RGB(0,0,0), RGB(0,128,128), RGB(0,128,128)};
|
|
auto colors=defaultColors;
|
|
constexpr auto schemeKey = L"Software\\KSwitcher\\Files";
|
|
constexpr auto placementPreference = L"WindowPlacement";
|
|
|
|
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<long long>(rect.right)-rect.left<540 ||
|
|
static_cast<long long>(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<int>(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<WPARAM>(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<sizeof(wchar_t)) return {};
|
|
std::wstring path(size/sizeof(wchar_t),L'\0');
|
|
if(RegGetValueW(HKEY_CURRENT_USER,schemeKey,key,RRF_RT_REG_SZ,
|
|
nullptr,path.data(),&size)!=ERROR_SUCCESS) return {};
|
|
path.resize(wcslen(path.c_str()));
|
|
return path;
|
|
}
|
|
|
|
void setMode(int index, bool wide)
|
|
{
|
|
panels[index].wide=wide;
|
|
savePreference(index ? L"RightWide" : L"LeftWide",wide);
|
|
notifyFilesPreferences(GetParent(panels[index].list));
|
|
layout(GetParent(panels[index].list));
|
|
InvalidateRect(panels[index].list,nullptr,TRUE);
|
|
showFileDetails(panels[active]);
|
|
}
|
|
HFONT mono{};
|
|
LOGFONTW displayFont{};
|
|
int fontPoints=110;
|
|
int textHeight=20, cellWidth=9;
|
|
constexpr UINT refreshProgressLayout=WM_APP+1;
|
|
|
|
BOOL CALLBACK updateDialogFont(HWND window,LPARAM parameter)
|
|
{
|
|
const auto old=reinterpret_cast<HFONT>(parameter);
|
|
if(reinterpret_cast<HFONT>(SendMessageW(window,WM_GETFONT,0,0))==old)
|
|
SendMessageW(window,WM_SETFONT,reinterpret_cast<WPARAM>(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<HFONT,4> 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<LONG>(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<fileStyleFonts.size();++i)
|
|
{
|
|
if(fileStyleFonts[i]) DeleteObject(fileStyleFonts[i]);
|
|
auto decorated=font;
|
|
decorated.lfUnderline=(i&1)!=0;
|
|
decorated.lfStrikeOut=(i&2)!=0;
|
|
fileStyleFonts[i]=CreateFontIndirectW(&decorated);
|
|
}
|
|
displayFont=font;
|
|
fontPoints=points;
|
|
terminalView.setFont(displayFont,fontPoints);
|
|
for(auto &panel:panels)
|
|
{
|
|
if(panel.path) SendMessageW(panel.path,WM_SETFONT,reinterpret_cast<WPARAM>(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<WPARAM>(mono),FALSE);
|
|
for(const auto control:{terminalInput,terminalOutput,terminalPrompt})
|
|
if(control) SendMessageW(control,WM_SETFONT,reinterpret_cast<WPARAM>(mono),FALSE);
|
|
if(old)
|
|
{
|
|
EnumThreadWindows(GetCurrentThreadId(),[](HWND window,LPARAM parameter)->BOOL
|
|
{
|
|
EnumChildWindows(window,updateDialogFont,parameter);
|
|
return TRUE;
|
|
},reinterpret_cast<LPARAM>(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<ColorCount; ++i)
|
|
{
|
|
auto name = std::to_wstring(i);
|
|
DWORD value{}, size=sizeof(value);
|
|
if (RegGetValueW(HKEY_CURRENT_USER, schemeKey, name.c_str(), RRF_RT_REG_DWORD,
|
|
nullptr, &value, &size)==ERROR_SUCCESS && value<=0xffffff)
|
|
colors[i]=value;
|
|
}
|
|
refreshColors();
|
|
}
|
|
|
|
bool choosePanelColor(HWND owner,COLORREF &color,COLORREF *custom)
|
|
{
|
|
CHOOSECOLORW dialog{};
|
|
dialog.lStructSize=sizeof(dialog);
|
|
dialog.hwndOwner=owner;
|
|
dialog.rgbResult=color;
|
|
dialog.lpCustColors=custom;
|
|
dialog.Flags=CC_RGBINIT | CC_FULLOPEN;
|
|
if(!ChooseColorW(&dialog)) return false;
|
|
color=dialog.rgbResult;
|
|
return true;
|
|
}
|
|
|
|
void paintText(HDC dc, RECT rect, const wchar_t *text, COLORREF foreground, COLORREF background, bool right=false,HFONT font=nullptr)
|
|
{
|
|
SetTextColor(dc, foreground);
|
|
SetBkColor(dc, background);
|
|
SelectObject(dc, font?font:mono);
|
|
auto alignment=SetTextAlign(dc,right?TA_RIGHT:TA_LEFT);
|
|
ExtTextOutW(dc,right?rect.right-5:rect.left+5,rect.top+3,ETO_OPAQUE|ETO_CLIPPED,&rect,text,lstrlenW(text),nullptr);
|
|
SetTextAlign(dc,alignment);
|
|
}
|
|
|
|
void paintMenuLabel(HDC dc,RECT rect,const std::wstring &text,size_t access,COLORREF foreground,COLORREF background)
|
|
{
|
|
paintText(dc,rect,text.c_str(),foreground,background);
|
|
if(access>=text.size()) return;
|
|
SIZE prefix{};
|
|
GetTextExtentPoint32W(dc,text.c_str(),static_cast<int>(access),&prefix);
|
|
const auto mode=SetBkMode(dc,TRANSPARENT);
|
|
SetTextColor(dc,RGB(255,255,0));
|
|
ExtTextOutW(dc,rect.left+5+prefix.cx,rect.top+3,ETO_CLIPPED,&rect,text.c_str()+access,1,nullptr);
|
|
SetBkMode(dc,mode);
|
|
}
|
|
|
|
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<int>(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<int>(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<int>(SendMessageW(list, LB_GETTOPINDEX, 0, 0));
|
|
int count = static_cast<int>(SendMessageW(list, LB_GETCOUNT, 0, 0));
|
|
RECT rect{};
|
|
GetClientRect(list, &rect);
|
|
int rowHeight = static_cast<int>(SendMessageW(list, LB_GETITEMHEIGHT, 0, 0));
|
|
int visible = std::max(1, static_cast<int>(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<POINT *>(&track), 2);
|
|
track.left = track.right;
|
|
track.right += 5;
|
|
SetDCBrushColor(dc, background);
|
|
FillRect(dc, &track, static_cast<HBRUSH>(GetStockObject(DC_BRUSH)));
|
|
int count = static_cast<int>(SendMessageW(list, LB_GETCOUNT, 0, 0));
|
|
int rowHeight = static_cast<int>(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<int>(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<HBRUSH>(GetStockObject(DC_BRUSH)));
|
|
}
|
|
|
|
void placePanelSearch(HWND window)
|
|
{
|
|
RECT panel{};
|
|
GetWindowRect(panels[active].list,&panel);
|
|
MapWindowPoints(nullptr,window,reinterpret_cast<POINT *>(&panel),2);
|
|
const auto frame=dialogFrameMetrics(GetDpiForWindow(window));
|
|
const int width=std::min(static_cast<int>(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<terminalBottom;
|
|
traceTerminal("layout",terminalRows,terminalTop,terminalBottom,showOutput,terminalVisible);
|
|
int middle = std::clamp(split ? split : width / 2, 120, std::max(120, width - 120));
|
|
auto place=[](HWND control, int x, int y, int width, int height)
|
|
{
|
|
if(control) SetWindowPos(control,nullptr,x,y,width,height,
|
|
SWP_NOZORDER|SWP_NOACTIVATE|SWP_NOCOPYBITS|SWP_NOREDRAW);
|
|
};
|
|
for (int i = 0; i < 2; ++i)
|
|
{
|
|
int x = i ? middle + 2 : 0;
|
|
int end = i ? width : middle - 2;
|
|
place(panels[i].path, x + 5, 4, std::max(0, end - x - 10), textHeight);
|
|
int headingHeight=panels[i].wide?textHeight:0;
|
|
place(panels[i].heading,x+5,textHeight+10,std::max(0,end-x-13),headingHeight);
|
|
ShowWindow(panels[i].heading,!terminalVisible && panels[i].wide?SW_SHOWNA:SW_HIDE);
|
|
place(panels[i].list, x + 5, textHeight+10+headingHeight, std::max(0, end - x - 13),
|
|
std::max(0, panelBottom-textHeight-10-headingHeight));
|
|
}
|
|
place(menuBar,0,0,width,textHeight+4);
|
|
const int menuEdges[]{0,7,15,27,38,46};
|
|
for(int i=0;i<5;++i)
|
|
place(menuItems[i],4+menuEdges[i]*cellWidth,0,(menuEdges[i+1]-menuEdges[i])*cellWidth,textHeight+4);
|
|
const int detailsLeft=active?middle+2:0;
|
|
const int detailsRight=active?width:middle-2;
|
|
place(status,detailsLeft+8,panelBottom+4,std::max(0,detailsRight-detailsLeft-16),textHeight);
|
|
const auto ¤t=panels[active];
|
|
const auto prompt=(current.peer?L"[Remote panel]":current.directory.wstring()+L">");
|
|
SetWindowTextW(terminalPrompt,prompt.c_str());
|
|
SIZE promptExtent{static_cast<LONG>(prompt.size())*cellWidth,0};
|
|
if(const auto dc=GetDC(window))
|
|
{
|
|
const auto previous=SelectObject(dc,mono);
|
|
GetTextExtentPoint32W(dc,prompt.c_str(),static_cast<int>(prompt.size()),&promptExtent);
|
|
SelectObject(dc,previous);
|
|
ReleaseDC(window,dc);
|
|
}
|
|
const int promptWidth=std::min(width/2,static_cast<int>(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<int>(std::size(commands)); ++i)
|
|
{
|
|
const int count=static_cast<int>(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<int>(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<size_t>(row)<panel.rows.size()?row:LB_ERR;
|
|
}
|
|
|
|
std::wstring powerShellPath(const fs::path &path)
|
|
{
|
|
std::wstring quoted=L"'";
|
|
for(const auto character:path.native())
|
|
{
|
|
// PowerShell recognizes typographic apostrophes as quote delimiters too.
|
|
if(character==L'\'' || character==L'\u2018' || character==L'\u2019') quoted+=character;
|
|
quoted+=character;
|
|
}
|
|
return quoted+L"'";
|
|
}
|
|
|
|
void insertCurrentPanelPath()
|
|
{
|
|
const auto &panel=panels[active];
|
|
const auto row=currentPanelRow(panel);
|
|
if(row==LB_ERR) return;
|
|
const auto &path=panel.rows[row].native();
|
|
std::wstring quoted;
|
|
if(path.find(L' ')!=std::wstring::npos)
|
|
{
|
|
quoted=L"\"";
|
|
for(const auto character:path)
|
|
{
|
|
if(character==L'`' || character==L'$' || character==L'"' ||
|
|
character==L'\u201c' || character==L'\u201d') quoted+=L'`';
|
|
quoted+=character;
|
|
}
|
|
quoted+=L'"';
|
|
}
|
|
SetFocus(terminalInput);
|
|
SendMessageW(terminalInput,EM_REPLACESEL,TRUE,
|
|
reinterpret_cast<LPARAM>(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 "<<ks::utf8(message.substr(7))<<std::endl;
|
|
else if(terminalTrace.is_open() && message.starts_with(L"trace:"))
|
|
{
|
|
int phase{},height{},rows{},viewport{},base{},offset{};
|
|
if(swscanf_s(message.c_str()+6,L"%d,%d,%d,%d,%d,%d",
|
|
&phase,&height,&rows,&viewport,&base,&offset)==6 && phase>=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<SHORT>(columns),static_cast<SHORT>(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<int>(&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<WPARAM>(&first),reinterpret_cast<LPARAM>(&last));
|
|
if(first==last && first)
|
|
{
|
|
std::wstring text(GetWindowTextLengthW(input)+1,L'\0');
|
|
GetWindowTextW(input,text.data(),static_cast<int>(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<LPARAM>(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<terminalHistory.size()) recallCommand(terminalHistoryIndex+1);
|
|
return 0;
|
|
}
|
|
return DefSubclassProc(input,message,wp,lp);
|
|
}
|
|
|
|
void showFileDetails(const Panel &panel)
|
|
{
|
|
auto row=SendMessageW(panel.list,LB_GETCURSEL,0,0);
|
|
if(!panel.wide || !showDetails || row<0 || static_cast<size_t>(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<DWORD>(found->second),static_cast<DWORD>(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<FilesPeer> 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<fs::path> folders, files;
|
|
std::map<fs::path,FolderEntryMetadata> metadata;
|
|
std::map<fs::path,uint64_t> 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<UINT>(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<ULONGLONG>(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<fs::path> 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<size_t>(previousRow)<panel.rows.size())
|
|
previousSelection=panel.rows[previousRow];
|
|
for(size_t i=0;i<panel.rows.size();++i)
|
|
if(panel.marked[i]) markedPaths.insert(panel.rows[i]);
|
|
}
|
|
panel.directory = directory;
|
|
panel.peer=std::move(peer);
|
|
if(!sameDirectory || nextWatch.handle!=INVALID_HANDLE_VALUE)
|
|
{
|
|
panel.watch.reset();
|
|
std::swap(panel.watch.handle,nextWatch.handle);
|
|
}
|
|
panel.timestamps=std::move(timestamps);
|
|
panel.rows.clear();
|
|
panel.sizes.clear();
|
|
panel.dimmed.clear();
|
|
panel.marked.clear();
|
|
panel.fileExtensionLength=panel.folderExtensionLength=0;
|
|
SendMessageW(panel.list, LB_RESETCONTENT, 0, 0);
|
|
auto add = [&](const fs::path &path, const std::wstring &label, bool folder)
|
|
{
|
|
if (SendMessageW(panel.list, LB_ADDSTRING, 0, reinterpret_cast<LPARAM>(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<LRESULT>(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<size_t>(row)==drivePaths.size())) return;
|
|
const auto remote=remoteDriveRows.find(row);
|
|
if(commandPicker || row<static_cast<LRESULT>(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<int>(row):110+static_cast<int>(row);
|
|
SendMessageW(owner,WM_COMMAND,MAKEWPARAM(command,BN_CLICKED),0);
|
|
}
|
|
return;
|
|
}
|
|
if(row>=0 && static_cast<size_t>(row)<drivePaths.size())
|
|
openOn(panels[pickerPanel],drivePaths[row],false,{});
|
|
else if(remote!=remoteDriveRows.end())
|
|
openOn(panels[pickerPanel],remote->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<wchar_t>(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)<drivePaths.size()) row=LOWORD(hit);
|
|
}
|
|
if(driveHover!=row)
|
|
{
|
|
driveHover=row;
|
|
InvalidateRect(window,nullptr,FALSE);
|
|
}
|
|
}
|
|
if(msg==WM_KEYDOWN && wp==VK_ESCAPE)
|
|
{
|
|
hidePicker();
|
|
return 0;
|
|
}
|
|
if(msg==WM_KEYDOWN && wp==VK_RETURN){selectDrive();return 0;}
|
|
if(msg==WM_KEYDOWN && (wp==VK_LEFT || wp==VK_RIGHT) && IsWindowVisible(menuBar))
|
|
{
|
|
int next=(menuIndex+(wp==VK_LEFT?4:1))%5;
|
|
SendMessageW(GetParent(window),WM_COMMAND,MAKEWPARAM(130+next,BN_CLICKED),0);
|
|
return 0;
|
|
}
|
|
return scrollList(window,msg,wp,lp);
|
|
}
|
|
|
|
void queryDrivePeers()
|
|
{
|
|
drivePeers.clear();
|
|
drivePeerLookupError.clear();
|
|
drivePeerReplyReceived=false;
|
|
drivePeerService=FindWindowW(L"KSwitcher.Main",L"KSwitcher");
|
|
if(drivePeerService)
|
|
{
|
|
COPYDATASTRUCT request{FilesLanPeers,0,nullptr};
|
|
DWORD_PTR result{};
|
|
SetLastError(ERROR_SUCCESS);
|
|
const auto delivered=SendMessageTimeoutW(drivePeerService,WM_COPYDATA,
|
|
reinterpret_cast<WPARAM>(GetParent(picker)),reinterpret_cast<LPARAM>(&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<Json()> 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<std::wstring,3> 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<std::size(types)?type:0],
|
|
total?formatSize(total):L"—",total?formatSize(free):L"—"};
|
|
}
|
|
|
|
void layoutDrivePicker(int selected,bool focus)
|
|
{
|
|
HWND owner=GetParent(picker);
|
|
RECT panel{};
|
|
GetWindowRect(panels[pickerPanel].list,&panel);
|
|
MapWindowPoints(nullptr,owner,reinterpret_cast<POINT*>(&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<int>(panel.bottom-panel.top)-2*vertical)/textHeight),
|
|
static_cast<int>(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<DriveChoice> previous;
|
|
if(auto row=remoteDriveRows.find(selection);row!=remoteDriveRows.end()) previous=row->second;
|
|
if(selection>static_cast<LRESULT>(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<LRESULT>(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<int>(label.size()+2)*cellWidth);
|
|
return SendMessageW(picker,LB_ADDSTRING,0,reinterpret_cast<LPARAM>(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'A' || 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<int>(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<std::array<std::wstring,3>> driveColumns;
|
|
std::array<LONG,3> columnWidths{};
|
|
SIZE space{cellWidth,0};
|
|
if(dc) GetTextExtentPoint32W(dc,L" ",1,&space);
|
|
for(int letter=0;letter<26;++letter)
|
|
{
|
|
if(!(disks & (1UL<<letter))) continue;
|
|
wchar_t drive[]{static_cast<wchar_t>(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<columnWidths.size();++i)
|
|
{
|
|
const auto &value=driveColumns.back()[i];
|
|
SIZE extent{static_cast<LONG>(value.size())*cellWidth,0};
|
|
if(dc) GetTextExtentPoint32W(dc,value.c_str(),static_cast<int>(value.size()),&extent);
|
|
columnWidths[i]=std::max(columnWidths[i],extent.cx);
|
|
}
|
|
if(fs::path(drive)==panels[index].directory.root_path()) selected=static_cast<int>(drivePaths.size());
|
|
drivePaths.emplace_back(drive);
|
|
}
|
|
LONG edge=0;
|
|
for(size_t i=0;i<driveColumnStarts.size();++i)
|
|
{
|
|
driveColumnStarts[i]=edge+space.cx;
|
|
edge=driveColumnStarts[i]+columnWidths[i]+space.cx+1;
|
|
}
|
|
if(!driveColumns.empty()) textWidth=edge-1;
|
|
for(const auto &values:driveColumns)
|
|
{
|
|
const auto label=values[0]+L"\t"+values[1]+L"\t"+values[2];
|
|
SendMessageW(picker,LB_ADDSTRING,0,reinterpret_cast<LPARAM>(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<INT_PTR>(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<fs::path> 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<int>(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<int>(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<LPARAM>(&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<WPARAM>(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<POINT*>(&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<POINT*>(&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<int> 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<int> 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<LONG>(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<int>(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<HBRUSH>(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<int> 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<HBRUSH>(GetStockObject(LTGRAY_BRUSH));
|
|
if(message==WM_CTLCOLORDLG) return reinterpret_cast<INT_PTR>(brush);
|
|
if(message==WM_CTLCOLORSTATIC || message==WM_CTLCOLOREDIT || message==WM_CTLCOLORBTN)
|
|
{
|
|
const auto dc=reinterpret_cast<HDC>(wp);
|
|
const bool edit=message==WM_CTLCOLOREDIT ||
|
|
(message==WM_CTLCOLORSTATIC && isDialogEdit(reinterpret_cast<HWND>(lp)));
|
|
SetTextColor(dc,edit?colors[Text]:foreground);
|
|
SetBkColor(dc,edit?colors[Dialog]:surface);
|
|
return reinterpret_cast<INT_PTR>(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<HBRUSH>(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<const DRAWITEMSTRUCT*>(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<LPARAM>(label));
|
|
auto arrow=bounds;
|
|
arrow.left=std::max(bounds.left,bounds.right-2*static_cast<LONG>(cellWidth));
|
|
bounds.right=arrow.left;
|
|
SetDCBrushColor(dc,colors[Dialog]);
|
|
FillRect(dc,&bounds,static_cast<HBRUSH>(GetStockObject(DC_BRUSH)));
|
|
FillRect(dc,&arrow,static_cast<HBRUSH>(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<const DRAWITEMSTRUCT*>(lp);
|
|
const auto request=reinterpret_cast<const CopyRequest*>(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<const DRAWITEMSTRUCT*>(lp);
|
|
wchar_t label[64]{};
|
|
if(item.itemID!=UINT(-1))
|
|
SendMessageW(item.hwndItem,CB_GETLBTEXT,item.itemID,reinterpret_cast<LPARAM>(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<HDC>(wp);
|
|
SetTextColor(dc,colors[Text]);
|
|
SetBkColor(dc,colors[Dialog]);
|
|
return reinterpret_cast<INT_PTR>(teal);
|
|
}
|
|
const auto prompt=reinterpret_cast<const CopyRequest*>(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<CopyRequest*>(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<LPARAM>(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<WPARAM>(mono),FALSE);
|
|
SendDlgItemMessageW(dialog,summary,WM_SETFONT,reinterpret_cast<WPARAM>(mono),FALSE);
|
|
SendDlgItemMessageW(dialog,conflicts,WM_SETFONT,reinterpret_cast<WPARAM>(mono),FALSE);
|
|
for(const auto control:{213,214})
|
|
SendDlgItemMessageW(dialog,control,WM_SETFONT,reinterpret_cast<WPARAM>(mono),FALSE);
|
|
SendDlgItemMessageW(dialog,213,CB_SETITEMHEIGHT,0,textHeight);
|
|
SendDlgItemMessageW(dialog,213,CB_SETITEMHEIGHT,static_cast<WPARAM>(-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<POINT*>(&combo),2);
|
|
MapWindowPoints(nullptr,dialog,reinterpret_cast<POINT*>(&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<CopyRequest*>(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<int>(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<int>(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<ConflictMode>(
|
|
request.move && mode==static_cast<int>(ConflictMode::append)?static_cast<int>(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<const OwnedWindowMove*>(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<LONG>(bounds.right*percent/100);
|
|
FillRect(dc,&bounds,static_cast<HBRUSH>(GetStockObject(WHITE_BRUSH)));
|
|
FillRect(dc,&filled,static_cast<HBRUSH>(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<refreshIntervalMs) return;
|
|
progress.lastPaint=now;
|
|
if(!progress.from.empty())
|
|
{
|
|
SetDlgItemTextW(progress.window,220,(L"From: "+progress.from.wstring()).c_str());
|
|
SetDlgItemTextW(progress.window,221,(L"To: "+progress.to.wstring()).c_str());
|
|
for(const auto id:{220,221}) InvalidateRect(GetDlgItem(progress.window,id),nullptr,FALSE);
|
|
}
|
|
const auto percent=[](uint64_t done,uint64_t total)
|
|
{
|
|
return total?static_cast<int>(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<const MSG*>(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<const wchar_t*>(lp));
|
|
SendDlgItemMessageW(window,235,WM_SETFONT,reinterpret_cast<WPARAM>(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<LPARAM>(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<const DRAWITEMSTRUCT*>(lp);
|
|
std::wstring path(GetWindowTextLengthW(item.hwndItem)+1,L'\0');
|
|
GetWindowTextW(item.hwndItem,path.data(),static_cast<int>(path.size()));
|
|
auto bounds=item.rcItem;
|
|
FillRect(item.hDC,&bounds,static_cast<HBRUSH>(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<const CopyProgress*>(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<WPARAM>(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)<bounds.top+18)
|
|
{
|
|
SetWindowLongPtrW(window,DWLP_MSGRESULT,HTCAPTION);
|
|
return TRUE;
|
|
}
|
|
}
|
|
if(message!=WM_CLOSE && (message!=WM_COMMAND || LOWORD(wp)!=IDCANCEL)) return FALSE;
|
|
auto &progress=*reinterpret_cast<CopyProgress*>(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<DWORD>(file.modified),static_cast<DWORD>(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<const DRAWITEMSTRUCT*>(lp);
|
|
const auto warning=reinterpret_cast<const WarningPrompt*>(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<WarningPrompt*>(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<WPARAM>(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<std::size(buttons);++i)
|
|
SetWindowSubclass(GetDlgItem(window,buttons[i]),warningButtonKeys,1,i);
|
|
SetWindowSubclass(GetDlgItem(window,232),warningButtonKeys,1,std::size(buttons));
|
|
SetFocus(GetDlgItem(window,IDNO));
|
|
return FALSE;
|
|
}
|
|
if(message==WM_CLOSE)
|
|
{
|
|
finishFilesDialog(window,IDCANCEL);
|
|
return TRUE;
|
|
}
|
|
if(message==WM_COMMAND && LOWORD(wp)==232 && HIWORD(wp)==BN_CLICKED)
|
|
{
|
|
auto &warning=*reinterpret_cast<WarningPrompt*>(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<int>(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<std::pair<fs::path,FileInfo>>;
|
|
|
|
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<INT_PTR>(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<LPARAM>(&warning));
|
|
if(warning.remember && (answer==IDYES || answer==IDNO))
|
|
progress.conflictChoice=static_cast<int>(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<source.rows.size();++i)
|
|
if(source.marked[i]) request.sources.push_back(source.rows[i]);
|
|
if(request.sources.empty())
|
|
{
|
|
const auto row=SendMessageW(source.list,LB_GETCURSEL,0,0);
|
|
if(row<0 || static_cast<size_t>(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<LPARAM>(&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<LPARAM>(&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<fs::path> results;
|
|
std::wstring failure;
|
|
try
|
|
{
|
|
FilesConnection sourceHost(request.sourcePeer);
|
|
std::unique_ptr<FilesConnection> otherHost;
|
|
if(!sameHost) otherHost=std::make_unique<FilesConnection>(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<const DeleteConfirmation*>(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<WPARAM>(mono),FALSE);
|
|
const auto entries=dialogControlBounds(window,241);
|
|
const auto question=dialogControlBounds(window,240);
|
|
const std::vector<int> buttons=data->readOnlyPrompt?
|
|
std::vector<int>{IDYES,244,IDNO,249,IDCANCEL}:data->folderPrompt?
|
|
std::vector<int>{IDYES,244,IDNO,IDCANCEL}:std::vector<int>{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<LONG>(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<WPARAM>(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<bool*>(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<int>(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<fs::path> paths;
|
|
for(size_t i=0;i<panel.rows.size();++i)
|
|
if(panel.marked[i] && panel.sizes[i]!=L"Up") paths.push_back(panel.rows[i]);
|
|
if(paths.empty())
|
|
{
|
|
const auto row=SendMessageW(panel.list,LB_GETCURSEL,0,0);
|
|
if(row<0 || static_cast<size_t>(row)>=panel.rows.size() || panel.sizes[row]==L"Up") return;
|
|
paths.push_back(panel.rows[row]);
|
|
}
|
|
const auto shown=std::min<size_t>(paths.size(),10);
|
|
std::wstring entries;
|
|
for(size_t i=0;i<shown;++i)
|
|
{
|
|
if(i) entries+=L"\r\n";
|
|
entries+=paths[i].filename().wstring();
|
|
}
|
|
if(shown<paths.size()) entries+=L"\r\n…and "+std::to_wstring(paths.size()-shown)+L" more";
|
|
const auto instance=GetModuleHandleW(nullptr);
|
|
warningBrush=CreateSolidBrush(RGB(128,0,0));
|
|
const wchar_t *message=permanent?L"Delete the selected items permanently?":
|
|
L"Move the selected items to the Recycle Bin?";
|
|
if(paths.size()==1)
|
|
{
|
|
DWORD attributes=INVALID_FILE_ATTRIBUTES;
|
|
try
|
|
{
|
|
FilesConnection host(panel.peer?&*panel.peer:nullptr);
|
|
if(const auto info=statFile(host,paths.front())) attributes=info->attributes;
|
|
}
|
|
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<int>(shown+(shown<paths.size())),
|
|
permanent,false};
|
|
if(runFilesDialog(instance,MAKEINTRESOURCEW(106),owner,deleteConfirmationDialog,
|
|
reinterpret_cast<LPARAM>(&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<LPARAM>(&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<std::vector<fs::path>> groups;
|
|
std::wstring failure;
|
|
try
|
|
{
|
|
FilesConnection host(panel.peer?&*panel.peer:nullptr);
|
|
const auto collect=[&](auto &&self,const fs::path &path,std::vector<fs::path> &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<int>(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<LPARAM>(&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<FileInfo>{};
|
|
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<INT_PTR>(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<LPARAM>(&warning));
|
|
if(answer==244 || answer==249) readOnlyChoice=static_cast<int>(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<int>(&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<LPARAM>(&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<size_t>(row)>=panel.marked.size()) return 0;
|
|
const auto last=static_cast<LRESULT>(panel.marked.size()-1);
|
|
auto target=row;
|
|
switch(wp)
|
|
{
|
|
case VK_UP: target=std::max<LRESULT>(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<int>(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<int>(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<LONG>(cellWidth));
|
|
line.right=line.left+1;
|
|
SetDCBrushColor(dc,colors[Frame]);
|
|
FillRect(dc,&line,static_cast<HBRUSH>(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<int>(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<int>(std::clamp(preference(L"SizeWidth",defaultSizeWidth),6UL,16UL));
|
|
alignExtensions=preference(L"AlignExtensions",defaultAlignExtensions)!=0;
|
|
showDetails=preference(L"ShowDetails",defaultShowDetails)!=0;
|
|
alignFolders=preference(L"AlignFolders",defaultAlignFolders)!=0;
|
|
statusFields=preference(L"StatusFields",defaultStatusFields)&15;
|
|
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<std::wstring> 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<wchar_t> used;
|
|
commandAccessPositions.assign(rows.size(),std::wstring::npos);
|
|
const auto reserve=[&](size_t row,wchar_t key)
|
|
{
|
|
const auto &text=rows[row];
|
|
for(size_t pos=0;pos<text.size() && text[pos]!=L'\t';++pos)
|
|
if(towupper(text[pos])==key && used.insert(key).second)
|
|
{
|
|
commandAccessPositions[row]=pos;
|
|
return true;
|
|
}
|
|
return false;
|
|
};
|
|
if(index==0 || index==4) {reserve(0,L'B');reserve(1,L'W');reserve(2,L'C');}
|
|
if(index==1) {reserve(0,L'C');reserve(2,L'X');}
|
|
if(index==2) {reserve(1,L'V');reserve(2,L'E');}
|
|
for(size_t i=0;i<rows.size();++i)
|
|
{
|
|
const auto &text=rows[i];
|
|
if(commandAccessPositions[i]==std::wstring::npos)
|
|
for(const auto ch:text)
|
|
{
|
|
if(ch==L'\t') break;
|
|
const auto key=towupper(ch);
|
|
if(key>=L'A' && key<=L'Z' && reserve(i,static_cast<wchar_t>(key))) break;
|
|
}
|
|
SendMessageW(picker,LB_ADDSTRING,0,reinterpret_cast<LPARAM>(text.c_str()));
|
|
}
|
|
RECT anchor{}, client{};
|
|
GetWindowRect(menuItems[index],&anchor);
|
|
MapWindowPoints(nullptr,window,reinterpret_cast<POINT*>(&anchor),2);
|
|
GetClientRect(window,&client);
|
|
const int inset=dialogFrameMetrics(GetDpiForWindow(window)).content();
|
|
const int width=std::max(1,std::min(static_cast<int>(client.right)-2*inset,26*cellWidth+16));
|
|
const int x=std::clamp(static_cast<int>(anchor.left)+inset,inset,
|
|
std::max(inset,static_cast<int>(client.right)-width-inset));
|
|
const int y=anchor.bottom+5;
|
|
const int height=static_cast<int>(rows.size())*static_cast<int>(SendMessageW(picker,LB_GETITEMHEIGHT,0,0));
|
|
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<wchar_t>(key));
|
|
if(category!=menuAccessKeys.npos)
|
|
{
|
|
if(!repeated) openMenu(window,static_cast<int>(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<int>(panelSearchPrefix.size());
|
|
for(size_t row=0;length && row<panel.rows.size();++row)
|
|
{
|
|
if(panel.sizes[row]==L"Up") continue;
|
|
const auto name=panel.rows[row].filename().native();
|
|
if(name.size()<panelSearchPrefix.size() ||
|
|
CompareStringOrdinal(name.data(),length,panelSearchPrefix.data(),length,TRUE)!=CSTR_EQUAL)
|
|
continue;
|
|
SendMessageW(panel.list,LB_SETCURSEL,row,0);
|
|
InvalidateRect(panel.list,nullptr,FALSE);
|
|
showFileDetails(panel);
|
|
if(!IsWindowVisible(panelSearchPopup))
|
|
{
|
|
placePanelSearch(window);
|
|
ShowWindow(panelSearchPopup,SW_SHOWNA);
|
|
}
|
|
break;
|
|
}
|
|
if(IsWindowVisible(panelSearchPopup)) InvalidateRect(panelSearchPopup,nullptr,FALSE);
|
|
else panelSearchPrefix.clear();
|
|
}
|
|
|
|
bool handlePanelSearchKey(HWND window,MSG &message)
|
|
{
|
|
const bool searching=IsWindowVisible(panelSearchPopup);
|
|
if(message.message==WM_LBUTTONDOWN || message.message==WM_RBUTTONDOWN ||
|
|
message.message==WM_NCLBUTTONDOWN || message.message==WM_MOUSEWHEEL)
|
|
{
|
|
if(searching && message.hwnd!=panelSearchPopup) closePanelSearch();
|
|
return false;
|
|
}
|
|
const bool input=message.hwnd==terminalInput || message.hwnd==panels[0].list ||
|
|
message.hwnd==panels[1].list;
|
|
if(!input || terminalVisible || IsWindowVisible(menuBar) || IsWindowVisible(picker)) return false;
|
|
|
|
const bool modified=(GetKeyState(VK_CONTROL)&0x8000) || (GetKeyState(VK_LWIN)&0x8000) ||
|
|
(GetKeyState(VK_RWIN)&0x8000);
|
|
const bool alt=(message.lParam & (1LL<<29))!=0;
|
|
if((message.message==WM_SYSCHAR && alt) || (searching && message.message==WM_CHAR))
|
|
{
|
|
if(!modified && (message.wParam==L'\b' ||
|
|
(message.wParam>=32 && message.wParam!=127 && (!alt || message.wParam!=L' '))))
|
|
{
|
|
searchPanelByPrefix(window,static_cast<wchar_t>(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<UINT>(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<UINT>(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<HWND>(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<const COPYDATASTRUCT*>(lp);
|
|
if(!drivePeerService || reinterpret_cast<HWND>(wp)!=drivePeerService || !reply ||
|
|
reply->dwData!=FilesLanPeersReply || reply->cbData>FilesLanPeersLimit ||
|
|
(!reply->cbData || !reply->lpData)) return FALSE;
|
|
try
|
|
{
|
|
const auto *begin=static_cast<const char*>(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<HMENU>(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<HMENU>(130+static_cast<INT_PTR>(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<HMENU>(121),instance,nullptr);
|
|
pickerFrame=CreateWindowW(L"STATIC",L"",WS_CHILD|WS_CLIPSIBLINGS|SS_OWNERDRAW,
|
|
0,0,0,0,window,reinterpret_cast<HMENU>(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<HMENU>(144), instance, nullptr);
|
|
panelSearchPopup=CreateWindowW(L"STATIC",L"Search",WS_CHILD|WS_CLIPSIBLINGS|SS_OWNERDRAW|SS_NOTIFY,
|
|
0,0,0,0,window,reinterpret_cast<HMENU>(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<HMENU>(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<WPARAM>(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<WPARAM>(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<int>(std::size(commands)); ++i)
|
|
commands[i] = CreateWindowW(L"BUTTON", labels[i], WS_CHILD | WS_VISIBLE | BS_OWNERDRAW,
|
|
0, 0, 0, 0, window, reinterpret_cast<HMENU>(110 + static_cast<INT_PTR>(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<HMENU>(142 + static_cast<INT_PTR>(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<HMENU>(102 + static_cast<INT_PTR>(i)), instance, nullptr);
|
|
panel.heading=CreateWindowW(L"STATIC",L"",WS_CHILD|WS_CLIPSIBLINGS|SS_OWNERDRAW,
|
|
0,0,0,0,window,reinterpret_cast<HMENU>(140+static_cast<INT_PTR>(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<WPARAM>(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<const RECT*>(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<LPARAM>(&move));
|
|
}
|
|
}
|
|
return 0;
|
|
case WM_SIZE:
|
|
hidePicker();
|
|
layout(window);
|
|
layoutOpenCommandHistory();
|
|
return 0;
|
|
case WM_GETMINMAXINFO:
|
|
reinterpret_cast<MINMAXINFO *>(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<POINT*>(&list),2);
|
|
if(point.x>=list.left && point.x<list.right && point.y>=0 && point.y<list.top)
|
|
{
|
|
selectPanelEdge(panel,true);
|
|
return 0;
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case WM_MOUSEMOVE:
|
|
if (dragging) { split = std::max(1, GET_X_LPARAM(lp)); layout(window); }
|
|
return 0;
|
|
case WM_LBUTTONUP:
|
|
if (dragging) ReleaseCapture();
|
|
return 0;
|
|
case WM_CAPTURECHANGED:
|
|
dragging = false;
|
|
return 0;
|
|
case WM_ERASEBKGND:
|
|
return 1;
|
|
case WM_PAINT:
|
|
{
|
|
PAINTSTRUCT paint{};
|
|
auto dc=BeginPaint(window,&paint);
|
|
FillRect(dc,&paint.rcPaint,blue);
|
|
auto border=CreateSolidBrush(colors[Frame]);
|
|
for(const auto &panel:panels)
|
|
{
|
|
if(terminalVisible) break;
|
|
RECT box{};
|
|
GetWindowRect(panel.list,&box);
|
|
MapWindowPoints(nullptr,window,reinterpret_cast<POINT*>(&box),2);
|
|
const LONG columnX=std::max(box.left,box.right-(sizeWidth+2)*static_cast<LONG>(cellWidth));
|
|
box.right+=5;
|
|
InflateRect(&box,2,2);
|
|
box.left-=2;
|
|
RECT pathBounds{};
|
|
GetWindowRect(panel.path,&pathBounds);
|
|
MapWindowPoints(nullptr,window,reinterpret_cast<POINT*>(&pathBounds),2);
|
|
box.top=pathBounds.top+(pathBounds.bottom-pathBounds.top)/2-2;
|
|
RECT details{};
|
|
GetWindowRect(status,&details);
|
|
MapWindowPoints(nullptr,window,reinterpret_cast<POINT*>(&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<HDC>(wp);
|
|
if(reinterpret_cast<HWND>(lp)==terminalInput || reinterpret_cast<HWND>(lp)==terminalOutput ||
|
|
reinterpret_cast<HWND>(lp)==terminalPrompt)
|
|
{
|
|
SetTextColor(dc,RGB(232,232,232));
|
|
SetBkColor(dc,RGB(24,24,24));
|
|
return reinterpret_cast<LRESULT>(terminalBackground);
|
|
}
|
|
SetTextColor(dc,colors[Frame]);
|
|
SetBkColor(dc,colors[PanelBlue]);
|
|
return reinterpret_cast<LRESULT>(blue);
|
|
}
|
|
case WM_CTLCOLORLISTBOX:
|
|
return reinterpret_cast<LRESULT>(reinterpret_cast<HWND>(lp)==picker?teal:blue);
|
|
case WM_DRAWITEM:
|
|
{
|
|
const auto &item=*reinterpret_cast<const DRAWITEMSTRUCT *>(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<size_t>(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<HBRUSH>(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<POINT*>(&list),2);
|
|
const auto x=std::max(list.left,list.right-(sizeWidth+2)*static_cast<LONG>(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<int>(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<int>(item.CtlID)-142==active;
|
|
FillRect(item.hDC,&bounds,highlighted?teal:blue);
|
|
const auto padding=std::min(static_cast<LONG>(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<LONG>(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<HBRUSH>(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<POINT*>(&list),2);
|
|
for(size_t i=1;i<driveColumnStarts.size();++i)
|
|
{
|
|
const LONG x=list.left+driveColumnStarts[i]-driveColumnStarts[0]-1;
|
|
if(x<list.right)
|
|
{
|
|
const RECT line{x,box.top,x+1,list.top};
|
|
FillRect(item.hDC,&line,border);
|
|
}
|
|
}
|
|
}
|
|
paintDialogFrameCaptions(item.hDC,item.rcItem,box,L"Change drive",teal,colors[Text],mono);
|
|
}
|
|
DeleteObject(border);
|
|
return TRUE;
|
|
}
|
|
std::wstring text;
|
|
if(item.CtlType==ODT_LISTBOX && item.itemID!=UINT(-1))
|
|
{
|
|
auto length=SendMessageW(item.hwndItem,LB_GETTEXTLEN,item.itemID,0);
|
|
if(length>=0)
|
|
{
|
|
text.resize(length+1);
|
|
SendMessageW(item.hwndItem,LB_GETTEXT,item.itemID,reinterpret_cast<LPARAM>(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<HBRUSH>(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<HBRUSH>(GetStockObject(DC_BRUSH)));
|
|
return TRUE;
|
|
}
|
|
if(item.CtlID>=130 && item.CtlID<=134 && static_cast<int>(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<UINT>(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<panel.rows.size() && panel.sizes[item.itemID]!=L"Up")
|
|
{
|
|
const bool marked=panel.marked[item.itemID];
|
|
const bool transferred=transferHighlightPhase%2==0 && panel.transferResults.contains(panel.rows[item.itemID]);
|
|
const bool cursor=SendMessageW(panel.list,LB_GETCURSEL,0,0)==item.itemID;
|
|
const auto state=marked?FileRowState::Marked:!cursor?FileRowState::Normal:
|
|
activePanelRow?FileRowState::Focused:FileRowState::Unfocused;
|
|
const auto resolved=resolveFileStyle(panel.rows[item.itemID].extension().native(),
|
|
panel.sizes[item.itemID]!=L"Folder",panel.dimmed[item.itemID],
|
|
background==PanelCursor || marked || transferred,foreground,rowBackground,fileStyles,state,transferred);
|
|
foreground=resolved.foreground;
|
|
rowBackground=resolved.background;
|
|
rowFont=fileStyleFonts[resolved.decorations];
|
|
}
|
|
}
|
|
auto shortcut=text.find(L'\t');
|
|
if(item.hwndItem==picker && commandPicker && item.itemID<commandAccessPositions.size())
|
|
{
|
|
auto labelRect=item.rcItem, keyRect=item.rcItem;
|
|
SelectObject(item.hDC,mono);
|
|
SIZE extent{};
|
|
if(shortcut!=text.npos)
|
|
{
|
|
const auto key=text.c_str()+shortcut+1;
|
|
GetTextExtentPoint32W(item.hDC,key,lstrlenW(key),&extent);
|
|
keyRect.left=std::max(keyRect.left,keyRect.right-extent.cx-16);
|
|
labelRect.right=keyRect.left;
|
|
paintText(item.hDC,keyRect,key,colors[Text],colors[background],true);
|
|
}
|
|
paintMenuLabel(item.hDC,labelRect,text.substr(0,shortcut),commandAccessPositions[item.itemID],
|
|
colors[Text],colors[background]);
|
|
}
|
|
else if(item.hwndItem==picker && !commandPicker && item.itemID<drivePaths.size())
|
|
{
|
|
SelectObject(item.hDC,mono);
|
|
SetTextColor(item.hDC,foreground);
|
|
const auto normal=colors[Dialog];
|
|
const auto hover=RGB(3*GetRValue(normal)/4,3*GetGValue(normal)/4,
|
|
3*GetBValue(normal)/4);
|
|
SetBkColor(item.hDC,background==Dialog && static_cast<int>(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<HBRUSH>(GetStockObject(DC_BRUSH));
|
|
size_t start=0;
|
|
for(size_t i=0;i<driveColumnStarts.size();++i)
|
|
{
|
|
const LONG x=item.rcItem.left+driveColumnStarts[i];
|
|
if(i)
|
|
{
|
|
const LONG divider=x-driveColumnStarts[0]-1;
|
|
if(divider<item.rcItem.right)
|
|
{
|
|
const RECT line{divider,item.rcItem.top,divider+1,item.rcItem.bottom};
|
|
FillRect(item.hDC,&line,brush);
|
|
}
|
|
}
|
|
const auto end=text.find(L'\t',start);
|
|
const auto length=end==text.npos?lstrlenW(text.c_str()+start):end-start;
|
|
SetTextColor(item.hDC,foreground);
|
|
ExtTextOutW(item.hDC,x,item.rcItem.top+3,ETO_CLIPPED,&item.rcItem,
|
|
text.c_str()+start,static_cast<UINT>(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<panel.sizes.size())
|
|
{
|
|
auto name=item.rcItem, size=item.rcItem;
|
|
size.left=std::max(size.left,size.right-(sizeWidth+2)*static_cast<LONG>(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<LONG>(extensionLength)*cellWidth+5;
|
|
end.left=std::max(end.left+static_cast<LONG>(cellWidth),end.right-width);
|
|
end.left=std::min(end.left,end.right);
|
|
name.right=end.left;
|
|
paintText(item.hDC,end,extension.c_str()+1,foreground,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<size_t>(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<HBRUSH>(GetStockObject(DC_BRUSH)));
|
|
}
|
|
}
|
|
if (item.CtlType==ODT_LISTBOX && item.hwndItem!=picker)
|
|
{
|
|
RECT track{};
|
|
GetWindowRect(item.hwndItem,&track);
|
|
MapWindowPoints(nullptr,window,reinterpret_cast<POINT*>(&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<DWORD>((path.size()+1)*sizeof(wchar_t)));
|
|
const auto row=SendMessageW(panels[i].list,LB_GETCURSEL,0,0);
|
|
const auto cursor=row>=0 && static_cast<size_t>(row)<panels[i].rows.size() ?
|
|
panels[i].rows[row].native() : std::wstring{};
|
|
if(!panels[i].peer) RegSetKeyValueW(HKEY_CURRENT_USER,schemeKey,panelCursorKeys[i],REG_SZ,cursor.c_str(),
|
|
static_cast<DWORD>((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<int>(result);
|
|
}
|
|
for(int i=1;i<count;++i)
|
|
if(std::wstring_view(args[i])==L"--terminal-trace" ||
|
|
std::wstring_view(args[i])==L"--terminal-output-trace")
|
|
{
|
|
traceTerminalOutput=std::wstring_view(args[i])==L"--terminal-output-trace";
|
|
wchar_t temp[MAX_PATH]{};
|
|
const auto length=GetTempPathW(MAX_PATH,temp);
|
|
if(length && length<MAX_PATH)
|
|
terminalTrace.open(fs::path(temp)/(L"KSwitcher.Files.terminal-trace-"+
|
|
std::to_wstring(GetCurrentProcessId())+L".log"),std::ios::trunc);
|
|
traceTerminal("start");
|
|
}
|
|
LocalFree(args);
|
|
}
|
|
INITCOMMONCONTROLSEX controls{sizeof(controls), ICC_STANDARD_CLASSES|ICC_PROGRESS_CLASS|ICC_TREEVIEW_CLASSES|ICC_HOTKEY_CLASS};
|
|
InitCommonControlsEx(&controls);
|
|
loadCommandHistory();
|
|
WNDCLASSW type{};
|
|
type.hInstance = instance;
|
|
type.lpfnWndProc = filesWindow;
|
|
type.hIcon = LoadIconW(instance, MAKEINTRESOURCEW(1));
|
|
type.hCursor = LoadCursorW(nullptr, IDC_ARROW);
|
|
|
|
type.lpszClassName = L"KSwitcher.Files";
|
|
if (!RegisterClassW(&type)) return 1;
|
|
auto window = CreateWindowW(type.lpszClassName, L"KSwitcher Files — local prototype",
|
|
WS_OVERLAPPEDWINDOW | WS_CLIPCHILDREN, CW_USEDEFAULT, CW_USEDEFAULT, 900, 540, nullptr, nullptr, instance, nullptr);
|
|
if (!window) return 1;
|
|
if(!restorePlacement(window)) ShowWindow(window, show);
|
|
SetFocus(terminalInput);
|
|
const auto com=CoInitializeEx(nullptr,COINIT_APARTMENTTHREADED);
|
|
MSG message{};
|
|
for(;;)
|
|
{
|
|
if(GetForegroundWindow()==window) restoreCommandFocus();
|
|
if(GetMessageW(&message,nullptr,0,0)<=0) break;
|
|
if(message.hwnd==window && message.message>=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<int>(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<int>(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<wchar_t>(message.wParam));
|
|
if(letter>=L'A' && letter<=L'Z')
|
|
{
|
|
if(!IsWindowVisible(picker))
|
|
{
|
|
const auto category=menuAccessKeys.find(static_cast<wchar_t>(letter));
|
|
if(category!=menuAccessKeys.npos) openMenu(window,static_cast<int>(category));
|
|
}
|
|
else if(commandPicker)
|
|
{
|
|
for(size_t row=0;row<commandAccessPositions.size();++row)
|
|
{
|
|
const auto pos=commandAccessPositions[row];
|
|
if(pos==std::wstring::npos) continue;
|
|
const auto length=SendMessageW(picker,LB_GETTEXTLEN,row,0);
|
|
if(length<=0 || pos>=static_cast<size_t>(length)) continue;
|
|
std::wstring label(static_cast<size_t>(length)+1,L'\0');
|
|
SendMessageW(picker,LB_GETTEXT,row,reinterpret_cast<LPARAM>(label.data()));
|
|
if(towupper(label[pos])==letter)
|
|
{
|
|
SendMessageW(picker,LB_SETCURSEL,row,0);
|
|
selectDrive();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
TranslateMessage(&message);
|
|
DispatchMessageW(&message);
|
|
}
|
|
continue;
|
|
}
|
|
}
|
|
if(message.message==WM_KEYDOWN && IsWindowVisible(menuBar) && !IsWindowVisible(picker))
|
|
{
|
|
if(message.wParam==VK_ESCAPE){showMenu(false);SetFocus(panels[active].list);continue;}
|
|
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<int>(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;
|
|
}
|