/** * 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 will eventually replace 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 #ifdef _WIN32 #include #include "../utfutils.h" #else #include #include #include #include #endif #include #include static const char* sizeSuffixes=".KMGTPEZ"; static void _fileThread(void* item) { ((FurnaceFilePicker*)item)->readDirectorySub(); } #ifdef _WIN32 // Windows implementation void FurnaceFilePicker::readDirectorySub() { /// SPECIAL CASE: empty path returns drive list if (path=="") { 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; haveStat=true; return; } /// STAGE 1: get file list WString pathW=utf8To16(path.c_str()); WIN32_FIND_DATAW entry; SYSTEMTIME tempTM; 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); } LocalFree(errorStr); haveFiles=true; haveStat=true; return; } do { FileEntry* newEntry=new FileEntry; 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->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; } entries.push_back(newEntry); 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=new FileEntry; newEntry->name=entry->d_name; newEntry->nameLower=entry->d_name; 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. logV("Read a symlink..."); readLinkDir=opendir(readLinkPath.c_str()); if (readLinkDir!=NULL) { logV("Is file"); 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'; } } } entries.push_back(newEntry); 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 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(); } 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(); // start new file thread this->path=normalizePath(path); failMessage=""; editingPath=false; haveFiles=false; haveStat=false; stopReading=false; scheduledSort=1; fileThread=new std::thread(_fileThread,this); } 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, 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]=="*") { // copy entire list sortedEntries=entries; } 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->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 (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; } } // 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()) { 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::draw() { if (!isOpen) return false; String newDir; bool acknowledged=false; bool readDrives=false; ImGui::SetNextWindowSizeConstraints(ImVec2(800.0,600.0),ImVec2(8000.0,6000.0)); if (ImGui::Begin(windowName.c_str(),NULL,ImGuiWindowFlags_NoSavedSettings)) { // header bars if (ImGui::Button(ICON_FA_PLUS "##MakeDir")) { mkdirError=""; mkdirPath=""; } if (ImGui::BeginPopupContextItem("CreateDir",ImGuiPopupFlags_MouseButtonLeft)) { if (mkdirError.empty()) { ImGui::Text("Directory name:"); ImGui::InputText("##DirName",&mkdirPath); ImGui::BeginDisabled(mkdirPath.empty()); if (ImGui::Button("OK")) { if (mkdirPath.empty()) { mkdirError="Maybe try that again under better circumstances..."; } else { #ifdef _WIN32 // convert to absolute path if necessary bool willConvert=(mkdirPath.size()<3); if (!willConvert) { // test for drive letter if (!(((mkdirPath[0]>='A' && mkdirPath[0]<='Z') || (mkdirPath[0]>='a' && mkdirPath[0]<='z')) && mkdirPath[1]==':' && mkdirPath[2]=='\\')) { if (mkdirPath.size()<4) { willConvert=true; } else { if (!(((mkdirPath[0]>='A' && mkdirPath[0]<='Z') || (mkdirPath[0]>='a' && mkdirPath[0]<='z')) && ((mkdirPath[1]>='A' && mkdirPath[1]<='Z') || (mkdirPath[1]>='a' && mkdirPath[1]<='z')) && mkdirPath[2]==':' && mkdirPath[3]=='\\')) { willConvert=true; } } } } if (willConvert) { if (path.empty()) { // error out in the drives view mkdirError="Trying to create a directory in the drives list"; } else if (*path.rbegin()=='\\') { mkdirPath=path+mkdirPath; } else { mkdirPath=path+'\\'+mkdirPath; } } // create directory if (mkdirError.empty()) { WString mkdirPathW=utf8To16(mkdirPath); if (CreateDirectoryW(mkdirPathW.c_str(),NULL)==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) { mkdirError="Unknown error"; } else { mkdirError=utf16To8(errorStr); } LocalFree(errorStr); } else { newDir=mkdirPath; ImGui::CloseCurrentPopup(); } } #else // convert to absolute path if necessary if (mkdirPath[0]!='/') { 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::SameLine(); if (ImGui::Button(ICON_FA_CHEVRON_UP "##ParentDir")) { 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 // stop at the root if (pos!=String::npos && path!="/") { newDir=path.substr(0,pos); if (newDir.empty()) newDir="/"; } #endif } ImGui::SameLine(); if (ImGui::Button(ICON_FA_PENCIL "##EditPath")) { editablePath=path; editingPath=true; } 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(pathLevel); ImGui::SameLine(); if (ImGui::Button(nextButton.c_str())) { newDir=pathAsOfNow; } pathLevel++; ImGui::PopID(); nextButton=""; } else { nextButton+=i; } } } // search bar if (ImGui::Button(ICON_FA_REPEAT "##ClearFilter")) { filter=""; filterFiles(); } ImGui::SameLine(); ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); if (ImGui::InputTextWithHint("##Filter","Search",&filter)) { filterFiles(); } if (scheduledSort && haveFiles) { if (haveStat) { scheduledSort=0; } sortFiles(); filterFiles(); } ImVec2 tableSize=ImGui::GetContentRegionAvail(); tableSize.y-=ImGui::GetFrameHeightWithSpacing()*2.0f; // display a message on empty dir, no matches or error if (!haveFiles) { if (ImGui::BeginTable("LoadingFiles",1,ImGuiTableFlags_BordersOuter,tableSize)) { ImGui::EndTable(); } } else 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 (sortedEntries.empty()) { if (failMessage.empty()) { ImGui::Text("This directory is empty!"); } else { ImGui::Text("%s!",failMessage.c_str()); } } else { if (failMessage.empty()) { ImGui::Text("No results"); } else { ImGui::Text("%s!",failMessage.c_str()); } } ImGui::TableNextColumn(); ImGui::EndTable(); } } else { // this is the list view. I might add other view modes in the future... if (ImGui::BeginTable("FileList",4,ImGuiTableFlags_BordersOuter|ImGuiTableFlags_ScrollY|ImGuiTableFlags_RowBg,tableSize)) { float rowHeight=ImGui::GetTextLineHeight()+ImGui::GetStyle().CellPadding.y*2.0f; ImGui::TableSetupColumn("c0",ImGuiTableColumnFlags_WidthStretch); ImGui::TableSetupColumn("c1",ImGuiTableColumnFlags_WidthFixed,ImGui::CalcTextSize(" .eeee").x); ImGui::TableSetupColumn("c2",ImGuiTableColumnFlags_WidthFixed,ImGui::CalcTextSize(" 999.99G").x); ImGui::TableSetupColumn("c3",ImGuiTableColumnFlags_WidthFixed,ImGui::CalcTextSize(" 6969/69/69 04:20").x); ImGui::TableSetupScrollFreeze(0,1); // header (sort options) const char* nameHeader="Name##SortName"; const char* typeHeader="Type##SortType"; const char* sizeHeader="Size##SortSize"; const char* dateHeader="Date##SortDate"; switch (sortMode) { case FP_SORT_NAME: if (sortInvert) { nameHeader=ICON_FA_CHEVRON_UP "Name##SortName"; } else { nameHeader=ICON_FA_CHEVRON_DOWN "Name##SortName"; } break; case FP_SORT_EXT: if (sortInvert) { typeHeader=ICON_FA_CHEVRON_UP "Type##SortType"; } else { typeHeader=ICON_FA_CHEVRON_DOWN "Type##SortType"; } break; case FP_SORT_SIZE: if (sortInvert) { sizeHeader=ICON_FA_CHEVRON_UP "Size##SortSize"; } else { sizeHeader=ICON_FA_CHEVRON_DOWN "Size##SortSize"; } break; case FP_SORT_DATE: if (sortInvert) { dateHeader=ICON_FA_CHEVRON_UP "Date##SortDate"; } else { dateHeader=ICON_FA_CHEVRON_DOWN "Date##SortDate"; } break; } ImGui::TableNextRow(ImGuiTableRowFlags_Headers,rowHeight); ImGui::TableNextColumn(); if (ImGui::Selectable(nameHeader)) { if (sortMode==FP_SORT_NAME) { sortInvert=!sortInvert; } else { sortMode=FP_SORT_NAME; scheduledSort=1; } } ImGui::TableNextColumn(); if (ImGui::Selectable(typeHeader)) { if (sortMode==FP_SORT_EXT) { sortInvert=!sortInvert; } else { sortMode=FP_SORT_EXT; scheduledSort=1; } } ImGui::TableNextColumn(); if (ImGui::Selectable(sizeHeader)) { if (sortMode==FP_SORT_SIZE) { sortInvert=!sortInvert; } else { sortMode=FP_SORT_SIZE; scheduledSort=1; } } ImGui::TableNextColumn(); if (ImGui::Selectable(dateHeader)) { if (sortMode==FP_SORT_DATE) { sortInvert=!sortInvert; } 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); 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)) { for (FileEntry* j: chosenEntries) { j->isSelected=false; } chosenEntries.clear(); chosenEntries.push_back(i); i->isSelected=true; updateEntryName(); if (isMobile) { acknowledged=true; } else if (ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) { acknowledged=true; } } ImGui::PopID(); ImGui::SameLine(); ImGui::TextUnformatted(i->name.c_str()); ImGui::TableNextColumn(); ImGui::TextUnformatted(i->ext.c_str()); ImGui::TableNextColumn(); if (i->hasSize && i->type==FP_TYPE_NORMAL) { 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); } } 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(); } } // file name input ImGui::AlignTextToFramePadding(); ImGui::TextUnformatted("Name: "); ImGui::SameLine(); ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x*0.68f); if (ImGui::InputText("##EntryName",&entryName)) { // find an entry with this name } ImGui::SameLine(); ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); if (ImGui::BeginCombo("##FilterType",filterOptions[curFilterType].c_str())) { for (size_t i=0; iisDir) { if (path.empty()) { newDir=chosenEntries[0]->name; } else { // go there unless we've been required to select a directory if (*path.rbegin()==DIR_SEPARATOR) { newDir=path+chosenEntries[0]->name; } else { newDir=path+DIR_SEPARATOR+chosenEntries[0]->name; } } } else { // select this entry curStatus=FP_STATUS_ACCEPTED; isOpen=false; } } } } ImGui::End(); if (!newDir.empty() || readDrives) { // change directory readDirectory(newDir); } return false; } bool FurnaceFilePicker::open(String name, String pa, bool modal, const std::vector& filter) { if (isOpen) return false; if (filter.size()&1) { logE("invalid filter data! it should be an even-sized vector with even elements containing names and odd ones being the filters."); return false; } filterOptions=filter; if (filterOptions.size()<2) { filterOptions.push_back("all files"); filterOptions.push_back("*"); } curFilterType=0; readDirectory(pa); windowName=name; isOpen=true; return true; } const String& FurnaceFilePicker::getEntryName() { return entryName; } void FurnaceFilePicker::setMobile(bool val) { isMobile=val; } FilePickerStatus FurnaceFilePicker::getStatus() { FilePickerStatus retStatus=curStatus; curStatus=FP_STATUS_WAITING; return retStatus; } void FurnaceFilePicker::loadSettings(DivConfig& conf) { } void FurnaceFilePicker::saveSettings(DivConfig& conf) { } void FurnaceFilePicker::setTypeStyle(FileType type, ImVec4 color, String icon) { // "##File" is appended here for performance. defaultTypeStyle[type].icon=icon+"##File"; defaultTypeStyle[type].color=color; } void FurnaceFilePicker::registerType(String ext, ImVec4 color, String icon) { FileTypeStyle t; t.ext=ext; // "##File" is appended here for performance. t.icon=icon+"##File"; t.color=color; fileTypeRegistry.push_back(t); } void FurnaceFilePicker::clearTypes() { fileTypeRegistry.clear(); } FurnaceFilePicker::FurnaceFilePicker(): fileThread(NULL), haveFiles(false), haveStat(false), stopReading(false), isOpen(false), isMobile(false), sortInvert(false), scheduledSort(0), curFilterType(0), sortMode(FP_SORT_NAME), curStatus(FP_STATUS_WAITING), editingPath(false) { for (int i=0; i