diff --git a/src/gui/commandPalette.cpp b/src/gui/commandPalette.cpp index e4cca5bf9..868b5f1af 100644 --- a/src/gui/commandPalette.cpp +++ b/src/gui/commandPalette.cpp @@ -25,20 +25,124 @@ #include #include #include "../ta-log.h" +#include "util.h" -static inline bool matchFuzzy(const char* haystack, const char* needle) { - size_t h_i=0; // haystack idx - size_t n_i=0; // needle idx - while (needle[n_i]!='\0') { - for (; std::tolower(haystack[h_i])!=std::tolower(needle[n_i]); h_i++) { - if (haystack[h_i]=='\0') - return false; - } - n_i+=1; +struct MatchScore { + size_t charsBeforeNeedle=0; + size_t charsWithinNeedle=0; + static bool IsFirstPreferable(const MatchScore& a, const MatchScore& b) { + int aBetter; + aBetter=b.charsWithinNeedle-a.charsWithinNeedle; + if (aBetter!=0) return aBetter>0; + aBetter=b.charsBeforeNeedle-a.charsBeforeNeedle; + if (aBetter!=0) return aBetter>0; + return false; } - return true; +}; + +struct MatchResult { + MatchScore score; + std::vector highlightChars; +}; + +static bool charMatch(const char* a, const char* b) { + // stub for future utf8 support, possibly with matching for related chars? + return std::tolower(*a)==std::tolower(*b); } +// #define MATCH_GREEDY +// #define RUN_MATCH_TEST + +static bool matchFuzzy(const char* haystack, int haystackLen, const char* needle, int needleLen, MatchResult* result) { + if (needleLen==0) { + result->score.charsBeforeNeedle=0; + result->score.charsWithinNeedle=0; + result->highlightChars.clear(); + return true; + } + + std::vector matchPool(needleLen+1); + std::vector unusedMatches(needleLen+1); + std::vector matchesByLen(needleLen+1); + for (int i=0; i=0; matchLen--) { + MatchResult*& m=matchesByLen[matchLen]; + + // ignore null matches except for 0 + if (matchLen>0 && !m) continue; + +#ifdef MATCH_GREEDY + // in greedy mode, don't start any new matches once we've already started matching. + // this will still return the correct bool result, but its score could be much poorer + // than the optimal match. consider the case: + // + // find "gl" in "google" + // + // greedy will see the match "g...l.", which has charsWithinNeedle of 3, while the + // fully algorithm will find the tighter match "...gl.", which has + // charsWithinNeedle of 0 + + if (matchLen==0 && unusedMatches.size() < matchPool.size()) { + continue; + } +#endif + + // check match! + if (charMatch(haystack+hIdx, needle+matchLen)) { + + // pull a fresh match from the pool if necessary + if (matchLen==0) { + m=unusedMatches.back(); + unusedMatches.pop_back(); + m->score.charsBeforeNeedle=hIdx; + m->score.charsWithinNeedle=0; + m->highlightChars.clear(); + } + + m->highlightChars.push_back(hIdx); + + // advance, replacing the previous match of an equal len, which can only have been + // worse because it existed before us, so we can prune it out + if (matchesByLen[matchLen+1]) { + unusedMatches.push_back(matchesByLen[matchLen+1]); + } + + matchesByLen[matchLen+1]=m; + m=NULL; + + } else { + // tally up charsWithinNeedle + if (matchLen>0) { + matchesByLen[matchLen]->score.charsWithinNeedle++; + } + } + } + } + + if (matchesByLen[needleLen]) { + if (result) *result=*matchesByLen[needleLen]; + return true; + } + + return false; +} + +#ifdef RUN_MATCH_TEST +static void matchFuzzyTest() { + String hay="a__i_a_i__o"; + String needle="aio"; + MatchResult match; + matchFuzzy(hay.c_str(), hay.length(), needle.c_str(), needle.length(), &match); + logI( "match.score.charsWithinNeedle: %d", match.score.charsWithinNeedle ); +} +#endif + void FurnaceGUI::drawPalette() { bool accepted=false; @@ -67,45 +171,52 @@ void FurnaceGUI::drawPalette() { break; } +#ifdef RUN_MATCH_TEST + matchFuzzyTest(); +#endif + if (ImGui::InputTextWithHint("##CommandPaletteSearch",hint,&paletteQuery) || paletteFirstFrame) { paletteSearchResults.clear(); + std::vector matchScores; + + auto Evaluate=[&](int i, const char* name, int nameLen) { + MatchResult result; + if (matchFuzzy(name, nameLen, paletteQuery.c_str(), paletteQuery.length(), &result)) { + paletteSearchResults.emplace_back(); + paletteSearchResults.back().id=i; + paletteSearchResults.back().highlightChars=std::move(result.highlightChars); + matchScores.push_back(result.score); + } + }; switch (curPaletteType) { case CMDPAL_TYPE_MAIN: for (int i=0; isong.insLen; i++) { String s=fmt::sprintf("%02X: %s", i, e->song.ins[i]->name.c_str()); - if (matchFuzzy(s.c_str(),paletteQuery.c_str())) { - paletteSearchResults.push_back(i+1); // because over here ins=0 is 'None' - } + Evaluate(i+1,s.c_str(),s.length()); // because over here ins=0 is 'None' } break; + } case CMDPAL_TYPE_SAMPLES: for (int i=0; isong.sampleLen; i++) { - if (matchFuzzy(e->song.sample[i]->name.c_str(),paletteQuery.c_str())) { - paletteSearchResults.push_back(i); - } + Evaluate(i,e->song.sample[i]->name.c_str(),e->song.sample[i]->name.length()); } break; @@ -113,9 +224,7 @@ void FurnaceGUI::drawPalette() { for (int i=0; availableSystems[i]; i++) { int ds=availableSystems[i]; const char* sysname=getSystemName((DivSystem)ds); - if (matchFuzzy(sysname,paletteQuery.c_str())) { - paletteSearchResults.push_back(ds); - } + Evaluate(ds,sysname,strlen(sysname)); } break; @@ -124,67 +233,110 @@ void FurnaceGUI::drawPalette() { ImGui::CloseCurrentPopup(); break; }; + + // sort indices by match quality + std::vector sortingIndices(paletteSearchResults.size()); + for (size_t i=0; i paletteSearchResultsCopy=paletteSearchResults; + for (size_t i=0; i0) { - curPaletteChoice-=1; - navigated=true; - } - if (ImGui::IsKeyPressed(ImGuiKey_DownArrow)) { - curPaletteChoice+=1; - navigated=true; - } - - if (paletteSearchResults.size()>0 && curPaletteChoice<0) { - curPaletteChoice=0; - navigated=true; - } - if (curPaletteChoice>=(int)paletteSearchResults.size()) { - curPaletteChoice=paletteSearchResults.size()-1; - navigated=true; - } - - for (int i=0; i<(int)paletteSearchResults.size(); i++) { - bool current=(i==curPaletteChoice); - int id=paletteSearchResults[i]; - - String s="???"; - switch (curPaletteType) { - case CMDPAL_TYPE_MAIN: - s=guiActions[id].friendlyName; - break; - case CMDPAL_TYPE_RECENT: - s=recentFile[id].c_str(); - break; - case CMDPAL_TYPE_INSTRUMENTS: - case CMDPAL_TYPE_INSTRUMENT_CHANGE: - if (id==0) { - s=_("- None -"); - } else { - s=fmt::sprintf("%02X: %s", id-1, e->song.ins[id-1]->name.c_str()); - } - break; - case CMDPAL_TYPE_SAMPLES: - s=e->song.sample[id]->name.c_str(); - break; - case CMDPAL_TYPE_ADD_CHIP: - s=getSystemName((DivSystem)id); - break; - default: - logE(_("invalid command palette type")); - break; - }; - - if (ImGui::Selectable(s.c_str(),current)) { - curPaletteChoice=i; - accepted=true; + bool navigated=false; + if (ImGui::IsKeyPressed(ImGuiKey_UpArrow) && curPaletteChoice>0) { + curPaletteChoice-=1; + navigated=true; } - if ((navigated || paletteFirstFrame) && current) ImGui::SetScrollHereY(); + if (ImGui::IsKeyPressed(ImGuiKey_DownArrow)) { + curPaletteChoice+=1; + navigated=true; + } + + if (paletteSearchResults.size()>0 && curPaletteChoice<0) { + curPaletteChoice=0; + navigated=true; + } + if (curPaletteChoice>=(int)paletteSearchResults.size()) { + curPaletteChoice=paletteSearchResults.size()-1; + navigated=true; + } + + int columnCount=curPaletteType==CMDPAL_TYPE_MAIN ? 2 : 1; + if (ImGui::BeginTable("##commandPaletteTable",columnCount,ImGuiTableFlags_SizingStretchProp)) { + // ImGui::TableSetupColumn("##action",ImGuiTableColumnFlags_WidthStretch); + // ImGui::TableSetupColumn("##shortcut"); + for (int i=0; i<(int)paletteSearchResults.size(); i++) { + bool current=(i==curPaletteChoice); + int id=paletteSearchResults[i].id; + + String s="???"; + switch (curPaletteType) { + case CMDPAL_TYPE_MAIN: + s=guiActions[id].friendlyName; + break; + case CMDPAL_TYPE_RECENT: + s=recentFile[id].c_str(); + break; + case CMDPAL_TYPE_INSTRUMENTS: + case CMDPAL_TYPE_INSTRUMENT_CHANGE: + if (id==0) { + s=_("- None -"); + } else { + s=fmt::sprintf("%02X: %s", id-1, e->song.ins[id-1]->name.c_str()); + } + break; + case CMDPAL_TYPE_SAMPLES: + s=e->song.sample[id]->name.c_str(); + break; + case CMDPAL_TYPE_ADD_CHIP: + s=getSystemName((DivSystem)id); + break; + default: + logE(_("invalid command palette type")); + break; + }; + + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + + ImGui::PushID(s.c_str()); + bool selectable=ImGui::Selectable("##paletteSearchItem",current,ImGuiSelectableFlags_SpanAllColumns|ImGuiSelectableFlags_AllowOverlap); + const char* str=s.c_str(); + size_t chCursor=0; + const std::vector& highlights=paletteSearchResults[i].highlightChars; + for (size_t ch=0; ch0) { - int i=paletteSearchResults[curPaletteChoice]; + int i=paletteSearchResults[curPaletteChoice].id; switch (curPaletteType) { case CMDPAL_TYPE_MAIN: doAction(i); diff --git a/src/gui/gui.h b/src/gui/gui.h index 135393ee6..57d6403ca 100644 --- a/src/gui/gui.h +++ b/src/gui/gui.h @@ -1628,10 +1628,11 @@ class FurnaceGUI { String mmlStringSNES[DIV_MAX_CHIPS]; String folderString; + struct PaletteSearchResult { int id; std::vector highlightChars; }; std::vector sysSearchResults; std::vector> sampleBankSearchResults; std::vector newSongSearchResults; - std::vector paletteSearchResults; + std::vector paletteSearchResults; FixedQueue recentFile; std::vector makeInsTypeList; std::vector waveSizeList;