furnace/src/gui/commandPalette.cpp

315 lines
9.4 KiB
C++
Raw Normal View History

2023-06-15 01:04:45 -04:00
/**
* Furnace Tracker - multi-system chiptune tracker
* Copyright (C) 2021-2024 tildearrow and contributors
2023-06-15 01:04:45 -04:00
*
* 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.
*/
#include "gui.h"
#include "guiConst.h"
#include "commandPalette.h"
2023-06-15 01:04:45 -04:00
#include "misc/cpp/imgui_stdlib.h"
#include <fmt/printf.h>
#include <algorithm>
2024-02-04 19:10:41 -05:00
#include <ctype.h>
2023-06-15 01:04:45 -04:00
#include "../ta-log.h"
// @TODO: when there's a tie on both within and before-needle costs, we have options.
// It's reasonable to let the original order stand, but also reasonable to favor
// minimizing the number of chars that follow afterward. Leaving this code in for now,
// but disabling until further thought/discussion.
// #define MATCH_SCORE_PREFER_LOWER_CHARS_AFTER_NEEDLE
struct MatchScore {
bool valid=true;
enum Cost { COST_BEFORE_NEEDLE, COST_WITHIN_NEEDLE, COST_AFTER_NEEDLE, COST_COUNT };
size_t costs[COST_COUNT] = {0, 0, 0};
static MatchScore INVALID() {
MatchScore score;
score.valid=false;
return score;
}
static bool IsFirstPreferable(const MatchScore& a, const MatchScore& b) {
auto PreferenceForAAmount=[&](Cost cost) {
// prefer a if lower cost
return b.costs[cost]-a.costs[cost];
};
if (a.valid && b.valid) {
int prefA;
prefA=PreferenceForAAmount(COST_WITHIN_NEEDLE);
if (prefA!=0) return prefA>0;
prefA=PreferenceForAAmount(COST_BEFORE_NEEDLE);
if (prefA!=0) return prefA>0;
#ifdef MATCH_SCORE_PREFER_LOWER_CHARS_AFTER_NEEDLE
// prefA=PreferenceForAAmount(COST_AFTER_NEEDLE);
// if (prefA!=0) return prefA>0;
#endif
return false;
} else {
return a.valid;
}
}
};
static inline MatchScore matchFuzzy(const char* haystack, const char* needle) {
MatchScore score;
2023-06-15 01:04:45 -04:00
size_t h_i=0; // haystack idx
size_t n_i=0; // needle idx
while (needle[n_i]!='\0') {
size_t cost=0;
for (; std::tolower(haystack[h_i])!=std::tolower(needle[n_i]); h_i++, cost++) {
// needle not completed, return invalid
2023-06-15 01:04:45 -04:00
if (haystack[h_i]=='\0')
return MatchScore::INVALID();
}
// contribute this run of non-matches toward pre-needle or within-needle cost
if (n_i==0) {
score.costs[MatchScore::COST_BEFORE_NEEDLE]=cost;
} else {
score.costs[MatchScore::COST_WITHIN_NEEDLE]+=cost;
2023-06-15 01:04:45 -04:00
}
2023-06-15 01:04:45 -04:00
n_i+=1;
}
#ifdef MATCH_SCORE_PREFER_LOWER_CHARS_AFTER_NEEDLE
// count the remaining chars in haystack as a tie-breaker (we won't reach this if it's a failed
// match anyway)
for (; haystack[h_i]!='\0'; h_i++, score.costs[MatchScore::COST_AFTER_NEEDLE]++) {}
#endif
score.valid=true;
return score;
2023-06-15 01:04:45 -04:00
}
void FurnaceGUI::drawPalette() {
bool accepted=false;
if (paletteFirstFrame)
ImGui::SetKeyboardFocusHere();
int width=ImGui::GetContentRegionAvail().x;
ImGui::SetNextItemWidth(width);
2023-06-15 01:04:45 -04:00
2024-05-26 20:31:17 -04:00
const char* hint=_("Search...");
2023-07-31 15:12:29 -04:00
switch (curPaletteType) {
case CMDPAL_TYPE_RECENT:
2024-05-26 20:31:17 -04:00
hint=_("Search recent files...");
2023-07-31 15:12:29 -04:00
break;
case CMDPAL_TYPE_INSTRUMENTS:
2024-05-26 20:31:17 -04:00
hint=_("Search instruments...");
2023-07-31 15:12:29 -04:00
break;
case CMDPAL_TYPE_SAMPLES:
2024-05-26 20:31:17 -04:00
hint=_("Search samples...");
2023-07-31 15:12:29 -04:00
break;
case CMDPAL_TYPE_INSTRUMENT_CHANGE:
2024-05-26 20:31:17 -04:00
hint=_("Search instruments (to change to)...");
break;
2023-08-27 13:19:26 -04:00
case CMDPAL_TYPE_ADD_CHIP:
2024-05-26 20:31:17 -04:00
hint=_("Search chip (to add)...");
2023-08-27 13:19:26 -04:00
break;
2023-07-31 15:12:29 -04:00
}
if (ImGui::InputTextWithHint("##CommandPaletteSearch",hint,&paletteQuery) || paletteFirstFrame) {
2023-06-15 01:04:45 -04:00
paletteSearchResults.clear();
std::vector<MatchScore> matchScores;
auto Evaluate=[&](int i, const char* name) {
MatchScore score=matchFuzzy(name,paletteQuery.c_str());
if (score.valid) {
paletteSearchResults.push_back(i);
matchScores.push_back(score);
}
};
2023-06-18 22:21:16 -04:00
switch (curPaletteType) {
case CMDPAL_TYPE_MAIN:
for (int i=0; i<GUI_ACTION_MAX; i++) {
if (guiActions[i].defaultBind==-1) continue; // not a bind
Evaluate(i,guiActions[i].friendlyName);
2023-06-15 01:04:45 -04:00
}
2023-06-18 22:21:16 -04:00
break;
case CMDPAL_TYPE_RECENT:
for (int i=0; i<(int)recentFile.size(); i++) {
Evaluate(i,recentFile[i].c_str());
2023-06-18 22:21:16 -04:00
}
break;
case CMDPAL_TYPE_INSTRUMENTS:
case CMDPAL_TYPE_INSTRUMENT_CHANGE:
Evaluate(0,_("- None -"));
for (int i=0; i<e->song.insLen; i++) {
String s=fmt::sprintf("%02X: %s", i, e->song.ins[i]->name.c_str());
Evaluate(i+1,s.c_str()); // because over here ins=0 is 'None'
}
break;
case CMDPAL_TYPE_SAMPLES:
for (int i=0; i<e->song.sampleLen; i++) {
Evaluate(i,e->song.sample[i]->name.c_str());
}
break;
2023-08-27 13:19:26 -04:00
case CMDPAL_TYPE_ADD_CHIP:
for (int i=0; availableSystems[i]; i++) {
int ds=availableSystems[i];
const char* sysname=getSystemName((DivSystem)ds);
Evaluate(ds,sysname);
2023-08-27 13:19:26 -04:00
}
break;
2023-06-18 22:21:16 -04:00
default:
2024-05-26 20:31:17 -04:00
logE(_("invalid command palette type"));
ImGui::CloseCurrentPopup();
2023-06-18 22:21:16 -04:00
break;
};
// sort indices by match quality
std::vector<int> sortingIndices(paletteSearchResults.size());
for (size_t i=0; i<sortingIndices.size(); ++i) sortingIndices[i]=(int)i;
std::sort(sortingIndices.begin(), sortingIndices.end(), [&](size_t a, size_t b) {
return MatchScore::IsFirstPreferable(matchScores[a], matchScores[b]);
});
// update paletteSearchResults from sorted indices (taking care not to stomp while we iterate
for (size_t i=0; i<sortingIndices.size(); ++i) sortingIndices[i]=(int)paletteSearchResults[sortingIndices[i]];
for (size_t i=0; i<sortingIndices.size(); ++i) paletteSearchResults[i]=sortingIndices[i];
2023-06-15 01:04:45 -04:00
}
ImVec2 avail=ImGui::GetContentRegionAvail();
avail.y-=ImGui::GetFrameHeightWithSpacing();
if (ImGui::BeginChild("CommandPaletteList",avail,false,0)) {
2023-06-15 01:04:45 -04:00
bool navigated=false;
if (ImGui::IsKeyPressed(ImGuiKey_UpArrow) && curPaletteChoice>0) {
curPaletteChoice-=1;
navigated=true;
}
if (ImGui::IsKeyPressed(ImGuiKey_DownArrow)) {
curPaletteChoice+=1;
navigated=true;
}
2023-08-27 13:19:26 -04:00
if (paletteSearchResults.size()>0 && curPaletteChoice<0) {
curPaletteChoice=0;
navigated=true;
}
if (curPaletteChoice>=(int)paletteSearchResults.size()) {
curPaletteChoice=paletteSearchResults.size()-1;
navigated=true;
}
2023-06-18 22:21:16 -04:00
for (int i=0; i<(int)paletteSearchResults.size(); i++) {
2023-06-15 01:04:45 -04:00
bool current=(i==curPaletteChoice);
int id=paletteSearchResults[i];
String s="???";
2023-06-18 22:21:16 -04:00
switch (curPaletteType) {
case CMDPAL_TYPE_MAIN:
s=guiActions[id].friendlyName;
2023-06-18 22:21:16 -04:00
break;
case CMDPAL_TYPE_RECENT:
s=recentFile[id].c_str();
2023-06-18 22:21:16 -04:00
break;
case CMDPAL_TYPE_INSTRUMENTS:
case CMDPAL_TYPE_INSTRUMENT_CHANGE:
if (id==0) {
2024-05-26 20:31:17 -04:00
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;
2023-08-27 13:19:26 -04:00
case CMDPAL_TYPE_ADD_CHIP:
s=getSystemName((DivSystem)id);
break;
2023-06-18 22:21:16 -04:00
default:
2024-05-26 20:31:17 -04:00
logE(_("invalid command palette type"));
2023-06-18 22:21:16 -04:00
break;
};
if (ImGui::Selectable(s.c_str(),current)) {
2023-06-15 01:04:45 -04:00
curPaletteChoice=i;
accepted=true;
}
if ((navigated || paletteFirstFrame) && current) ImGui::SetScrollHereY();
2023-06-15 01:04:45 -04:00
}
}
ImGui::EndChild();
if (!accepted) {
2023-06-18 22:21:16 -04:00
if (curPaletteChoice>=(int)paletteSearchResults.size()) {
2023-06-15 01:04:45 -04:00
curPaletteChoice=paletteSearchResults.size()-1;
}
accepted=ImGui::IsKeyPressed(ImGuiKey_Enter);
2023-06-15 01:04:45 -04:00
}
2024-05-26 20:31:17 -04:00
if (ImGui::Button(_("Cancel")) || ImGui::IsKeyPressed(ImGuiKey_Escape)) {
2023-06-15 01:04:45 -04:00
ImGui::CloseCurrentPopup();
}
// do not move this to after the resetPalette() calls!
// if they are called before and we're jumping from one palette to the next, the paletteFirstFrame won't be true at the start and the setup will not happen.
paletteFirstFrame=false;
2023-06-15 01:04:45 -04:00
if (accepted) {
2023-07-31 15:12:29 -04:00
if (paletteSearchResults.size()>0) {
int i=paletteSearchResults[curPaletteChoice];
switch (curPaletteType) {
2024-02-07 18:29:08 -05:00
case CMDPAL_TYPE_MAIN:
doAction(i);
break;
case CMDPAL_TYPE_RECENT:
openRecentFile(recentFile[i]);
break;
case CMDPAL_TYPE_INSTRUMENTS:
curIns=i-1;
break;
case CMDPAL_TYPE_SAMPLES:
curSample=i;
break;
case CMDPAL_TYPE_INSTRUMENT_CHANGE:
doChangeIns(i-1);
break;
case CMDPAL_TYPE_ADD_CHIP:
if (i!=DIV_SYSTEM_NULL) {
if (!e->addSystem((DivSystem)i)) {
showError("cannot add chip! ("+e->getLastError()+")");
} else {
MARK_MODIFIED;
}
ImGui::CloseCurrentPopup();
if (e->song.autoSystem) {
autoDetectSystem();
}
updateWindowTitle();
2023-08-27 13:19:26 -04:00
}
2024-02-07 18:29:08 -05:00
break;
default:
2024-05-26 20:31:17 -04:00
logE(_("invalid command palette type"));
2024-02-07 18:29:08 -05:00
break;
};
}
2023-07-31 15:12:29 -04:00
ImGui::CloseCurrentPopup();
2023-06-18 22:21:16 -04:00
}
2023-06-15 01:04:45 -04:00
}