2023-06-15 01:04:45 -04:00
/**
* Furnace Tracker - multi - system chiptune tracker
2024-02-04 15:44:11 -05:00
* 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"
2023-07-01 14:02:12 -04:00
# 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"
2024-08-27 03:59:16 -04:00
// @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 ' ) {
2024-08-27 03:59:16 -04:00
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 ' )
2024-08-27 03:59:16 -04:00
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
}
2024-08-27 03:59:16 -04:00
2023-06-15 01:04:45 -04:00
n_i + = 1 ;
}
2024-08-27 03:59:16 -04:00
# 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 ( ) ;
2023-07-03 15:01:46 -04:00
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 ;
2023-08-15 17:37:30 -04:00
case CMDPAL_TYPE_INSTRUMENT_CHANGE :
2024-05-26 20:31:17 -04:00
hint = _ ( " Search instruments (to change to)... " ) ;
2023-08-15 17:37:30 -04:00
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 ( ) ;
2024-08-27 03:59:16 -04:00
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 + + ) {
2024-08-27 03:59:16 -04:00
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 + + ) {
2024-08-27 03:59:16 -04:00
Evaluate ( i , recentFile [ i ] . c_str ( ) ) ;
2023-06-18 22:21:16 -04:00
}
break ;
2023-07-31 14:58:38 -04:00
case CMDPAL_TYPE_INSTRUMENTS :
2023-08-15 17:37:30 -04:00
case CMDPAL_TYPE_INSTRUMENT_CHANGE :
2024-08-27 03:59:16 -04:00
Evaluate ( 0 , _ ( " - None - " ) ) ;
2023-07-31 14:58:38 -04:00
for ( int i = 0 ; i < e - > song . insLen ; i + + ) {
2024-02-04 23:20:40 -05:00
String s = fmt : : sprintf ( " %02X: %s " , i , e - > song . ins [ i ] - > name . c_str ( ) ) ;
2024-08-27 03:59:16 -04:00
Evaluate ( i + 1 , s . c_str ( ) ) ; // because over here ins=0 is 'None'
2023-07-31 14:58:38 -04:00
}
break ;
case CMDPAL_TYPE_SAMPLES :
for ( int i = 0 ; i < e - > song . sampleLen ; i + + ) {
2024-08-27 03:59:16 -04:00
Evaluate ( i , e - > song . sample [ i ] - > name . c_str ( ) ) ;
2023-07-31 14:58:38 -04:00
}
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 ) ;
2024-08-27 03:59:16 -04:00
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 " ) ) ;
2023-07-01 14:35:13 -04:00
ImGui : : CloseCurrentPopup ( ) ;
2023-06-18 22:21:16 -04:00
break ;
} ;
2024-08-27 03:59:16 -04:00
// 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
}
2023-07-03 15:38:08 -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 ] ;
2023-10-29 19:27:06 -04:00
String s = " ??? " ;
2023-06-18 22:21:16 -04:00
switch ( curPaletteType ) {
case CMDPAL_TYPE_MAIN :
2023-07-01 14:02:12 -04:00
s = guiActions [ id ] . friendlyName ;
2023-06-18 22:21:16 -04:00
break ;
case CMDPAL_TYPE_RECENT :
2023-10-29 19:05:01 -04:00
s = recentFile [ id ] . c_str ( ) ;
2023-06-18 22:21:16 -04:00
break ;
2023-07-31 14:58:38 -04:00
case CMDPAL_TYPE_INSTRUMENTS :
2023-08-15 17:37:30 -04:00
case CMDPAL_TYPE_INSTRUMENT_CHANGE :
2023-07-31 14:58:38 -04:00
if ( id = = 0 ) {
2024-05-26 20:31:17 -04:00
s = _ ( " - None - " ) ;
2023-07-31 14:58:38 -04:00
} else {
2024-02-04 23:20:40 -05:00
s = fmt : : sprintf ( " %02X: %s " , id - 1 , e - > song . ins [ id - 1 ] - > name . c_str ( ) ) ;
2023-07-31 14:58:38 -04:00
}
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 ;
} ;
2023-10-29 19:27:06 -04:00
if ( ImGui : : Selectable ( s . c_str ( ) , current ) ) {
2023-06-15 01:04:45 -04:00
curPaletteChoice = i ;
accepted = true ;
}
2023-07-12 10:14:51 -04:00
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 ;
}
2023-07-01 14:15:41 -04:00
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 ( ) ;
}
2023-07-31 14:07:10 -04:00
// 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 ) {
2023-07-01 14:15:41 -04:00
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-01 14:15:41 -04:00
} ;
}
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
}