From 9dc7e0871c2581ab3f67f5234d47a51eba4d7239 Mon Sep 17 00:00:00 2001
From: Angelo Haller <angelo@szanni.org>
Date: Sun, 9 Aug 2020 13:13:38 -0500
Subject: [PATCH 1/3] Add table column IDs matching the column add order/index.

To be able to identify table columns individually use the order
that columns have been added to the table as the ID.
First added column: 0, second: 1, ...
---
 darwin/tablecolumn.m | 28 +++++++++++++++++++++-------
 1 file changed, 21 insertions(+), 7 deletions(-)

diff --git a/darwin/tablecolumn.m b/darwin/tablecolumn.m
index 5038cc6bd..d2b9bcb29 100644
--- a/darwin/tablecolumn.m
+++ b/darwin/tablecolumn.m
@@ -584,6 +584,7 @@ void uiTableAppendTextColumn(uiTable *t, const char *name, int textModelColumn,
 	struct textColumnCreateParams p;
 	uiprivTableColumn *col;
 	NSString *str;
+	NSString *ident;
 
 	memset(&p, 0, sizeof (struct textColumnCreateParams));
 	p.t = t;
@@ -597,8 +598,9 @@ void uiTableAppendTextColumn(uiTable *t, const char *name, int textModelColumn,
 	else
 		p.textParams = uiprivDefaultTextColumnOptionalParams;
 
+	ident = [@([[t->tv tableColumns] count]) stringValue];
+	col = [[uiprivTextImageCheckboxTableColumn alloc] initWithIdentifier:ident params:&p];
 	str = [NSString stringWithUTF8String:name];
-	col = [[uiprivTextImageCheckboxTableColumn alloc] initWithIdentifier:str params:&p];
 	[col setTitle:str];
 	[t->tv addTableColumn:col];
 }
@@ -608,6 +610,7 @@ void uiTableAppendImageColumn(uiTable *t, const char *name, int imageModelColumn
 	struct textColumnCreateParams p;
 	uiprivTableColumn *col;
 	NSString *str;
+	NSString *ident;
 
 	memset(&p, 0, sizeof (struct textColumnCreateParams));
 	p.t = t;
@@ -616,8 +619,9 @@ void uiTableAppendImageColumn(uiTable *t, const char *name, int imageModelColumn
 	p.makeImageView = YES;
 	p.imageModelColumn = imageModelColumn;
 
+	ident = [@([[t->tv tableColumns] count]) stringValue];
+	col = [[uiprivTextImageCheckboxTableColumn alloc] initWithIdentifier:ident params:&p];
 	str = [NSString stringWithUTF8String:name];
-	col = [[uiprivTextImageCheckboxTableColumn alloc] initWithIdentifier:str params:&p];
 	[col setTitle:str];
 	[t->tv addTableColumn:col];
 }
@@ -627,6 +631,7 @@ void uiTableAppendImageTextColumn(uiTable *t, const char *name, int imageModelCo
 	struct textColumnCreateParams p;
 	uiprivTableColumn *col;
 	NSString *str;
+	NSString *ident;
 
 	memset(&p, 0, sizeof (struct textColumnCreateParams));
 	p.t = t;
@@ -643,8 +648,9 @@ void uiTableAppendImageTextColumn(uiTable *t, const char *name, int imageModelCo
 	p.makeImageView = YES;
 	p.imageModelColumn = imageModelColumn;
 
+	ident = [@([[t->tv tableColumns] count]) stringValue];
+	col = [[uiprivTextImageCheckboxTableColumn alloc] initWithIdentifier:ident params:&p];
 	str = [NSString stringWithUTF8String:name];
-	col = [[uiprivTextImageCheckboxTableColumn alloc] initWithIdentifier:str params:&p];
 	[col setTitle:str];
 	[t->tv addTableColumn:col];
 }
@@ -654,6 +660,7 @@ void uiTableAppendCheckboxColumn(uiTable *t, const char *name, int checkboxModel
 	struct textColumnCreateParams p;
 	uiprivTableColumn *col;
 	NSString *str;
+	NSString *ident;
 
 	memset(&p, 0, sizeof (struct textColumnCreateParams));
 	p.t = t;
@@ -663,8 +670,9 @@ void uiTableAppendCheckboxColumn(uiTable *t, const char *name, int checkboxModel
 	p.checkboxModelColumn = checkboxModelColumn;
 	p.checkboxEditableModelColumn = checkboxEditableModelColumn;
 
+	ident = [@([[t->tv tableColumns] count]) stringValue];
+	col = [[uiprivTextImageCheckboxTableColumn alloc] initWithIdentifier:ident params:&p];
 	str = [NSString stringWithUTF8String:name];
-	col = [[uiprivTextImageCheckboxTableColumn alloc] initWithIdentifier:str params:&p];
 	[col setTitle:str];
 	[t->tv addTableColumn:col];
 }
@@ -674,6 +682,7 @@ void uiTableAppendCheckboxTextColumn(uiTable *t, const char *name, int checkboxM
 	struct textColumnCreateParams p;
 	uiprivTableColumn *col;
 	NSString *str;
+	NSString *ident;
 
 	memset(&p, 0, sizeof (struct textColumnCreateParams));
 	p.t = t;
@@ -691,8 +700,9 @@ void uiTableAppendCheckboxTextColumn(uiTable *t, const char *name, int checkboxM
 	p.checkboxModelColumn = checkboxModelColumn;
 	p.checkboxEditableModelColumn = checkboxEditableModelColumn;
 
+	ident = [@([[t->tv tableColumns] count]) stringValue];
+	col = [[uiprivTextImageCheckboxTableColumn alloc] initWithIdentifier:ident params:&p];
 	str = [NSString stringWithUTF8String:name];
-	col = [[uiprivTextImageCheckboxTableColumn alloc] initWithIdentifier:str params:&p];
 	[col setTitle:str];
 	[t->tv addTableColumn:col];
 }
@@ -701,9 +711,11 @@ void uiTableAppendProgressBarColumn(uiTable *t, const char *name, int progressMo
 {
 	uiprivTableColumn *col;
 	NSString *str;
+	NSString *ident;
 
+	ident = [@([[t->tv tableColumns] count]) stringValue];
+	col = [[uiprivProgressBarTableColumn alloc] initWithIdentifier:ident table:t model:t->m modelColumn:progressModelColumn];
 	str = [NSString stringWithUTF8String:name];
-	col = [[uiprivProgressBarTableColumn alloc] initWithIdentifier:str table:t model:t->m modelColumn:progressModelColumn];
 	[col setTitle:str];
 	[t->tv addTableColumn:col];
 }
@@ -712,9 +724,11 @@ void uiTableAppendButtonColumn(uiTable *t, const char *name, int buttonModelColu
 {
 	uiprivTableColumn *col;
 	NSString *str;
+	NSString *ident;
 
+	ident = [@([[t->tv tableColumns] count]) stringValue];
+	col = [[uiprivButtonTableColumn alloc] initWithIdentifier:ident table:t model:t->m modelColumn:buttonModelColumn editableColumn:buttonClickableModelColumn];
 	str = [NSString stringWithUTF8String:name];
-	col = [[uiprivButtonTableColumn alloc] initWithIdentifier:str table:t model:t->m modelColumn:buttonModelColumn editableColumn:buttonClickableModelColumn];
 	[col setTitle:str];
 	[t->tv addTableColumn:col];
 }

From b673b26f5866766e860b4ad1d990be1c1867684d Mon Sep 17 00:00:00 2001
From: Angelo Haller <angelo@szanni.org>
Date: Thu, 17 Sep 2020 20:47:48 -0500
Subject: [PATCH 2/3] Add new type uiSort for sorting related operations.

uiSort:
uiSortNone
uiSortAscending
uiSortDescending
---
 ui.h | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/ui.h b/ui.h
index 40aea9498..36706f24c 100644
--- a/ui.h
+++ b/ui.h
@@ -1255,6 +1255,12 @@ _UI_EXTERN uiTableValue *uiNewTableValueColor(double r, double g, double b, doub
 // TODO define whether all this, for both uiTableValue and uiAttribute, is undefined behavior or a caught error
 _UI_EXTERN void uiTableValueColor(const uiTableValue *v, double *r, double *g, double *b, double *a);
 
+_UI_ENUM(uiSort) {
+	uiSortNone,
+	uiSortAscending,
+	uiSortDescending
+};
+
 // uiTableModel is an object that provides the data for a uiTable.
 // This data is returned via methods you provide in the
 // uiTableModelHandler struct.

From 844da8d3122472d32ef09b14bfd7c5b5123a834b Mon Sep 17 00:00:00 2001
From: Angelo Haller <angelo@szanni.org>
Date: Mon, 8 Jul 2019 16:53:50 -0500
Subject: [PATCH 3/3] Add new API for sort related operations.

Add functions to get/set table header sort indicators:
uiTableHeaderSortIndicator
uiTableHeaderSetSortIndicator

Add callback setter for table header clicks:
uiTableHeaderOnClicked
---
 darwin/table.h    |  2 ++
 darwin/table.m    | 49 ++++++++++++++++++++++++++++++++++
 test/page16.c     | 17 ++++++++++++
 ui.h              | 16 +++++++++++
 unix/table.c      | 59 +++++++++++++++++++++++++++++++++++++++++
 windows/table.cpp | 67 ++++++++++++++++++++++++++++++++++++++++++++++-
 windows/table.hpp |  2 ++
 7 files changed, 211 insertions(+), 1 deletion(-)

diff --git a/darwin/table.h b/darwin/table.h
index 4146ab71a..edca49f45 100644
--- a/darwin/table.h
+++ b/darwin/table.h
@@ -16,6 +16,8 @@ struct uiTable {
 	uiprivScrollViewData *d;
 	int backgroundColumn;
 	uiTableModel *m;
+	void (*headerOnClicked)(uiTable *, int, void *);
+	void *headerOnClickedData;
 };
 
 // tablecolumn.m
diff --git a/darwin/table.m b/darwin/table.m
index 1bdc7f8da..a13bfd40a 100644
--- a/darwin/table.m
+++ b/darwin/table.m
@@ -16,6 +16,7 @@ @interface uiprivTableView : NSTableView {
 	uiTableModel *uiprivM;
 }
 - (id)initWithFrame:(NSRect)r uiprivT:(uiTable *)t uiprivM:(uiTableModel *)m;
+- (uiTable *)uiTable;
 @end
 
 @implementation uiprivTableView
@@ -30,6 +31,11 @@ - (id)initWithFrame:(NSRect)r uiprivT:(uiTable *)t uiprivM:(uiTableModel *)m
 	return self;
 }
 
+- (uiTable *)uiTable
+{
+	return self->uiprivT;
+}
+
 // TODO is this correct for overflow scrolling?
 static void setBackgroundColor(uiprivTableView *t, NSTableRowView *rv, NSInteger row)
 {
@@ -88,6 +94,12 @@ - (void)tableView:(NSTableView *)tv didAddRowView:(NSTableRowView *)rv forRow:(N
 	setBackgroundColor((uiprivTableView *) tv, rv, row);
 }
 
+- (void)tableView:(uiprivTableView *)tv didClickTableColumn:(NSTableColumn *) tc
+{
+	uiTable *t = [tv uiTable];
+	t->headerOnClicked(t, [[tc identifier] intValue], t->headerOnClickedData);
+}
+
 @end
 
 uiTableModel *uiNewTableModel(uiTableModelHandler *mh)
@@ -170,6 +182,17 @@ static void uiTableDestroy(uiControl *c)
 	uiFreeControl(uiControl(t));
 }
 
+void uiTableHeaderOnClicked(uiTable *t, void (*f)(uiTable *, int, void *), void *data)
+{
+	t->headerOnClicked = f;
+	t->headerOnClickedData = data;
+}
+
+static void defaultHeaderOnClicked(uiTable *table, int column, void *data)
+{
+	// do nothing
+}
+
 uiTable *uiNewTable(uiTableParams *p)
 {
 	uiTable *t;
@@ -209,6 +232,8 @@ static void uiTableDestroy(uiControl *c)
 	sp.VScroll = YES;
 	t->sv = uiprivMkScrollView(&sp, &(t->d));
 
+	uiTableHeaderOnClicked(t, defaultHeaderOnClicked, NULL);
+
 	// TODO WHY DOES THIS REMOVE ALL GRAPHICAL GLITCHES?
 	// I got the idea from http://jwilling.com/blog/optimized-nstableview-scrolling/ but that was on an unrelated problem I didn't seem to have (although I have small-ish tables to start with)
 	// I don't get layer-backing... am I supposed to layer-back EVERYTHING manually? I need to check Interface Builder again...
@@ -216,3 +241,27 @@ static void uiTableDestroy(uiControl *c)
 
 	return t;
 }
+
+uiSort uiTableHeaderSortIndicator(uiTable *t, int lcol)
+{
+	NSTableColumn *tc = [t->tv tableColumnWithIdentifier:[@(lcol) stringValue]];
+	NSString *si = [[t->tv indicatorImageInTableColumn:tc] name];
+	if ([si isEqualToString:@"NSAscendingSortIndicator"])
+		return uiSortAscending;
+	else if ([si isEqualToString:@"NSDescendingSortIndicator"])
+		return uiSortDescending;
+	return uiSortNone;
+}
+
+void uiTableHeaderSetSortIndicator(uiTable *t, int lcol, uiSort order)
+{
+	NSTableColumn *tc = [t->tv tableColumnWithIdentifier:[@(lcol) stringValue]];
+	NSImage *img;
+	if (order == uiSortAscending)
+		img = [NSImage imageNamed:@"NSAscendingSortIndicator"];
+	else if (order == uiSortDescending)
+		img = [NSImage imageNamed:@"NSDescendingSortIndicator"];
+	else
+		img = nil;
+	[t->tv setIndicatorImage:img inTableColumn:tc];
+}
diff --git a/test/page16.c b/test/page16.c
index f28ba3c7b..4a97ba000 100644
--- a/test/page16.c
+++ b/test/page16.c
@@ -100,6 +100,21 @@ static void modelSetCellValue(uiTableModelHandler *mh, uiTableModel *m, int row,
 
 static uiTableModel *m;
 
+static void headerOnClicked(uiTable *t, int col, void *data)
+{
+	static int prev = 0;
+
+	if (prev != col)
+		uiTableHeaderSetSortIndicator(t, prev, uiSortNone);
+
+	if (uiTableHeaderSortIndicator(t, col) == uiSortAscending)
+		uiTableHeaderSetSortIndicator(t, col, uiSortDescending);
+	else
+		uiTableHeaderSetSortIndicator(t, col, uiSortAscending);
+
+	prev = col;
+}
+
 uiBox *makePage16(void)
 {
 	uiBox *page16;
@@ -152,6 +167,8 @@ uiBox *makePage16(void)
 	uiTableAppendProgressBarColumn(t, "Progress Bar",
 		8);
 
+	uiTableHeaderOnClicked(t, headerOnClicked, NULL);
+
 	return page16;
 }
 
diff --git a/ui.h b/ui.h
index 36706f24c..0bdf380db 100644
--- a/ui.h
+++ b/ui.h
@@ -1464,6 +1464,22 @@ _UI_EXTERN void uiTableAppendButtonColumn(uiTable *t,
 // uiNewTable() creates a new uiTable with the specified parameters.
 _UI_EXTERN uiTable *uiNewTable(uiTableParams *params);
 
+// uiTableHeaderSetSortIndicator() sets the sort indicator of the table
+// header to display an appropriate arrow on the column header
+_UI_EXTERN void uiTableHeaderSetSortIndicator(uiTable *t,
+	int column,
+	uiSort order);
+
+// uiTableHeaderSortIndicator returns the sort indicator of the specified
+// column
+_UI_EXTERN uiSort uiTableHeaderSortIndicator(uiTable *t, int column);
+
+// uiTableHeaderOnClicked() sets a callback function to be called
+// when a table column header is clicked
+_UI_EXTERN void uiTableHeaderOnClicked(uiTable *t,
+	void (*f)(uiTable *table, int column, void *data),
+	void *data);
+
 #ifdef __cplusplus
 }
 #endif
diff --git a/unix/table.c b/unix/table.c
index e29ada07c..7fb7f2db3 100644
--- a/unix/table.c
+++ b/unix/table.c
@@ -18,6 +18,8 @@ struct uiTable {
 	// TODO document this properly
 	GHashTable *indeterminatePositions;
 	guint indeterminateTimer;
+	void (*headerOnClicked)(uiTable *, int, void *);
+	void *headerOnClickedData;
 };
 
 // use the same size as GtkFileChooserWidget's treeview
@@ -327,6 +329,59 @@ static void buttonColumnClicked(GtkCellRenderer *r, gchar *pathstr, gpointer dat
 	onEdited(p->m, p->modelColumn, pathstr, NULL, NULL);
 }
 
+uiSort uiTableHeaderSortIndicator(uiTable *t, int lcol)
+{
+	GtkTreeViewColumn *c = gtk_tree_view_get_column(t->tv, lcol);
+
+	if (c == NULL || gtk_tree_view_column_get_sort_indicator(c) == FALSE)
+		return uiSortNone;
+
+	if (gtk_tree_view_column_get_sort_order(c) == GTK_SORT_ASCENDING)
+		return uiSortAscending;
+	else
+		return uiSortDescending;
+}
+
+void uiTableHeaderSetSortIndicator(uiTable *t, int lcol, uiSort order)
+{
+	GtkTreeViewColumn *c = gtk_tree_view_get_column(t->tv, lcol);
+
+	if (c == NULL)
+		return;
+
+	if (order == uiSortNone) {
+		gtk_tree_view_column_set_sort_indicator(c, FALSE);
+		return;
+	}
+
+	gtk_tree_view_column_set_sort_indicator(c, TRUE);
+	if (order == uiSortAscending)
+		gtk_tree_view_column_set_sort_order(c, GTK_SORT_ASCENDING);
+	else
+		gtk_tree_view_column_set_sort_order(c, GTK_SORT_DESCENDING);
+}
+
+void uiTableHeaderOnClicked(uiTable *t, void (*f)(uiTable *, int, void *), void *data)
+{
+	t->headerOnClicked = f;
+	t->headerOnClickedData = data;
+}
+
+static void defaultHeaderOnClicked(uiTable *table, int column, void *data)
+{
+	// do nothing
+}
+
+static void headerOnClicked(GtkTreeViewColumn *c, gpointer data)
+{
+	guint i;
+	uiTable *t = uiTable(data);
+
+	for (i = 0; i < gtk_tree_view_get_n_columns(t->tv); ++i)
+		if (gtk_tree_view_get_column(t->tv, i) == c)
+			t->headerOnClicked(t, i, t->headerOnClickedData);
+}
+
 static GtkTreeViewColumn *addColumn(uiTable *t, const char *name)
 {
 	GtkTreeViewColumn *c;
@@ -334,6 +389,8 @@ static GtkTreeViewColumn *addColumn(uiTable *t, const char *name)
 	c = gtk_tree_view_column_new();
 	gtk_tree_view_column_set_resizable(c, TRUE);
 	gtk_tree_view_column_set_title(c, name);
+	gtk_tree_view_column_set_clickable(c, 1);
+	g_signal_connect(c, "clicked", G_CALLBACK(headerOnClicked), t);
 	gtk_tree_view_append_column(t->tv, c);
 	return c;
 }
@@ -520,5 +577,7 @@ uiTable *uiNewTable(uiTableParams *p)
 	t->indeterminatePositions = g_hash_table_new_full(rowcolHash, rowcolEqual,
 		uiprivFree, uiprivFree);
 
+	uiTableHeaderOnClicked(t, defaultHeaderOnClicked, NULL);
+
 	return t;
 }
diff --git a/windows/table.cpp b/windows/table.cpp
index 85a51c1a3..143d1a04a 100644
--- a/windows/table.cpp
+++ b/windows/table.cpp
@@ -230,6 +230,59 @@ int uiprivTableProgress(uiTable *t, int item, int subitem, int modelColumn, LONG
 	return progress;
 }
 
+void uiTableHeaderSetSortIndicator(uiTable *t, int column, uiSort order)
+{
+	HWND lvhdr;
+	int fmt;
+
+	if (order == uiSortAscending)
+		fmt = HDF_SORTUP;
+	else if (order == uiSortDescending)
+		fmt = HDF_SORTDOWN;
+	else
+		fmt = 0;
+
+	lvhdr = ListView_GetHeader(t->hwnd);
+	if (lvhdr) {
+		HDITEM hdri = {};
+		hdri.mask = HDI_FORMAT;
+		if (Header_GetItem(lvhdr, column, &hdri)) {
+			hdri.fmt &= ~(HDF_SORTUP | HDF_SORTDOWN);
+			hdri.fmt |= fmt;
+			Header_SetItem(lvhdr, column, &hdri);
+		}
+	}
+}
+
+uiSort uiTableHeaderSortIndicator(uiTable *t, int column)
+{
+	HWND lvhdr;
+
+	lvhdr = ListView_GetHeader(t->hwnd);
+	if (lvhdr) {
+		HDITEM hdri = {};
+		hdri.mask = HDI_FORMAT;
+		if (Header_GetItem(lvhdr, column, &hdri)) {
+			if (hdri.fmt & HDF_SORTUP)
+				return uiSortAscending;
+			if (hdri.fmt & HDF_SORTDOWN)
+				return uiSortDescending;
+		}
+	}
+	return uiSortNone;
+}
+
+void uiTableHeaderOnClicked(uiTable *t, void (*f)(uiTable *table, int column, void *data), void *data)
+{
+	t->headerOnClicked = f;
+	t->headerOnClickedData = data;
+}
+
+static void defaultHeaderOnClicked(uiTable *table, int column, void *data)
+{
+	// do nothing
+}
+
 // TODO properly integrate compound statements
 static BOOL onWM_NOTIFY(uiControl *c, HWND hwnd, NMHDR *nmhdr, LRESULT *lResult)
 {
@@ -310,9 +363,20 @@ static BOOL onWM_NOTIFY(uiControl *c, HWND hwnd, NMHDR *nmhdr, LRESULT *lResult)
 			}
 			return FALSE;
 		}
+	case LVN_COLUMNCLICK:
+		{
+			NMLISTVIEW *nm = (NMLISTVIEW *) nmhdr;
+
+			hr = uiprivTableFinishEditingText(t);
+			if (hr != S_OK) {
+				// TODO
+				return FALSE;
+			}
+			t->headerOnClicked(t, nm->iSubItem, t->headerOnClickedData);
+			return TRUE;
+		}
 	// the real list view accepts changes when scrolling or clicking column headers
 	case LVN_BEGINSCROLL:
-	case LVN_COLUMNCLICK:
 		hr = uiprivTableFinishEditingText(t);
 		if (hr != S_OK) {
 			// TODO
@@ -490,6 +554,7 @@ uiTable *uiNewTable(uiTableParams *p)
 	t->columns = new std::vector<uiprivTableColumnParams *>;
 	t->model = p->Model;
 	t->backgroundColumn = p->RowBackgroundColorModelColumn;
+	uiTableHeaderOnClicked(t, defaultHeaderOnClicked, NULL);
 
 	// WS_CLIPCHILDREN is here to prevent drawing over the edit box used for editing text
 	t->hwnd = uiWindowsEnsureCreateControlHWND(WS_EX_CLIENTEDGE,
diff --git a/windows/table.hpp b/windows/table.hpp
index 71946d623..b5297ae8a 100644
--- a/windows/table.hpp
+++ b/windows/table.hpp
@@ -40,6 +40,8 @@ struct uiTable {
 	HWND edit;
 	int editedItem;
 	int editedSubitem;
+	void (*headerOnClicked)(uiTable *, int, void *);
+	void *headerOnClickedData;
 };
 extern int uiprivTableProgress(uiTable *t, int item, int subitem, int modelColumn, LONG *pos);