/** * Furnace Tracker - multi-system chiptune tracker * Copyright (C) 2021-2025 tildearrow and contributors * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ // this is the code to a new file picker using Dear ImGui. // this replaces ImGuiFileDialog as the built-in file picker. #include "newFilePicker.h" #include "IconsFontAwesome4.h" #include "misc/cpp/imgui_stdlib.h" #include "../ta-log.h" #include #include #include #ifdef _WIN32 #include #include #include "../utfutils.h" #else #include #include #include #include #endif #include "../fileutils.h" #include #include static const char* sizeSuffixes=".KMGTPEZ"; static void _fileThread(void* item) { ((FurnaceFilePicker*)item)->readDirectorySub(); } static void _searchThread(void* item) { ((FurnaceFilePicker*)item)->searchSub("",0); } #ifdef _WIN32 void FurnaceFilePicker::completeStat() { // no need to. } #else void FurnaceFilePicker::completeStat() { // get file information struct stat st; String filePath; for (FileEntry* i: entries) { if (stopReading) { return; } if (path.empty()) { filePath=i->name; } else if (*path.rbegin()=='/') { filePath=path+i->name; } else { filePath=path+'/'+i->name; } if (stat(filePath.c_str(),&st)<0) { // fall back to unknown continue; } // read file information entryLock.lock(); struct tm* retTM=localtime_r(&st.st_mtime,&i->time); if (retTM!=NULL) { i->hasTime=true; } i->size=st.st_size; i->hasSize=true; entryLock.unlock(); } } #endif #ifdef _WIN32 FurnaceFilePicker::FileEntry* FurnaceFilePicker::makeEntry(void* _entry, const char* prefix) { WIN32_FIND_DATAW* entry=(WIN32_FIND_DATAW*)_entry; SYSTEMTIME tempTM; FileEntry* newEntry=new FileEntry; if (prefix!=NULL) { if (prefix[0]=='\\') { newEntry->name=String(&prefix[1])+"\\"+utf16To8(entry->cFileName); } else { newEntry->name=utf16To8(entry->cFileName); } } else { newEntry->name=utf16To8(entry->cFileName); } newEntry->nameLower=newEntry->name; for (char& i: newEntry->nameLower) { if (i>='A' && i<='Z') i+='a'-'A'; } newEntry->isDir=entry->dwFileAttributes&FILE_ATTRIBUTE_DIRECTORY; newEntry->isHidden=(entry->dwFileAttributes&FILE_ATTRIBUTE_HIDDEN) || (entry->dwFileAttributes&FILE_ATTRIBUTE_SYSTEM); newEntry->type=newEntry->isDir?FP_TYPE_DIR:FP_TYPE_NORMAL; if (!newEntry->isDir) { size_t extPos=newEntry->name.rfind('.'); if (extPos!=String::npos) { newEntry->ext=newEntry->name.substr(extPos); for (char& i: newEntry->ext) { if (i>='A' && i<='Z') i+='a'-'A'; } } } newEntry->size=((uint64_t)entry->nFileSizeHigh<<32)|(uint64_t)entry->nFileSizeLow; newEntry->hasSize=true; if (FileTimeToSystemTime(&entry->ftLastWriteTime,&tempTM)) { // we only use these newEntry->time.tm_year=tempTM.wYear-1900; newEntry->time.tm_mon=tempTM.wMonth-1; newEntry->time.tm_mday=tempTM.wDay; newEntry->time.tm_hour=tempTM.wHour; newEntry->time.tm_min=tempTM.wMinute; newEntry->time.tm_sec=tempTM.wSecond; newEntry->hasTime=true; } return newEntry; } #else FurnaceFilePicker::FileEntry* FurnaceFilePicker::makeEntry(void* _entry, const char* prefix) { struct dirent* entry=(struct dirent*)_entry; FileEntry* newEntry=new FileEntry; if (prefix!=NULL) { if (prefix[0]=='/') { newEntry->name=String(&prefix[1])+"/"+entry->d_name; } else { newEntry->name=entry->d_name; } } else { newEntry->name=entry->d_name; } newEntry->nameLower=newEntry->name; newEntry->isHidden=(entry->d_name[0]=='.'); for (char& i: newEntry->nameLower) { if (i>='A' && i<='Z') i+='a'-'A'; } switch (entry->d_type) { case DT_REG: newEntry->type=FP_TYPE_NORMAL; break; case DT_DIR: newEntry->type=FP_TYPE_DIR; newEntry->isDir=true; break; case DT_LNK: { newEntry->type=FP_TYPE_LINK; // resolve link to determine whether this is a directory String readLinkPath; DIR* readLinkDir=NULL; if (path.empty()) { readLinkPath=newEntry->name; } else if (*path.rbegin()=='/') { readLinkPath=path+newEntry->name; } else { readLinkPath=path+'/'+newEntry->name; } // silly, but works. readLinkDir=opendir(readLinkPath.c_str()); if (readLinkDir!=NULL) { newEntry->isDir=true; closedir(readLinkDir); } break; } case DT_SOCK: newEntry->type=FP_TYPE_SOCKET; break; case DT_FIFO: newEntry->type=FP_TYPE_PIPE; break; case DT_CHR: newEntry->type=FP_TYPE_CHARDEV; break; case DT_BLK: newEntry->type=FP_TYPE_BLOCKDEV; break; default: newEntry->type=FP_TYPE_UNKNOWN; break; } if (!newEntry->isDir) { size_t extPos=newEntry->name.rfind('.'); if (extPos!=String::npos) { newEntry->ext=newEntry->name.substr(extPos); for (char& i: newEntry->ext) { if (i>='A' && i<='Z') i+='a'-'A'; } } } return newEntry; } #endif #ifdef _WIN32 // Windows implementation void FurnaceFilePicker::readDirectorySub() { /// SPECIAL CASE: empty path returns drive list if (path=="") { /// STAGE 1: get list of drives unsigned int drives=GetLogicalDrives(); for (int i=0; i<32; i++) { if (!(drives&(1U<=26) { newEntry->name='A'; newEntry->name+=('A'+(i-26)); } else { newEntry->name+=('A'+i); } newEntry->name+=":\\"; newEntry->nameLower=newEntry->name; for (char& i: newEntry->nameLower) { if (i>='A' && i<='Z') i+='a'-'A'; } newEntry->isDir=true; newEntry->type=FP_TYPE_DIR; entries.push_back(newEntry); } haveFiles=true; /// STAGE 2: get drive information (size!) for (FileEntry* i: entries) { logV("get info for drive %s",i->name); DWORD bytes, sectors, freeClusters, clusters; WString nameW=utf8To16(i->name); if (GetDiskFreeSpaceW(nameW.c_str(),§ors,&bytes,&freeClusters,&clusters)!=0) { i->size=(uint64_t)bytes*(uint64_t)sectors*(uint64_t)clusters; logV("SIZE: %" PRIu64,i->size); i->hasSize=true; } else { logE("COULD NOT..... %x",GetLastError()); } } haveStat=true; return; } /// STAGE 1: get file list WString pathW=utf8To16(path.c_str()); pathW+=L"\\*"; WIN32_FIND_DATAW entry; HANDLE dir=FindFirstFileW(pathW.c_str(),&entry); if (dir==INVALID_HANDLE_VALUE) { wchar_t* errorStr=NULL; int errorSize=FormatMessageW(FORMAT_MESSAGE_ALLOCATE_BUFFER|FORMAT_MESSAGE_FROM_SYSTEM|FORMAT_MESSAGE_IGNORE_INSERTS,NULL,GetLastError(),MAKELANGID(LANG_NEUTRAL,SUBLANG_DEFAULT),(wchar_t*)&errorStr,0,NULL); if (errorSize==0) { failMessage=_("Unknown error!"); } else { failMessage=utf16To8(errorStr); // remove trailing new-line if (failMessage.size()>=2) failMessage.resize(failMessage.size()-2); } LocalFree(errorStr); haveFiles=true; haveStat=true; return; } do { if (wcscmp(entry.cFileName,L".")==0) continue; if (wcscmp(entry.cFileName,L"..")==0) continue; FileEntry* newEntry=makeEntry(&entry,NULL); entryLock.lock(); entries.push_back(newEntry); entryLock.unlock(); if (stopReading) { break; } } while (FindNextFileW(dir,&entry)!=0); FindClose(dir); // on Windows, directory entries contain all the information the file picker needs. // no extra calls needed. haveFiles=true; haveStat=true; } #else // Linux/Unix implementation void FurnaceFilePicker::readDirectorySub() { /// STAGE 1: get file list DIR* dir=opendir(path.c_str()); if (dir==NULL) { failMessage=strerror(errno); haveFiles=true; haveStat=true; return; } struct dirent* entry=NULL; while (true) { entry=readdir(dir); if (entry==NULL) break; if (strcmp(entry->d_name,".")==0) continue; if (strcmp(entry->d_name,"..")==0) continue; FileEntry* newEntry=makeEntry(entry,NULL); entryLock.lock(); entries.push_back(newEntry); entryLock.unlock(); if (stopReading) { break; } } if (closedir(dir)!=0) { // ?! } // we're done - this is sufficient to show a file list (and sort by name) haveFiles=true; /// STAGE 2: retrieve file information completeStat(); haveStat=true; } #endif #ifdef _WIN32 // Windows implementation void FurnaceFilePicker::searchSub(String subPath, int depth) { /// refuse to if we're on the drive list if (path=="") { failMessage=_("Select a drive!"); haveFiles=true; haveStat=true; return; } WString searchQueryW=utf8To16(searchQuery); if (depth>15) logW("searchSub(%s,%d)",subPath,depth); /// STAGE 1: get file list String actualPath=path+subPath; WString pathW=utf8To16(actualPath.c_str()); pathW+=L"\\*"; WIN32_FIND_DATAW entry; HANDLE dir=FindFirstFileW(pathW.c_str(),&entry); if (dir==INVALID_HANDLE_VALUE) { if (depth==0) { wchar_t* errorStr=NULL; int errorSize=FormatMessageW(FORMAT_MESSAGE_ALLOCATE_BUFFER|FORMAT_MESSAGE_FROM_SYSTEM|FORMAT_MESSAGE_IGNORE_INSERTS,NULL,GetLastError(),MAKELANGID(LANG_NEUTRAL,SUBLANG_DEFAULT),(wchar_t*)&errorStr,0,NULL); if (errorSize==0) { failMessage=_("Unknown error!"); } else { failMessage=utf16To8(errorStr); // remove trailing new-line if (failMessage.size()>=2) failMessage.resize(failMessage.size()-2); } LocalFree(errorStr); haveFiles=true; haveStat=true; } return; } do { if (stopReading) { break; } if (wcscmp(entry.cFileName,L".")==0) continue; if (wcscmp(entry.cFileName,L"..")==0) continue; WString lower=entry.cFileName; for (auto& i: lower) { if (i>='A' && i<='Z') i+='a'-'A'; } if (lower.find(searchQueryW)!=WString::npos) { FileEntry* newEntry=makeEntry(&entry,NULL); entryLock.lock(); entries.push_back(newEntry); entryLock.unlock(); } if (entry.dwFileAttributes&FILE_ATTRIBUTE_DIRECTORY) { searchSub(subPath+String("\\")+utf16To8(entry.cFileName),depth+1); } } while (FindNextFileW(dir,&entry)!=0); FindClose(dir); if (depth==0) { haveFiles=true; haveStat=true; } } #else // Linux/Unix implementation void FurnaceFilePicker::searchSub(String subPath, int depth) { /// STAGE 1: get file list if (depth>15) logW("searchSub(%s,%d)",subPath,depth); String actualPath=path+subPath; DIR* dir=opendir(actualPath.c_str()); if (dir==NULL) { if (depth==0) { failMessage=strerror(errno); haveFiles=true; haveStat=true; } return; } struct dirent* entry=NULL; while (true) { if (stopReading) { break; } entry=readdir(dir); if (entry==NULL) break; if (strcmp(entry->d_name,".")==0) continue; if (strcmp(entry->d_name,"..")==0) continue; String lower=entry->d_name; for (char& i: lower) { if (i>='A' && i<='Z') i+='a'-'A'; } if (lower.find(searchQuery)!=String::npos) { FileEntry* newEntry=makeEntry(entry,subPath.c_str()); entryLock.lock(); entries.push_back(newEntry); entryLock.unlock(); } if (entry->d_type==DT_DIR) { searchSub(subPath+String("/")+entry->d_name,depth+1); } } if (closedir(dir)!=0) { // ?! } /// STAGE 2: retrieve file information if (depth==0) { haveFiles=true; completeStat(); haveStat=true; } } #endif String FurnaceFilePicker::normalizePath(const String& which) { String ret; #ifdef _WIN32 wchar_t temp[4096]; memset(temp,0,4096*sizeof(wchar_t)); #else char temp[PATH_MAX]; memset(temp,0,PATH_MAX); #endif #ifndef _WIN32 // don't reject the root on Linux/Unix if (which=="/") { ret=which; return ret; } #endif if (which.empty()) { // on Windows we don't reject an empty path as it has a special meaning #ifndef _WIN32 if (getcwd(temp,PATH_MAX)==NULL) { // sorry... return ""; } ret=temp; #endif } else { // remove redundant directory separators bool alreadySep=false; for (const char& i: which) { if (i==DIR_SEPARATOR) { if (!alreadySep) { alreadySep=true; ret+=i; } } else { alreadySep=false; ret+=i; } } } if (!ret.empty()) { #ifdef _WIN32 // resolve parh WString retW=utf8To16(ret); if (GetFullPathNameW(retW.c_str(),4095,temp,NULL)!=0) { ret=utf16To8(temp); } // if this is the root of a drive, don't remove dir separator if (ret.size()>=5) { // remove dir separator at the end if (*ret.rbegin()==DIR_SEPARATOR) { ret.resize(ret.size()-1); } } #else // resolve path if (realpath(ret.c_str(),temp)!=NULL) { ret=temp; } // remove dir separator at the end if (*ret.rbegin()==DIR_SEPARATOR) { ret.resize(ret.size()-1); } #endif } return ret; } void FurnaceFilePicker::readDirectory(String path) { if (fileThread!=NULL) { // stop current file thread stopReading=true; fileThread->join(); delete fileThread; fileThread=NULL; } // clear all entries sortedEntries.clear(); for (FileEntry* i: entries) { delete i; } entries.clear(); chosenEntries.clear(); updateEntryName(); if (!entryNameHint.empty()) { entryName=entryNameHint; } // start new file thread this->path=normalizePath(path); failMessage=""; editingPath=false; haveFiles=false; haveStat=false; stopReading=false; scheduledSort=1; if (isSearch) { fileThread=new std::thread(_searchThread,this); } else { fileThread=new std::thread(_fileThread,this); } // check whether this path is bookmarked isPathBookmarked=false; for (String& i: bookmarks) { size_t separator=i.find('\n'); if (separator==String::npos) continue; String iName=i.substr(0,separator); String iPath=i.substr(separator+1); if (this->path==iPath) { isPathBookmarked=true; break; } } } void FurnaceFilePicker::setHomeDir(String where) { homeDir=where; } void FurnaceFilePicker::updateEntryName() { if (chosenEntries.empty()) { entryName=""; } else if (chosenEntries.size()>1) { entryName=_(""); } else { entryName=chosenEntries[0]->name; } } // the name of this function is somewhat misleading. // it filters files by type/hidden status, then sorts them. void FurnaceFilePicker::sortFiles() { std::chrono::high_resolution_clock::time_point timeStart=std::chrono::high_resolution_clock::now(); entryLock.lock(); // check for "no filter" if (filterOptions[curFilterType+1]=="*") { // only filter hidden files if needed sortedEntries.clear(); for (FileEntry* i: entries) { if (i->isHidden && !showHiddenFiles) continue; sortedEntries.push_back(i); } } else { // sort by extension std::vector parsedSort; String nextType; for (char i: filterOptions[curFilterType+1]) { switch (i) { case '*': // ignore break; case ' ': // separator if (!nextType.empty()) { parsedSort.push_back(nextType); nextType=""; } break; default: // push nextType.push_back(i); break; } } if (!nextType.empty()) { parsedSort.push_back(nextType); nextType=""; } sortedEntries.clear(); for (FileEntry* i: entries) { if (i->isHidden && !showHiddenFiles) continue; if (i->isDir) { sortedEntries.push_back(i); continue; } for (const String& j: parsedSort) { if (i->ext==j) { sortedEntries.push_back(i); break; } } } } // sort by name std::sort(sortedEntries.begin(),sortedEntries.end(),[this](const FileEntry* a, const FileEntry* b) -> bool { if (sortDirsFirst) { if (a->isDir && !b->isDir) return true; if (!a->isDir && b->isDir) return false; } switch (sortMode) { case FP_SORT_NAME: { // don't do anything. this is handled below. break; } case FP_SORT_EXT: { int result=a->ext.compare(b->ext); // only sort if extensions differ if (result!=0) { return result<0; } break; } case FP_SORT_SIZE: { // only sort if sizes differ if (a->size!=b->size) { return a->sizesize; } break; } case FP_SORT_DATE: { // only sort if dates differ if (a->time.tm_year==b->time.tm_year) { if (a->time.tm_mon==b->time.tm_mon) { if (a->time.tm_mday==b->time.tm_mday) { if (a->time.tm_hour==b->time.tm_hour) { if (a->time.tm_min==b->time.tm_min) { if (a->time.tm_sec==b->time.tm_sec) { // fall back to sorting by name return a->nameLowernameLower; } return a->time.tm_sectime.tm_sec; } return a->time.tm_mintime.tm_min; } return a->time.tm_hourtime.tm_hour; } return a->time.tm_mdaytime.tm_mday; } return a->time.tm_montime.tm_mon; } return a->time.tm_yeartime.tm_year; break; } case FP_SORT_MAX: // impossible break; } // fall back to sorting by name return a->nameLowernameLower; }); entryLock.unlock(); std::chrono::high_resolution_clock::time_point timeEnd=std::chrono::high_resolution_clock::now(); logV("sortFiles() took %dµs",std::chrono::duration_cast(timeEnd-timeStart).count()); } void FurnaceFilePicker::filterFiles() { if (filter.empty() || isSearch) { filteredEntries=sortedEntries; return; } filteredEntries.clear(); String lowerFilter=filter; for (char& i: lowerFilter) { if (i>='A' && i<='Z') i+='a'-'A'; } for (FileEntry* i: sortedEntries) { if (i->nameLower.find(lowerFilter)!=String::npos) { filteredEntries.push_back(i); } } } bool FurnaceFilePicker::isPathAbsolute(const String& p) { #ifdef _WIN32 // TODO: test for UNC path? // convert to absolute path if necessary bool willConvert=(p.size()<3); if (!willConvert) { // test for drive letter if (!(((p[0]>='A' && p[0]<='Z') || (p[0]>='a' && p[0]<='z')) && p[1]==':' && p[2]=='\\')) { if (p.size()<4) { willConvert=true; } else { if (!(((p[0]>='A' && p[0]<='Z') || (p[0]>='a' && p[0]<='z')) && ((p[1]>='A' && p[1]<='Z') || (p[1]>='a' && p[1]<='z')) && p[2]==':' && p[3]=='\\')) { willConvert=true; } } } } return !willConvert; #else if (p.size()<1) return false; return (p[0]=='/'); #endif } void FurnaceFilePicker::addBookmark(const String& p, String n) { if (p==path) isPathBookmarked=true; for (String& i: bookmarks) { size_t separator=i.find('\n'); if (separator==String::npos) continue; String iName=i.substr(0,separator); String iPath=i.substr(separator+1); if (p==iPath) return; } if (n.empty()) { size_t lastSep=p.rfind(DIR_SEPARATOR); if (lastSep==String::npos) { String name=p; name+="\n"; name+=p; bookmarks.push_back(name); } else { String name=p.substr(lastSep+1); name+="\n"; name+=p; bookmarks.push_back(name); } } else { String name=n; name+="\n"; name+=p; bookmarks.push_back(name); } } void FurnaceFilePicker::setSizeConstraints(const ImVec2& min, const ImVec2& max) { minSize=min; maxSize=max; hasSizeConstraints=true; } void FurnaceFilePicker::drawFileList(ImVec2& tableSize, bool& acknowledged) { // display a message on empty dir, no matches or error if (filteredEntries.empty()) { if (ImGui::BeginTable("NoFiles",3,ImGuiTableFlags_BordersOuter,tableSize)) { ImGui::TableSetupColumn("c0",ImGuiTableColumnFlags_WidthStretch,0.5f); ImGui::TableSetupColumn("c1",ImGuiTableColumnFlags_WidthFixed); ImGui::TableSetupColumn("c2",ImGuiTableColumnFlags_WidthStretch,0.5f); ImGui::TableNextRow(); ImGui::TableNextColumn(); ImGui::TableNextColumn(); ImGui::SetCursorPosY(ImGui::GetCursorPosY()+(tableSize.y-ImGui::GetTextLineHeight())*0.5); if (haveFiles) { if (sortedEntries.empty()) { if (failMessage.empty()) { if (isSearch) { ImGui::TextUnformatted(_("No results")); } else { ImGui::TextUnformatted(_("This directory is empty!")); } } else { #ifdef _WIN32 ImGui::Text("%s",failMessage.c_str()); #else ImGui::Text("%s!",failMessage.c_str()); #endif } } else { if (failMessage.empty()) { ImGui::TextUnformatted(_("No results")); } else { #ifdef _WIN32 ImGui::Text("%s",failMessage.c_str()); #else ImGui::Text("%s!",failMessage.c_str()); #endif } } } else { // don't ImGui::TextUnformatted(_("Loading...")); } ImGui::TableNextColumn(); ImGui::EndTable(); } } else { // this is the list view. I might add other view modes in the future... int columns=1; if (displayType) columns++; if (displaySize) columns++; if (displayDate) columns++; if (ImGui::BeginTable("FileList",columns,ImGuiTableFlags_BordersOuter|ImGuiTableFlags_ScrollY|ImGuiTableFlags_RowBg,tableSize)) { float rowHeight=ImGui::GetTextLineHeight()+ImGui::GetStyle().CellPadding.y*2.0f; ImGui::TableSetupColumn("c0",ImGuiTableColumnFlags_WidthStretch); if (displayType) ImGui::TableSetupColumn("c1",ImGuiTableColumnFlags_WidthFixed,ImGui::CalcTextSize(" .eeee").x); if (displaySize) ImGui::TableSetupColumn("c2",ImGuiTableColumnFlags_WidthFixed,ImGui::CalcTextSize(" 999.99G").x); if (displayDate) ImGui::TableSetupColumn("c3",ImGuiTableColumnFlags_WidthFixed,ImGui::CalcTextSize(" 6969/69/69 04:20").x); ImGui::TableSetupScrollFreeze(0,1); // header (sort options) String nameHeader=_("Name")+String("###SortName"); String typeHeader=_("Type")+String("###SortType"); String sizeHeader=_("Size")+String("###SortSize"); String dateHeader=_("Date")+String("###SortDate"); switch (sortMode) { case FP_SORT_NAME: if (sortInvert[FP_SORT_NAME]) { nameHeader.insert(0,ICON_FA_CHEVRON_UP); } else { nameHeader.insert(0,ICON_FA_CHEVRON_DOWN); } break; case FP_SORT_EXT: if (sortInvert[FP_SORT_EXT]) { typeHeader.insert(0,ICON_FA_CHEVRON_UP); } else { typeHeader.insert(0,ICON_FA_CHEVRON_DOWN); } break; case FP_SORT_SIZE: if (sortInvert[FP_SORT_SIZE]) { sizeHeader.insert(0,ICON_FA_CHEVRON_UP); } else { sizeHeader.insert(0,ICON_FA_CHEVRON_DOWN); } break; case FP_SORT_DATE: if (sortInvert[FP_SORT_DATE]) { dateHeader.insert(0,ICON_FA_CHEVRON_UP); } else { dateHeader.insert(0,ICON_FA_CHEVRON_DOWN); } break; case FP_SORT_MAX: // impossible break; } ImGui::TableNextRow(ImGuiTableRowFlags_Headers,rowHeight); ImGui::TableNextColumn(); if (ImGui::Selectable(nameHeader.c_str())) { if (sortMode==FP_SORT_NAME) { sortInvert[sortMode]=!sortInvert[sortMode]; } else { sortMode=FP_SORT_NAME; scheduledSort=1; } } if (displayType) { ImGui::TableNextColumn(); if (ImGui::Selectable(typeHeader.c_str())) { if (sortMode==FP_SORT_EXT) { sortInvert[sortMode]=!sortInvert[sortMode]; } else { sortMode=FP_SORT_EXT; scheduledSort=1; } } } if (displaySize) { ImGui::TableNextColumn(); if (ImGui::Selectable(sizeHeader.c_str())) { if (sortMode==FP_SORT_SIZE) { sortInvert[sortMode]=!sortInvert[sortMode]; } else { sortMode=FP_SORT_SIZE; scheduledSort=1; } } } if (displayDate) { ImGui::TableNextColumn(); if (ImGui::Selectable(dateHeader.c_str())) { if (sortMode==FP_SORT_DATE) { sortInvert[sortMode]=!sortInvert[sortMode]; } else { sortMode=FP_SORT_DATE; scheduledSort=1; } } } // file list entryLock.lock(); int index=0; listClipper.Begin(filteredEntries.size(),rowHeight); while (listClipper.Step()) { for (int _i=listClipper.DisplayStart; _itype]; // get style for this entry if (i->isDir) { style=&defaultTypeStyle[FP_TYPE_DIR]; } else { if (!i->ext.empty()) { for (FileTypeStyle& j: fileTypeRegistry) { if (i->ext==j.ext) { style=&j; break; } } } } // draw ImGui::TableNextRow(0,rowHeight); // name ImGui::TableNextColumn(); ImGui::PushStyleColor(ImGuiCol_Text,ImGui::GetColorU32(style->color)); ImGui::PushID(index++); if (ImGui::Selectable(style->icon.c_str(),i->isSelected,ImGuiSelectableFlags_AllowDoubleClick|ImGuiSelectableFlags_SpanAllColumns|ImGuiSelectableFlags_SpanAvailWidth)) { bool doNotAcknowledge=false; if ((ImGui::IsKeyDown(ImGuiKey_LeftShift) || ImGui::IsKeyDown(ImGuiKey_RightShift)) && multiSelect) { // multiple selection doNotAcknowledge=true; } else { // clear selected entries for (FileEntry* j: chosenEntries) { j->isSelected=false; } chosenEntries.clear(); } bool alreadySelected=false; for (FileEntry* j: chosenEntries) { if (j==i) alreadySelected=true; } if (!alreadySelected) { // select this entry chosenEntries.push_back(i); i->isSelected=true; updateEntryName(); if (!doNotAcknowledge) { if (isMobile || singleClickSelect) { acknowledged=true; } else if (ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) { acknowledged=true; } } } } ImGui::PopID(); ImGui::SameLine(); ImGui::TextUnformatted(i->name.c_str()); // type if (displayType) { ImGui::TableNextColumn(); ImGui::TextUnformatted(i->ext.c_str()); } // size if (displaySize) { ImGui::TableNextColumn(); if (i->hasSize && (i->type==FP_TYPE_NORMAL || path.empty())) { int sizeShift=0; uint64_t sizeShifted=i->size; while (sizeShifted && sizeShift<7) { sizeShifted>>=10; sizeShift++; } sizeShift--; uint64_t intPart=i->size>>(sizeShift*10); uint64_t fracPart=i->size&((1U<<(sizeShift*10))-1); // shift so we have sufficient digits for 100 // (precision loss is negligible) if (sizeShift>0) { fracPart=(100*(fracPart>>3))>>((sizeShift*10)-3); if (fracPart>99) fracPart=99; ImGui::Text("%" PRIu64 ".%02" PRIu64 "%c",intPart,fracPart,sizeSuffixes[sizeShift&7]); } else { ImGui::Text("%" PRIu64,i->size); } } } // date if (displayDate) { ImGui::TableNextColumn(); if (i->hasTime) { ImGui::Text("%d/%02d/%02d %02d:%02d",i->time.tm_year+1900,i->time.tm_mon+1,i->time.tm_mday,i->time.tm_hour,i->time.tm_min); } } ImGui::PopStyleColor(); } } ImGui::EndTable(); entryLock.unlock(); } } } void FurnaceFilePicker::drawBookmarks(ImVec2& tableSize, String& newDir) { if (ImGui::BeginTable("BookmarksList",1,ImGuiTableFlags_BordersOuter|ImGuiTableFlags_ScrollY,tableSize)) { ImGui::TableSetupScrollFreeze(0,1); ImGui::TableNextRow(ImGuiTableRowFlags_Headers); ImGui::TableNextColumn(); ImGui::TextUnformatted(_("Bookmarks")); ImGui::SameLine(); float iconSize=ImGui::CalcTextSize(ICON_FA_PLUS).x; if (ImGui::Selectable(ICON_FA_PLUS "##AddBookmark",false,0,ImVec2(iconSize,0))) { newBookmarkName=_("New Bookmark"); newBookmarkPath=path; } ImGui::SetItemTooltip(_("Create bookmark")); if (ImGui::BeginPopupContextItem("NewBookmark",ImGuiPopupFlags_MouseButtonLeft)) { ImGui::TextUnformatted(_("Name:")); ImGui::InputText("##NameI",&newBookmarkName); ImGui::TextUnformatted(_("Path:")); ImGui::InputText("##PathI",&newBookmarkPath); ImGui::BeginDisabled(newBookmarkName.empty() || newBookmarkPath.empty()); if (ImGui::Button(_("OK"))) { if (newBookmarkName.empty() || newBookmarkPath.empty()) { // no! } else { newBookmarkPath=normalizePath(newBookmarkPath); addBookmark(newBookmarkPath,newBookmarkName); } ImGui::CloseCurrentPopup(); } ImGui::SameLine(); if (ImGui::Button(_("Cancel"))) { ImGui::CloseCurrentPopup(); } ImGui::EndDisabled(); ImGui::EndPopup(); } ImGui::SameLine(); if (ImGui::Selectable(ICON_FA_TIMES "##CloseBookmarks",false,0,ImVec2(iconSize,0))) { showBookmarks=false; } ImGui::SetItemTooltip(_("Hide bookmarks list")); int index=-1; int markedForRemoval=-1; for (String& i: bookmarks) { ++index; size_t separator=i.find('\n'); if (separator==String::npos) continue; String iName=i.substr(0,separator); String iPath=i.substr(separator+1); ImGui::TableNextRow(); ImGui::TableNextColumn(); ImGui::PushID(200000+index); if (ImGui::Selectable(iName.c_str(),iPath==path)) { newDir=iPath; } if (ImGui::BeginPopupContextItem("BookmarkOpts")) { if (ImGui::MenuItem(_("remove"))) { markedForRemoval=index; if (iPath==path) isPathBookmarked=false; } ImGui::EndPopup(); } ImGui::PopID(); } if (markedForRemoval>=0) { bookmarks.erase(bookmarks.begin()+markedForRemoval); } ImGui::EndTable(); } } bool FurnaceFilePicker::draw(ImGuiWindowFlags winFlags) { if (!isOpen) { hasSizeConstraints=false; return false; } String newDir; bool acknowledged=false; bool readDrives=false; bool wantSearch=false; bool began=false; // center the window if it is unmovable and not an embed if ((winFlags&ImGuiWindowFlags_NoMove) && !isEmbed) { ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(),ImGuiCond_Always,ImVec2(0.5f,0.5f)); } if (isEmbed) { began=true; } else if (isModal) { ImGui::OpenPopup(windowName.c_str()); if (hasSizeConstraints) ImGui::SetNextWindowSizeConstraints(minSize,maxSize); began=ImGui::BeginPopupModal(windowName.c_str(),NULL,ImGuiWindowFlags_NoScrollbar|winFlags); } else { if (hasSizeConstraints) ImGui::SetNextWindowSizeConstraints(minSize,maxSize); began=ImGui::Begin(windowName.c_str(),NULL,ImGuiWindowFlags_NoScrollbar|winFlags); } if (began) { // enforce window constraints if necessary if (hasSizeConstraints) { if (ImGui::GetWindowSize().x=2) mkdirError.resize(mkdirError.size()-2); } LocalFree(errorStr); } else { newDir=mkdirPath; ImGui::CloseCurrentPopup(); } } #else // convert to absolute path if necessary if (!isPathAbsolute(mkdirPath)) { if (!path.empty()) { if (*path.rbegin()=='/') { mkdirPath=path+mkdirPath; } else { mkdirPath=path+'/'+mkdirPath; } } } // create directory int result=mkdir(mkdirPath.c_str(),0755); if (result!=0) { mkdirError=strerror(errno); } else { newDir=mkdirPath; ImGui::CloseCurrentPopup(); } #endif } } ImGui::EndDisabled(); ImGui::SameLine(); if (ImGui::Button(_("Cancel"))) { ImGui::CloseCurrentPopup(); } } else { ImGui::Text(_("I can't! (%s)\nCheck whether the path is correct and you have access to it."),mkdirError.c_str()); if (ImGui::Button(_("Back"))) { mkdirError=""; } } ImGui::EndPopup(); } ImGui::SameLine(); ImGui::SeparatorEx(ImGuiSeparatorFlags_Vertical); ImGui::SameLine(); if (ImGui::Button(ICON_FA_HOME "##HomeDir")) { newDir=homeDir; } ImGui::SetItemTooltip(_("Go to home directory")); ImGui::SameLine(); if (ImGui::Button(ICON_FA_CHEVRON_UP "##ParentDir")) { logV("Parent dir......"); size_t pos=path.rfind(DIR_SEPARATOR); #ifdef _WIN32 if (pos==2 || pos==3) { if (path.size()<5) { newDir=""; readDrives=true; } else { newDir=path.substr(0,pos+1); } } else if (pos!=String::npos) { newDir=path.substr(0,pos); if (newDir.empty()) readDrives=true; } #else // stop at the root if (pos!=String::npos && path!="/") { newDir=path.substr(0,pos); if (newDir.empty()) newDir="/"; } #endif } ImGui::SetItemTooltip(_("Go to parent directory")); #ifdef _WIN32 // drives button only on Windows ImGui::SameLine(); if (ImGui::Button(ICON_FA_HDD_O "##DriveList")) { newDir=""; readDrives=true; } ImGui::SetItemTooltip(_("Drives")); #endif ImGui::SameLine(); if (ImGui::Button(ICON_FA_PENCIL "##EditPath")) { editablePath=path; editingPath=true; } ImGui::SetItemTooltip(_("Edit path")); ImGui::SameLine(); ImGui::SeparatorEx(ImGuiSeparatorFlags_Vertical); if (editingPath) { ImGui::SameLine(); ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x-(ImGui::GetStyle().ItemSpacing.x+ImGui::GetStyle().FramePadding.x*2.0f+ImGui::CalcTextSize(_("OK")).x)); ImGui::InputText("##EditablePath",&editablePath); ImGui::SameLine(); if (ImGui::Button(_("OK##AcceptPath"))) { newDir=editablePath; } } else { // explode path into buttons String pathLeading=path; if (!path.empty()) { if (*path.rbegin()!=DIR_SEPARATOR) pathLeading+=DIR_SEPARATOR_STR; } #ifdef _WIN32 String nextButton; #else String nextButton="/"; #endif String pathAsOfNow; int pathLevel=0x10000000; for (char i: pathLeading) { pathAsOfNow+=i; if (i==DIR_SEPARATOR) { // create button ImGui::PushID(100000+pathLevel); ImGui::SameLine(); if (ImGui::Button(nextButton.c_str())) { newDir=pathAsOfNow; } pathLevel++; ImGui::PopID(); nextButton=""; } else { nextButton+=i; } } } // search bar if (ImGui::Button(isPathBookmarked?(ICON_FA_BOOKMARK "##Bookmarks"):(ICON_FA_BOOKMARK_O "##Bookmarks"))) { if (isPathBookmarked && showBookmarks) { for (size_t i=0; i