ui/winui/table.cpp

changeset 0
2483f517c562
child 3
f154867f54dc
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ui/winui/table.cpp	Sun Jan 21 16:30:18 2024 +0100
@@ -0,0 +1,597 @@
+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright 2023 Olaf Wintermann. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *   1. Redistributions of source code must retain the above copyright
+ *      notice, this list of conditions and the following disclaimer.
+ *
+ *   2. Redistributions in binary form must reproduce the above copyright
+ *      notice, this list of conditions and the following disclaimer in the
+ *      documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "pch.h"
+
+#include "table.h"
+#include "container.h"
+#include "util.h"
+#include "icons.h"
+
+#include "../common/context.h"
+#include "../common/object.h"
+#include "../common/types.h"
+
+#include <winrt/Microsoft.UI.Xaml.Data.h>
+#include <winrt/Microsoft.UI.Xaml.Media.h>
+#include <winrt/Microsoft.UI.Xaml.Input.h>
+#include <winrt/Windows.UI.Core.h>
+#include <winrt/Windows.ApplicationModel.h>
+#include <winrt/Windows.ApplicationModel.DataTransfer.h>
+
+using namespace winrt;
+using namespace Microsoft::UI::Xaml;
+using namespace Microsoft::UI::Xaml::Controls;
+using namespace Windows::UI::Xaml::Interop;
+using namespace winrt::Windows::Foundation;
+using namespace winrt::Microsoft::UI::Xaml::Controls::Primitives;
+using namespace winrt::Microsoft::UI::Xaml::Media;
+using namespace winrt::Windows::UI::Xaml::Input;
+
+static UINT ui_double_click_time = GetDoubleClickTime();
+
+extern "C" void reg_table_destructor(UiContext * ctx, UiTable * table) {
+	// TODO:
+}
+
+UIEXPORT UIWIDGET ui_table_create(UiObject* obj, UiListArgs args) {
+	if (!args.model) {
+		return nullptr;
+	}
+
+	UiObject* current = uic_current_obj(obj);
+
+	// create widgets and wrapper obj
+	ScrollViewer scrollW = ScrollViewer();
+	Grid grid = Grid();
+	scrollW.Content(grid);
+	UiTable* uitable = new UiTable(obj, scrollW, grid);
+	reg_table_destructor(current->ctx, uitable);
+	
+	uitable->getvalue = args.model->getvalue ? args.model->getvalue : args.getvalue;
+	uitable->onselection = args.onselection;
+	uitable->onselectiondata = args.onselectiondata;
+	uitable->onactivate = args.onactivate;
+	uitable->onactivatedata = args.onactivatedata;
+	uitable->ondragstart = args.ondragstart;
+	uitable->ondragstartdata = args.ondragstartdata;
+	uitable->ondragcomplete = args.ondragcomplete;
+	uitable->ondrop = args.ondrop;
+	uitable->ondropdata = args.ondropsdata;
+
+	// grid styling
+	winrt::Windows::UI::Color bg = { 255, 255, 255, 255 }; // test color
+	SolidColorBrush brush = SolidColorBrush(bg);
+	grid.Background(brush);
+
+	// add columns from args.model
+	uitable->add_header(args.model);
+	
+	// bind var
+	UiVar* var = uic_widget_var(obj->ctx, current->ctx, args.list, args.varname, UI_VAR_LIST);
+	if (var) {
+		UiList* list = (UiList*)var->value;
+		list->update = ui_table_update;
+		list->obj = uitable;
+		uitable->update(list, 0);
+	}
+
+	// create toolkit wrapper object and register destructor
+	UIElement elm = scrollW;
+	UiWidget* widget = new UiWidget(elm);
+	ui_context_add_widget_destructor(current->ctx, widget);
+
+	// add scrollW to current container
+	UI_APPLY_LAYOUT1(current, args);
+
+	current->container->Add(scrollW, false);
+
+	return widget;
+}
+
+extern "C" void ui_table_update(UiList * list, int i) {
+	UiTable* table = (UiTable*)list->obj;
+	table->clear();
+	table->update(list, i);
+}
+
+UiTable::UiTable(UiObject *obj, winrt::Microsoft::UI::Xaml::Controls::ScrollViewer scrollW, winrt::Microsoft::UI::Xaml::Controls::Grid grid) {
+	this->obj = obj;
+
+	this->scrollw = scrollw;
+	this->grid = grid;
+
+	winrt::Windows::UI::Color highlightBg = { 255, 234, 234, 234 };
+	highlightBrush = SolidColorBrush(highlightBg);
+
+	winrt::Windows::UI::Color defaultBg = { 0, 0, 0, 0 }; // default
+	defaultBrush = SolidColorBrush(defaultBg);
+
+	winrt::Windows::UI::Color selectedBg = { 255, 204, 232, 255 }; // test color
+	selectedBrush = SolidColorBrush(selectedBg);
+
+	winrt::Windows::UI::Color selectedFg = { 255, 0, 90, 158 }; // test color
+	selectedBorderBrush = SolidColorBrush(selectedFg);
+
+	grid.KeyDown(
+		winrt::Microsoft::UI::Xaml::Input::KeyEventHandler(
+			[=](IInspectable const& sender, winrt::Microsoft::UI::Xaml::Input::KeyRoutedEventArgs const& args) {
+				// key event for hanling the table cursor or enter
+			})
+	);
+}
+
+UiTable::~UiTable() {
+	ui_model_free(NULL, model);
+}
+
+void UiTable::add_header(UiModel* model) {
+	this->model = ui_model_copy(NULL, model);
+
+	GridLength gl;
+	gl.Value = 0;
+	gl.GridUnitType = GridUnitType::Auto;
+
+	// add header row definition
+	auto headerRowDef = RowDefinition();
+	headerRowDef.Height(gl);
+	grid.RowDefinitions().Append(headerRowDef);
+
+
+	for (int i = 0; i < model->columns;i++) {
+		char* title = model->titles[i];
+		UiModelType type = model->types[i];
+
+		// add grid column definition
+		auto colDef = ColumnDefinition();
+		colDef.Width(gl);
+		grid.ColumnDefinitions().Append(colDef);
+
+		// add button
+		auto button = Button();
+		wchar_t* wlabel = str2wstr(title, nullptr);
+		button.Content(box_value(wlabel));
+		free(wlabel);
+
+		// some styling for the button
+		Thickness border = { 0,0,1,0 };
+		CornerRadius corner = { 0,0,0,0 };
+		button.BorderThickness(border);
+		button.CornerRadius(corner);
+
+		grid.SetColumn(button, i);
+		grid.SetRow(button, 0);
+		grid.Children().Append(button);
+
+		UiTableColumn h;
+		h.header = button;
+		header.push_back(h);
+	}
+
+	maxrows = 1;
+}
+
+static void textblock_set_str(TextBlock &t, const char *str) {
+	if (str) {
+		wchar_t* wstr = str2wstr(str, nullptr);
+		t.Text(winrt::hstring(wstr));
+		free(wstr);
+	}
+}
+
+static void textblock_set_int(TextBlock& t, int i) {
+	wchar_t buf[16];
+	swprintf(buf, 16, L"%d", i);
+	t.Text(winrt::hstring(buf));
+}
+
+static ULONG64 getsystime() {
+	SYSTEMTIME st;
+	GetSystemTime(&st);
+	return st.wYear   * 10000000000000 +
+		   st.wMonth  * 100000000000   +
+		   st.wDay    * 1000000000     +
+		   st.wHour   * 10000000       +
+		   st.wMinute * 100000         +
+		   st.wSecond * 1000           +
+		   st.wMilliseconds;
+}
+
+void UiTable::update(UiList* list,  int i) {
+	if (getvalue == nullptr) {
+		return;
+	}
+
+	Thickness b1 = { 1, 1, 0, 1 }; // first col
+	Thickness b2 = { 0, 1, 0, 1 }; // middle
+	Thickness b3 = { 0, 1, 1, 1 }; // last col
+
+	GridLength gl;
+	gl.Value = 0;
+	gl.GridUnitType = GridUnitType::Auto;
+
+	// iterate model
+	int row = 1;
+	void* elm = list->first(list);
+	while (elm) {
+		if (row >= maxrows) {
+			auto rowdef = RowDefinition();
+			rowdef.Height(gl);
+			grid.RowDefinitions().Append(rowdef);
+			maxrows = row;
+		}
+
+		Thickness cellpadding = { 10,0,4,0 };
+
+		// model column, usually the same as col, however UI_ICON_TEXT uses two columns in the model
+		int model_col = 0;
+		for (int col = 0; col < header.size(); col++, model_col++) {
+			// create ui elements with the correct cell border
+			// dependeing on the column
+			Border cellBorder = Border();
+			cellBorder.Background(defaultBrush);
+			cellBorder.BorderBrush(defaultBrush);
+			if (col == 0) {
+				cellBorder.BorderThickness(b1);
+			}
+			else if (col + 1 == header.size()) {
+				cellBorder.BorderThickness(b3);
+			}
+			else {
+				cellBorder.BorderThickness(b2);
+			}
+
+			// dnd
+			if (ondragstart) {
+				cellBorder.CanDrag(true);
+				cellBorder.DragStarting([this](IInspectable const& sender, DragStartingEventArgs args) {
+						UiDnD dnd;
+						dnd.evttype = 0;
+						dnd.dndstartargs = args;
+						dnd.dndcompletedargs = { nullptr };
+						dnd.drageventargs = { nullptr };
+						dnd.data = args.Data();
+
+						UiListDnd dndevt;
+						dndevt.selection = uiselection();
+						dndevt.dnd = &dnd;
+
+						UiEvent evt;
+						evt.obj = this->obj;
+						evt.window = evt.obj->window;
+						evt.document = obj->ctx->document;
+						evt.eventdata = &dndevt;
+						evt.intval = 0;
+					
+						this->ondragstart(&evt, this->ondragstartdata);
+
+						if (dndevt.selection.rows) {
+							free(dndevt.selection.rows);
+						}
+					});
+				cellBorder.DropCompleted([this](IInspectable const& sender, DropCompletedEventArgs args) {
+						UiDnD dnd;
+						dnd.evttype = 1;
+						dnd.dndstartargs = { nullptr };
+						dnd.dndcompletedargs = args;
+						dnd.drageventargs = { nullptr };
+						dnd.data = { nullptr };
+
+						UiListDnd dndevt;
+						dndevt.selection = uiselection();
+						dndevt.dnd = &dnd;
+
+						UiEvent evt;
+						evt.obj = this->obj;
+						evt.window = evt.obj->window;
+						evt.document = obj->ctx->document;
+						evt.eventdata = &dndevt;
+						evt.intval = 0;
+
+						if (this->ondragcomplete) {
+							this->ondragcomplete(&evt, this->ondragcompletedata);
+						}
+						if (dndevt.selection.rows) {
+							free(dndevt.selection.rows);
+						}
+					});
+			}
+			if (ondrop) {
+				cellBorder.AllowDrop(true);
+				cellBorder.Drop(DragEventHandler([this](winrt::Windows::Foundation::IInspectable const& sender, DragEventArgs const& args){
+						UiDnD dnd;
+						dnd.evttype = 2;
+						dnd.dndstartargs = { nullptr };
+						dnd.dndcompletedargs = { nullptr };
+						dnd.drageventargs = args;
+						dnd.data = args.Data();
+
+						UiListDnd dndevt;
+						dndevt.selection = uiselection();
+						dndevt.dnd = &dnd;
+
+						UiEvent evt;
+						evt.obj = this->obj;
+						evt.window = evt.obj->window;
+						evt.document = obj->ctx->document;
+						evt.eventdata = &dndevt;
+						evt.intval = 0;
+
+						this->ondrop(&evt, this->ondropdata);
+
+						if (dndevt.selection.rows) {
+							free(dndevt.selection.rows);
+						}
+					}));
+			}
+
+			// set the cell value
+			// depending on the type, we create different cell controls
+			UiModelType type = model->types[col];
+			switch (type) {
+				case UI_STRING: {
+					TextBlock cell = TextBlock();
+					cell.Padding(cellpadding);
+					cell.VerticalAlignment(VerticalAlignment::Stretch);
+					textblock_set_str(cell, (char*)getvalue(elm, model_col));
+					cellBorder.Child(cell);
+
+					break;
+				}
+				case UI_INTEGER: {
+					TextBlock cell = TextBlock();
+					cell.Padding(cellpadding);
+					cell.VerticalAlignment(VerticalAlignment::Stretch);
+					int *value = (int*)getvalue(elm, model_col);
+					if (value) {
+						textblock_set_int(cell, *value);
+					}
+					cellBorder.Child(cell);
+					break;
+				}
+				case UI_ICON: {
+					UiIcon* iconConstr = (UiIcon*)getvalue(elm, model_col);
+					if (iconConstr) {
+						IconElement icon = iconConstr->getIcon();
+						cellBorder.Child(icon);
+					}
+					break;
+				}
+				case UI_ICON_TEXT: {
+					StackPanel cellPanel = StackPanel();
+					cellPanel.Spacing(2);
+					cellPanel.Padding(cellpadding);
+					cellPanel.VerticalAlignment(VerticalAlignment::Stretch);
+
+					cellPanel.Orientation(Orientation::Horizontal);
+					UiIcon* iconConstr = (UiIcon*)getvalue(elm, model_col++);
+					char* str = (char*)getvalue(elm, model_col);
+					if (iconConstr) {
+						IconElement icon = iconConstr->getIcon();
+						cellPanel.Children().Append(icon);
+					}
+					TextBlock cell = TextBlock();
+					textblock_set_str(cell, str);
+					cellPanel.Children().Append(cell);
+					cellBorder.Child(cellPanel);
+					break;
+				}
+			}
+
+			// event handler
+			cellBorder.PointerPressed(
+				winrt::Microsoft::UI::Xaml::Input::PointerEventHandler(
+					[=](IInspectable const& sender, winrt::Microsoft::UI::Xaml::Input::PointerRoutedEventArgs const& args) {
+						winrt::Windows::System::VirtualKeyModifiers modifiers = args.KeyModifiers();
+						bool update_selection = true;
+
+						if (modifiers == winrt::Windows::System::VirtualKeyModifiers::Control) {
+							// add/remove current row
+							if (!is_row_selected(row)) {
+								row_background(row, selectedBrush, selectedBorderBrush);
+								selection.push_back(row);
+							}
+							else {
+								row_background(row, highlightBrush, highlightBrush);
+								remove_from_selection(row);
+							}
+						}
+						else if (modifiers == winrt::Windows::System::VirtualKeyModifiers::None || selection.size() == 0) {
+							// no modifier or shift is pressed but there is no selection
+							if (selection.size() > 0) {
+								change_rows_bg(selection, defaultBrush, defaultBrush);
+							}
+							
+							row_background(row, selectedBrush, selectedBorderBrush);
+							selection = { row };
+							if (modifiers == winrt::Windows::System::VirtualKeyModifiers::None) {
+								SYSTEMTIME st;
+								GetSystemTime(&st);
+								
+								ULONG64 now = getsystime();
+								ULONG64 tdiff = now - lastPointerPress;
+								if (tdiff < ui_double_click_time && onactivate != nullptr) {
+									// two pointer presse events in short time and we have an onactivate handler
+									update_selection = false; // we don't want an additional selection event
+									lastPointerPress = 0; // reset double-click
+
+									int selectedrow = row - 1; // subtract header row
+
+									UiListSelection selection;
+									selection.count = 1;
+									selection.rows = &selectedrow;
+
+									UiEvent evt;
+									evt.obj = obj;
+									evt.window = obj->window;
+									evt.document = obj->ctx->document;
+									evt.eventdata = &selection;
+									evt.intval = selectedrow;
+									onactivate(&evt, onactivatedata);
+								}
+								else {
+									lastPointerPress = now;
+								}
+							}
+						}
+						else if (modifiers == winrt::Windows::System::VirtualKeyModifiers::Shift) {
+							// select everything between the first selection and the current row
+							std::sort(selection.begin(), selection.end());
+							int first = selection.front();
+							int last = row;
+							if (first > row) {
+								last = first;
+								first = row;
+							}
+
+							// clear previous selection
+							change_rows_bg(selection, defaultBrush, defaultBrush);
+
+							// create new selection
+							std::vector<int> newselection;
+							for (int s = first; s <= last; s++) {
+								newselection.push_back(s);
+							}
+							selection = newselection;
+							change_rows_bg(selection, selectedBrush, selectedBorderBrush);
+						}
+
+						if (update_selection) {
+							call_handler(onselection, onselectiondata);
+						}
+					})
+			);
+			cellBorder.PointerReleased(
+				winrt::Microsoft::UI::Xaml::Input::PointerEventHandler(
+					[=](IInspectable const& sender, winrt::Microsoft::UI::Xaml::Input::PointerRoutedEventArgs const& args) {
+
+					})
+			);
+			cellBorder.PointerEntered(
+				winrt::Microsoft::UI::Xaml::Input::PointerEventHandler(
+					[=](IInspectable const& sender, winrt::Microsoft::UI::Xaml::Input::PointerRoutedEventArgs const& args) {
+						if (!is_row_selected(row)) {
+							row_background(row, highlightBrush, highlightBrush);
+						}
+					})
+			);
+			cellBorder.PointerExited(
+				winrt::Microsoft::UI::Xaml::Input::PointerEventHandler(
+					[=](IInspectable const& sender, winrt::Microsoft::UI::Xaml::Input::PointerRoutedEventArgs const& args) {
+						if (!is_row_selected(row)) {
+							row_background(row, defaultBrush, defaultBrush);
+						}
+					})
+			);
+
+			grid.SetColumn(cellBorder, col);
+			grid.SetRow(cellBorder, row);
+			grid.Children().Append(cellBorder);
+		}
+
+		row++;
+		elm = list->next(list);
+	}
+}
+
+void UiTable::clear() {
+	for (int i = grid.Children().Size()-1; i >= 0; i--) {
+		FrameworkElement elm = grid.Children().GetAt(i).as<FrameworkElement>();
+		int child_row = grid.GetRow(elm);
+		if (child_row > 0) {
+			grid.Children().RemoveAt(i);
+		}
+	}
+
+	// TODO: should we clean row definitions?
+}
+
+void UiTable::row_background(int row, winrt::Microsoft::UI::Xaml::Media::Brush brush, winrt::Microsoft::UI::Xaml::Media::Brush borderBrush) {
+	Thickness b1 = { 1, 1, 0, 1 }; // first col
+	Thickness b2 = { 0, 1, 0, 1 }; // middle
+	Thickness b3 = { 0, 1, 1, 1 }; // last col
+	
+	for (auto child : grid.Children()) {
+		FrameworkElement elm = child.as<FrameworkElement>();
+		int child_row = grid.GetRow(elm);
+		if (child_row == row) {
+			Border b = elm.as<Border>();
+			b.Background(brush);
+			b.BorderBrush(borderBrush);
+		}
+	}
+}
+
+void UiTable::change_rows_bg(std::vector<int> rows, winrt::Microsoft::UI::Xaml::Media::Brush brush, winrt::Microsoft::UI::Xaml::Media::Brush borderBrush) {
+	std::for_each(rows.cbegin(), rows.cend(), [&](const int& row) {row_background(row, brush, borderBrush); });
+}
+
+bool UiTable::is_row_selected(int row) {
+	return std::find(selection.begin(), selection.end(), row) != selection.end() ? true : false;
+}
+
+void UiTable::remove_from_selection(int row) {
+	selection.erase(std::remove(selection.begin(), selection.end(), row), selection.end());
+	selection.shrink_to_fit();
+}
+
+UiListSelection UiTable::uiselection() {
+	std::sort(selection.begin(), selection.end());
+
+	UiListSelection selobj;
+	selobj.count = selection.size();
+	selobj.rows = nullptr;
+	if (selobj.count > 0) {
+		selobj.rows = (int*)calloc(selobj.count, sizeof(int));
+		memcpy(selobj.rows, selection.data(), selobj.count * sizeof(int));
+		for (int i = 0; i < selobj.count; i++) {
+			selobj.rows[i]--;
+		}
+	}
+	return selobj;
+}
+
+void UiTable::call_handler(ui_callback cb, void* cbdata) {
+	if (!cb) {
+		return;
+	}
+
+	UiListSelection selobj = uiselection();
+
+	UiEvent evt;
+	evt.obj = obj;
+	evt.window = obj->window;
+	evt.document = obj->ctx->document;
+	evt.eventdata = &selobj;
+	evt.intval = 0;
+	cb(&evt, cbdata);
+
+	if (selobj.rows) {
+		free(selobj.rows);
+	}
+}

mercurial