diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b1acb24..2806f46 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -116,6 +116,9 @@ jobs: sudo apt-get update sudo apt-get install -y libcurl4-openssl-dev libgtk-3-dev + - name: Test string helpers + run: make test + - name: Build run: make dist diff --git a/Makefile b/Makefile index 4aeea76..3411e00 100644 --- a/Makefile +++ b/Makefile @@ -19,6 +19,11 @@ all: build .PHONY: build build: build-$(OS_NAME) @echo "Built for target: $(OS_NAME)" + +.PHONY: test +test: + $(CC) -Wall -Wextra -I. tests/string-helpers-test.c string-helpers.c -o tests/string-helpers-test + ./tests/string-helpers-test .PHONY: build-linux build-darwin build-linux: @@ -81,4 +86,4 @@ print-dists-darwin: .PHONY: clean clean: - rm -rf SGDBoop.app SGDBoop dist \ No newline at end of file + rm -rf SGDBoop.app SGDBoop dist tests/string-helpers-test diff --git a/gui-helper-gtk.c b/gui-helper-gtk.c index d3050a8..90992eb 100644 --- a/gui-helper-gtk.c +++ b/gui-helper-gtk.c @@ -1,4 +1,5 @@ #include "gui-helper.h" +#include "string-helpers.h" #include int ShowMessageBox(const char* title, const char* message) @@ -32,6 +33,7 @@ enum { typedef struct { GtkWidget* treeview; + GtkTreeModelFilter* filter; int selection; int selected_index; int tab_index; @@ -43,17 +45,69 @@ typedef struct { TabData* tab3; } TabsContext; -static void button_callback(gpointer data) +static gboolean filter_visible(GtkTreeModel* model, GtkTreeIter* iter, gpointer data) { - TabData* tab_data = (TabData*)data; - GtkWidget* treeview = tab_data->treeview; + gchar* item; + gtk_tree_model_get(model, iter, COLUMN_ITEM, &item, -1); + const char* filter = gtk_entry_get_text(GTK_ENTRY(data)); + gboolean visible = matchesFilter(item, filter); + g_free(item); + return visible; +} - GtkTreeSelection* selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(treeview)); +static int get_selected_index(TabData* data) +{ + GtkTreeSelection* selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(data->treeview)); GtkTreeModel* model; GtkTreeIter iter; - if (gtk_tree_selection_get_selected(selection, &model, &iter)) - { - gtk_tree_model_get(model, &iter, COLUMN_INDEX, &tab_data->selected_index, -1); + if (!gtk_tree_selection_get_selected(selection, &model, &iter)) { + return -1; + } + + int index; + gtk_tree_model_get(model, &iter, COLUMN_INDEX, &index, -1); + return index; +} + +static void select_index(TabData* data, int index) +{ + if (index < 0) { + return; + } + + GtkTreeModel* model = gtk_tree_view_get_model(GTK_TREE_VIEW(data->treeview)); + GtkTreeIter iter; + gboolean valid = gtk_tree_model_get_iter_first(model, &iter); + while (valid) { + int row_index; + gtk_tree_model_get(model, &iter, COLUMN_INDEX, &row_index, -1); + if (row_index == index) { + GtkTreeSelection* selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(data->treeview)); + gtk_tree_selection_select_iter(selection, &iter); + return; + } + valid = gtk_tree_model_iter_next(model, &iter); + } +} + +static void on_filter_changed(GtkSearchEntry* entry, gpointer user_data) +{ + (void)entry; + TabsContext* ctx = (TabsContext*)user_data; + TabData* tabs[] = { ctx->tab1, ctx->tab2, ctx->tab3 }; + + for (int i = 0; i < 3; i++) { + int selection = get_selected_index(tabs[i]); + gtk_tree_model_filter_refilter(tabs[i]->filter); + select_index(tabs[i], selection); + } +} + +static void button_callback(gpointer data) +{ + TabData* tab_data = (TabData*)data; + tab_data->selected_index = get_selected_index(tab_data); + if (tab_data->selected_index >= 0) { gtk_main_quit(); } } @@ -83,7 +137,7 @@ static void on_ok_button_clicked(GtkButton* button, gpointer user_data) button_callback(data); } -GtkWidget* create_treeview(const char** items, int count, int initial_selection, int* selected_index) +GtkWidget* create_treeview(const char** items, int count, int initial_selection, GtkWidget* search_entry, GtkTreeModelFilter** filter_out) { GtkListStore* store = gtk_list_store_new( 2, // 2 columns @@ -96,20 +150,15 @@ GtkWidget* create_treeview(const char** items, int count, int initial_selection, gtk_list_store_set(store, &iter, COLUMN_ITEM, items[i], COLUMN_INDEX, i, -1); } - GtkWidget* treeview = gtk_tree_view_new_with_model(GTK_TREE_MODEL(store)); - // Attached the selected index to the treeview so we can easily reference it in the button callback - g_object_set_data(G_OBJECT(treeview), "selected_index", &selected_index); + GtkTreeModel* filter = gtk_tree_model_filter_new(GTK_TREE_MODEL(store), NULL); + gtk_tree_model_filter_set_visible_func(GTK_TREE_MODEL_FILTER(filter), filter_visible, search_entry, NULL); + GtkWidget* treeview = gtk_tree_view_new_with_model(filter); + *filter_out = GTK_TREE_MODEL_FILTER(filter); gtk_tree_view_set_headers_visible(GTK_TREE_VIEW(treeview), FALSE); GtkTreeSelection* treeselection = gtk_tree_view_get_selection(GTK_TREE_VIEW(treeview)); gtk_tree_selection_set_mode(treeselection, GTK_SELECTION_SINGLE); - if (initial_selection < count && initial_selection >= 0) { - GtkTreePath* path = gtk_tree_path_new_from_indices(initial_selection, -1); - gtk_tree_selection_select_path(treeselection, path); - gtk_tree_path_free(path); - } - GtkCellRenderer* renderer = gtk_cell_renderer_text_new(); gtk_tree_view_insert_column_with_attributes( GTK_TREE_VIEW(treeview), @@ -121,6 +170,13 @@ GtkWidget* create_treeview(const char** items, int count, int initial_selection, NULL ); + if (initial_selection < count) { + TabData data = { treeview, GTK_TREE_MODEL_FILTER(filter), -1, -1, 0 }; + select_index(&data, initial_selection); + } + + g_object_unref(store); + g_object_unref(filter); return treeview; } @@ -147,6 +203,10 @@ int SelectionDialog(const char* title, int nonSteamCount, const char** nonSteamL gtk_widget_set_name(box, "box"); gtk_container_add(GTK_CONTAINER(window), box); + GtkWidget* search_entry = gtk_search_entry_new(); + gtk_entry_set_placeholder_text(GTK_ENTRY(search_entry), "Filter games..."); + gtk_box_pack_start(GTK_BOX(box), search_entry, FALSE, FALSE, 0); + // Tabs via GtkNotebook GtkWidget* notebook = gtk_notebook_new(); gtk_box_pack_start(GTK_BOX(box), notebook, TRUE, TRUE, 0); @@ -164,7 +224,8 @@ int SelectionDialog(const char* title, int nonSteamCount, const char** nonSteamL // Non-Steam apps tab TabData* nonsteam_data = g_new0(TabData, 1); nonsteam_data->tab_index = 0; - nonsteam_data->treeview = create_treeview(nonSteamList, nonSteamCount, selection, &nonsteam_data->selected_index); + nonsteam_data->selected_index = -1; + nonsteam_data->treeview = create_treeview(nonSteamList, nonSteamCount, selectionTab == 0 ? selection : -1, search_entry, &nonsteam_data->filter); GtkWidget* scroll1 = gtk_scrolled_window_new(NULL, NULL); gtk_container_add(GTK_CONTAINER(scroll1), nonsteam_data->treeview); @@ -173,7 +234,8 @@ int SelectionDialog(const char* title, int nonSteamCount, const char** nonSteamL // Mods tab TabData* mods_data = g_new0(TabData, 1); mods_data->tab_index = 1; - mods_data->treeview = create_treeview(modsList, modsCount, selection, &mods_data->selected_index); + mods_data->selected_index = -1; + mods_data->treeview = create_treeview(modsList, modsCount, selectionTab == 1 ? selection : -1, search_entry, &mods_data->filter); GtkWidget* scroll2 = gtk_scrolled_window_new(NULL, NULL); gtk_container_add(GTK_CONTAINER(scroll2), mods_data->treeview); @@ -182,7 +244,8 @@ int SelectionDialog(const char* title, int nonSteamCount, const char** nonSteamL // Steam tab TabData* steam_data = g_new0(TabData, 1); steam_data->tab_index = 2; - steam_data->treeview = create_treeview(steamList, steamCount, selection, &steam_data->selected_index); + steam_data->selected_index = -1; + steam_data->treeview = create_treeview(steamList, steamCount, selectionTab == 2 ? selection : -1, search_entry, &steam_data->filter); GtkWidget* scroll3 = gtk_scrolled_window_new(NULL, NULL); gtk_container_add(GTK_CONTAINER(scroll3), steam_data->treeview); @@ -192,6 +255,7 @@ int SelectionDialog(const char* title, int nonSteamCount, const char** nonSteamL context->tab1 = nonsteam_data; context->tab2 = mods_data; context->tab3 = steam_data; + g_signal_connect(search_entry, "search-changed", G_CALLBACK(on_filter_changed), context); // Button contianer GtkWidget* buttons = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 5); @@ -238,12 +302,17 @@ int SelectionDialog(const char* title, int nonSteamCount, const char** nonSteamL // Clean up and return TabData* final_tabdata = (TabData*)g_object_get_data(G_OBJECT(ok_button), "tabdata"); int final_selection = final_tabdata->selected_index; + int final_tab_index = final_tabdata->tab_index; + gtk_widget_destroy(window); g_free(nonsteam_data); g_free(mods_data); g_free(steam_data); + g_free(context); - if (final_tabdata->tab_index > 0) { + if (final_selection >= 0 && final_tab_index == 1) { final_selection += nonSteamCount; + } else if (final_selection >= 0 && final_tab_index == 2) { + final_selection += nonSteamCount + modsCount; } return final_selection; } diff --git a/gui-helper-mac.m b/gui-helper-mac.m index 7e3dbfe..033c5d2 100644 --- a/gui-helper-mac.m +++ b/gui-helper-mac.m @@ -2,6 +2,7 @@ // https://developer.apple.com/documentation/appkit?language=objc #include "gui-helper.h" +#include "string-helpers.h" #include #include #include @@ -50,6 +51,10 @@ @interface SGDBSelectionController : NSObject * nonSteamRows; +@property (nonatomic, strong) NSArray* modsRows; +@property (nonatomic, strong) NSArray* steamRows; - (id)initWithTitle:(const char*)title nonSteamItems:(const char**)nsItems @@ -61,8 +66,13 @@ - (id)initWithTitle:(const char*)title initialTab:(int)initialTab initialSelection:(int)initialSelection; +- (NSArray*)filteredRowsForItems:(const char**)items count:(int)count; +- (NSArray*)rowsForTable:(NSTableView*)tableView; +- (NSInteger)selectedOriginalIndexForTable:(NSTableView*)tableView; +- (void)restoreOriginalIndex:(NSInteger)index forTable:(NSTableView*)tableView; - (NSScrollView*)buildScrollingTableWithTag:(NSInteger)tag outTableView:(NSTableView**)outTableView; - (void)segmentChanged:(id)sender; +- (void)searchChanged:(id)sender; - (void)okClicked:(id)sender; - (void)cancelClicked:(id)sender; @@ -111,6 +121,19 @@ - (id)initWithTitle:(const char*)title NSView* contentView = [window contentView]; + NSSearchField* searchField = [[NSSearchField alloc] initWithFrame:NSMakeRect(10, 320, 434, 24)]; + [searchField setPlaceholderString:@"Filter games..."]; + [searchField setAutoresizingMask:(NSViewWidthSizable | NSViewMinYMargin)]; + [searchField setSendsSearchStringImmediately:YES]; + [searchField setTarget:self]; + [searchField setAction:@selector(searchChanged:)]; + self.searchField = searchField; + [contentView addSubview:searchField]; + + self.nonSteamRows = [self filteredRowsForItems:nonSteamItems count:nonSteamCount]; + self.modsRows = [self filteredRowsForItems:modsItems count:modsCount]; + self.steamRows = [self filteredRowsForItems:steamItems count:steamCount]; + // Segmented control switches between three lists NSSegmentedControl* segmentedControl = [ NSSegmentedControl segmentedControlWithLabels:@[@"Non-Steam", @"GoldSrc/Source Mods", @"Steam"] @@ -126,7 +149,7 @@ - (id)initWithTitle:(const char*)title NSTableView* nonSteamTable = nil; NSScrollView* nonSteamScroll = [self buildScrollingTableWithTag:0 outTableView:&nonSteamTable]; - [nonSteamScroll setFrame:NSMakeRect(10, 50, 434, 294)]; + [nonSteamScroll setFrame:NSMakeRect(10, 50, 434, 260)]; [nonSteamScroll setAutoresizingMask:(NSViewWidthSizable | NSViewHeightSizable)]; self.nonSteamTable = nonSteamTable; self.nonSteamScrollView = nonSteamScroll; @@ -134,7 +157,7 @@ - (id)initWithTitle:(const char*)title NSTableView* modsTable = nil; NSScrollView* modsScroll = [self buildScrollingTableWithTag:1 outTableView:&modsTable]; - [modsScroll setFrame:NSMakeRect(10, 50, 434, 294)]; + [modsScroll setFrame:NSMakeRect(10, 50, 434, 260)]; [modsScroll setAutoresizingMask:(NSViewWidthSizable | NSViewHeightSizable)]; self.modsTable = modsTable; self.modsScrollView = modsScroll; @@ -142,7 +165,7 @@ - (id)initWithTitle:(const char*)title NSTableView* steamTable = nil; NSScrollView* steamScroll = [self buildScrollingTableWithTag:2 outTableView:&steamTable]; - [steamScroll setFrame:NSMakeRect(10, 50, 434, 294)]; + [steamScroll setFrame:NSMakeRect(10, 50, 434, 260)]; [steamScroll setAutoresizingMask:(NSViewWidthSizable | NSViewHeightSizable)]; self.steamTable = steamTable; self.steamScrollView = steamScroll; @@ -186,9 +209,11 @@ - (id)initWithTitle:(const char*)title initialTable = steamTable; initialCount = steamCount; } - if (initialSelection >= 0 && initialSelection < initialCount) { - [initialTable selectRowIndexes:[NSIndexSet indexSetWithIndex:initialSelection] byExtendingSelection:NO]; - [initialTable scrollRowToVisible:initialSelection]; + NSArray* initialRows = [self rowsForTable:initialTable]; + NSUInteger initialRow = [initialRows indexOfObject:@(initialSelection)]; + if (initialSelection >= 0 && initialSelection < initialCount && initialRow != NSNotFound) { + [initialTable selectRowIndexes:[NSIndexSet indexSetWithIndex:initialRow] byExtendingSelection:NO]; + [initialTable scrollRowToVisible:initialRow]; [self.okButton setEnabled:YES]; } else { [self.okButton setEnabled:NO]; @@ -197,6 +222,73 @@ - (id)initWithTitle:(const char*)title return self; } +- (NSArray*)filteredRowsForItems:(const char**)items count:(int)count +{ + NSMutableArray* rows = [NSMutableArray array]; + const char* filter = [[self.searchField stringValue] UTF8String]; + for (int i = 0; i < count; i++) { + if (matchesFilter(items[i], filter)) { + [rows addObject:@(i)]; + } + } + return rows; +} + +- (NSArray*)rowsForTable:(NSTableView*)tableView +{ + switch ([tableView tag]) { + case 0: return self.nonSteamRows; + case 1: return self.modsRows; + case 2: return self.steamRows; + default: return @[]; + } +} + +- (NSInteger)selectedOriginalIndexForTable:(NSTableView*)tableView +{ + NSInteger row = [tableView selectedRow]; + NSArray* rows = [self rowsForTable:tableView]; + if (row < 0 || row >= (NSInteger)[rows count]) { + return -1; + } + return [rows[row] integerValue]; +} + +- (void)restoreOriginalIndex:(NSInteger)index forTable:(NSTableView*)tableView +{ + if (index < 0) { + return; + } + + NSUInteger row = [[self rowsForTable:tableView] indexOfObject:@(index)]; + if (row != NSNotFound) { + [tableView selectRowIndexes:[NSIndexSet indexSetWithIndex:row] byExtendingSelection:NO]; + [tableView scrollRowToVisible:row]; + } +} + +- (void)searchChanged:(id)sender +{ + (void)sender; + NSInteger nonSteamSelection = [self selectedOriginalIndexForTable:self.nonSteamTable]; + NSInteger modsSelection = [self selectedOriginalIndexForTable:self.modsTable]; + NSInteger steamSelection = [self selectedOriginalIndexForTable:self.steamTable]; + + self.nonSteamRows = [self filteredRowsForItems:nonSteamItems count:nonSteamCount]; + self.modsRows = [self filteredRowsForItems:modsItems count:modsCount]; + self.steamRows = [self filteredRowsForItems:steamItems count:steamCount]; + [self.nonSteamTable reloadData]; + [self.modsTable reloadData]; + [self.steamTable reloadData]; + + [self restoreOriginalIndex:nonSteamSelection forTable:self.nonSteamTable]; + [self restoreOriginalIndex:modsSelection forTable:self.modsTable]; + [self restoreOriginalIndex:steamSelection forTable:self.steamTable]; + + NSTableView* activeTable = [self tableViewForSegment:[self.segmentedControl selectedSegment]]; + [self.okButton setEnabled:([activeTable selectedRow] != -1)]; +} + // Builds a headerless, single-column table view inside a scroll view. - (NSScrollView*)buildScrollingTableWithTag:(NSInteger)tag outTableView:(NSTableView**)outTableView { @@ -253,7 +345,7 @@ - (void)okClicked:(id)sender NSInteger tabIndex = [self.segmentedControl selectedSegment]; NSTableView* activeTable = [self tableViewForSegment:tabIndex]; - NSInteger row = [activeTable selectedRow]; + NSInteger row = [self selectedOriginalIndexForTable:activeTable]; if (row < 0) { return; // nothing selected; OK should be disabled in this case anyway } @@ -280,12 +372,7 @@ - (void)cancelClicked:(id)sender - (NSInteger)numberOfRowsInTableView:(NSTableView*)tableView { - switch ([tableView tag]) { - case 0: return nonSteamCount; - case 1: return modsCount; - case 2: return steamCount; - default: return 0; - } + return [[self rowsForTable:tableView] count]; } // Custom vertically centered item + 4 padding @@ -313,10 +400,11 @@ - (NSView*)tableView:(NSTableView*)tableView viewForTableColumn:(NSTableColumn*) } const char* str = NULL; + NSInteger originalRow = [[self rowsForTable:tableView][row] integerValue]; switch ([tableView tag]) { - case 0: str = nonSteamItems[row]; break; - case 1: str = modsItems[row]; break; - case 2: str = steamItems[row]; break; + case 0: str = nonSteamItems[originalRow]; break; + case 1: str = modsItems[originalRow]; break; + case 2: str = steamItems[originalRow]; break; default: break; } cellView.textField.stringValue = NSStringFromCString(str); @@ -454,4 +542,4 @@ int macSetURLHandler() } return 0; #pragma clang diagnostic pop -} \ No newline at end of file +} diff --git a/gui-helper-win.c b/gui-helper-win.c index 1214385..2daca78 100644 --- a/gui-helper-win.c +++ b/gui-helper-win.c @@ -3,19 +3,24 @@ #include "gui-helper.h" #include "resource.h" +#include "string-helpers.h" const wchar_t* szClass = L"winWindowClass"; #define IDC_BUTTON_OK 101 #define IDC_BUTTON_CANCEL 102 +#define IDC_LIST_GAMES 103 +#define IDC_FILTER_GAMES 104 static HWND hWndList; +static HWND hWndFilter; static HWND hWndButtonOk; static HWND hWndButtonCancel; static int selected_index = -1; static HWND hWndTab; static const char** tabLists[3]; static int tabCounts[3]; +static int tabSelections[3] = { -1, -1, -1 }; static LRESULT CALLBACK WndProc(HWND hWnd, UINT Msg, WPARAM wParam, LPARAM lParam) @@ -48,13 +53,23 @@ static LRESULT CALLBACK WndProc(HWND hWnd, UINT Msg, WPARAM wParam, LPARAM lPara tie.pszText = "Steam"; TabCtrl_InsertItem(hWndTab, 2, &tie); + hWndFilter = CreateWindowExA( + WS_EX_CLIENTEDGE, + WC_EDIT, NULL, + ES_AUTOHSCROLL | WS_CHILD | WS_TABSTOP | WS_VISIBLE, + 0, 0, 100, 25, + hWnd, (HMENU)IDC_FILTER_GAMES, GetModuleHandle(NULL), NULL + ); + SendMessage(hWndFilter, WM_SETFONT, (WPARAM)hfFont, 0); + SendMessageW(hWndFilter, EM_SETCUEBANNER, TRUE, (LPARAM)L"Filter games..."); + // ListBox hWndList = CreateWindowExA( WS_OVERLAPPED, // no style to look like it's inside tabs WC_LISTBOX, NULL, LBS_NOTIFY | WS_CHILD | WS_VISIBLE | WS_VSCROLL, 0, 0, 100, 100, - hWnd, NULL, GetModuleHandle(NULL), NULL + hWnd, (HMENU)IDC_LIST_GAMES, GetModuleHandle(NULL), NULL ); SendMessage(hWndList, WM_SETFONT, (WPARAM)hfFont, 0); @@ -93,8 +108,10 @@ static LRESULT CALLBACK WndProc(HWND hWnd, UINT Msg, WPARAM wParam, LPARAM lPara GetClientRect(hWndTab, &rcTab); TabCtrl_AdjustRect(hWndTab, FALSE, &rcTab); + SetWindowPos(hWndFilter, NULL, rcTab.left + 10, rcTab.top + 10, rcTab.right - rcTab.left - 20, 24, SWP_NOZORDER); + // List inside tab - SetWindowPos(hWndList, NULL, rcTab.left + 10, rcTab.top + 10, rcTab.right - rcTab.left, rcTab.bottom - rcTab.top, SWP_NOZORDER); + SetWindowPos(hWndList, NULL, rcTab.left + 10, rcTab.top + 44, rcTab.right - rcTab.left - 20, rcTab.bottom - rcTab.top - 54, SWP_NOZORDER); // OK and Cancel buttons SetWindowPos(hWndButtonOk, NULL, rcClient.right - (80 * 2), rcClient.bottom - 45, 70, 30, SWP_NOZORDER); @@ -113,11 +130,20 @@ static LRESULT CALLBACK WndProc(HWND hWnd, UINT Msg, WPARAM wParam, LPARAM lPara } case WM_COMMAND: - switch (HIWORD(wParam)) + if (LOWORD(wParam) == IDC_LIST_GAMES && HIWORD(wParam) == LBN_SELCHANGE) { - case LBN_SELCHANGE: + LRESULT row = SendMessage(hWndList, LB_GETCURSEL, 0, 0); + if (row != LB_ERR) + { + int tabIndex = TabCtrl_GetCurSel(hWndTab); + tabSelections[tabIndex] = (int)SendMessage(hWndList, LB_GETITEMDATA, row, 0); EnableWindow(hWndButtonOk, TRUE); - break; + } + } + + if (LOWORD(wParam) == IDC_FILTER_GAMES && HIWORD(wParam) == EN_CHANGE) + { + PopulateListBox(TabCtrl_GetCurSel(hWndTab)); } switch (LOWORD(wParam)) @@ -127,7 +153,7 @@ static LRESULT CALLBACK WndProc(HWND hWnd, UINT Msg, WPARAM wParam, LPARAM lPara LRESULT res = SendMessage(hWndList, LB_GETCURSEL, (WPARAM)NULL, (LPARAM)NULL); if (res != LB_ERR) { - selected_index = res; + selected_index = (int)SendMessage(hWndList, LB_GETITEMDATA, res, 0); int tabIndex = TabCtrl_GetCurSel(hWndTab); for (int i = 0; i < tabIndex; i++) { @@ -181,6 +207,10 @@ int SelectionDialog(const char* title, int nonSteamCount, const char** nonSteamL tabLists[0] = nonSteamList; tabLists[1] = modsList; tabLists[2] = steamList; + selected_index = -1; + for (int i = 0; i < 3; i++) { + tabSelections[i] = -1; + } MSG Msg; HWND hWnd; @@ -233,12 +263,17 @@ int SelectionDialog(const char* title, int nonSteamCount, const char** nonSteamL int tabSize = sizeof(tabCounts) / sizeof(tabCounts[0]); for (tabIndex = 0; tabIndex < tabSize; tabIndex++) { - if (tabCounts[tabIndex] < selection) { + if (selection >= tabCounts[tabIndex]) { selection -= tabCounts[tabIndex]; } else { break; } } + if (tabIndex >= tabSize) { + tabIndex = 0; + selection = -1; + } + tabSelections[tabIndex] = selection; PopulateListBoxWithSelection(tabIndex, selection); // Show window and get all events @@ -257,25 +292,34 @@ int SelectionDialog(const char* title, int nonSteamCount, const char** nonSteamL // Load values into current ListBox with a highlighted option void PopulateListBoxWithSelection(int tabIndex, int selection) { + char filter[256]; + GetWindowTextA(hWndFilter, filter, sizeof(filter)); + SendMessageW(hWndList, LB_RESETCONTENT, 0, 0); TabCtrl_SetCurSel(hWndTab, tabIndex); for (int i = 0; i < tabCounts[tabIndex]; ++i) { + if (!matchesFilter(tabLists[tabIndex][i], filter)) { + continue; + } + wchar_t* unicode = ConvertStringToUnicode(tabLists[tabIndex][i]); - SendMessageW(hWndList, LB_ADDSTRING, 0, (LPARAM)unicode); + LRESULT row = SendMessageW(hWndList, LB_ADDSTRING, 0, (LPARAM)unicode); + SendMessage(hWndList, LB_SETITEMDATA, row, i); + if (selection == i) { + SendMessage(hWndList, LB_SETCURSEL, row, 0); + } free(unicode); } - if (selection > -1) { - SendMessageW(hWndList, LB_SETCURSEL, selection, 0); - } + EnableWindow(hWndButtonOk, SendMessage(hWndList, LB_GETCURSEL, 0, 0) != LB_ERR); } -// Load valuies into current ListBox without a highlighted option +// Load values into the current ListBox while preserving its selection void PopulateListBox(int tabIndex) { - PopulateListBoxWithSelection(tabIndex, -1); + PopulateListBoxWithSelection(tabIndex, tabSelections[tabIndex]); } @@ -286,4 +330,4 @@ wchar_t* ConvertStringToUnicode(const char* string) wchar_t* unicodeString = malloc(unicodeStringLength * sizeof(wchar_t)); MultiByteToWideChar(CP_UTF8, 0, string, -1, unicodeString, unicodeStringLength); return unicodeString; -} \ No newline at end of file +} diff --git a/string-helpers.c b/string-helpers.c index 1b1887e..e167ce0 100644 --- a/string-helpers.c +++ b/string-helpers.c @@ -165,6 +165,32 @@ int strcmp_i(const char * a, const char * b) } } +int matchesFilter(const char* value, const char* filter) +{ + if (value == NULL || filter == NULL) return 0; + if (*filter == '\0') return 1; + + for (; *value != '\0'; value++) + { + const char* valueChar = value; + const char* filterChar = filter; + + while ( + *valueChar != '\0' + && *filterChar != '\0' + && tolower((unsigned char)*valueChar) == tolower((unsigned char)*filterChar) + ) + { + valueChar++; + filterChar++; + } + + if (*filterChar == '\0') return 1; + } + + return 0; +} + // Custom memmem // https://stackoverflow.com/a/52989329 const unsigned char* sgdb_memmem(const void* haystack, size_t haystack_len, diff --git a/string-helpers.h b/string-helpers.h index 0984b64..33e286e 100644 --- a/string-helpers.h +++ b/string-helpers.h @@ -22,6 +22,9 @@ char* strreplace(char*, const char*, const char*); // https://stackoverflow.com/a/5820991/16642426 int strcmp_i(const char*, const char*); +// Case-insensitive substring match. An empty filter matches every string. +int matchesFilter(const char*, const char*); + // Custom memmem // https://stackoverflow.com/a/52989329 -const unsigned char* sgdb_memmem(const void*, size_t, const void* const, const size_t); \ No newline at end of file +const unsigned char* sgdb_memmem(const void*, size_t, const void* const, const size_t); diff --git a/tests/string-helpers-test.c b/tests/string-helpers-test.c new file mode 100644 index 0000000..fc3220b --- /dev/null +++ b/tests/string-helpers-test.c @@ -0,0 +1,14 @@ +#include + +#include "string-helpers.h" + +int main(void) +{ + assert(matchesFilter("Half-Life 2", "") == 1); + assert(matchesFilter("Half-Life 2", "half") == 1); + assert(matchesFilter("Half-Life 2", "LIFE 2") == 1); + assert(matchesFilter("Half-Life 2", "Portal") == 0); + assert(matchesFilter("", "game") == 0); + + return 0; +}