ui/winui/table.cpp

Thu, 21 Nov 2024 12:04:53 +0100

author
Olaf Wintermann <olaf.wintermann@gmail.com>
date
Thu, 21 Nov 2024 12:04:53 +0100
branch
newapi
changeset 388
473c03f85197
parent 248
22257f5f4019
child 394
bedd499b640d
permissions
-rw-r--r--

add menu builder API

/*
 * 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:
}

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));
}

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->getselection = ui_table_selection;
		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);
}

extern "C" UiListSelection ui_table_selection(UiList * list) {
	UiTable* table = (UiTable*)list->obj;
	return table->uiselection();
}

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);

	winrt::Windows::UI::Color borderColor = { 63, 0, 0, 0 };
	SolidColorBrush borderBrush = SolidColorBrush(borderColor);


	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);

		// header column border
		Border headerBorder = Border();
		Thickness border = { 0,0,1,0 };
		headerBorder.BorderThickness(border);
		headerBorder.BorderBrush(borderBrush);

		// add text
		auto hLabel = TextBlock();
		textblock_set_str(hLabel, title);
		Thickness cellpadding = { 10,4,4,4 };
		hLabel.Padding(cellpadding);
		hLabel.VerticalAlignment(VerticalAlignment::Stretch);

		// event handler for highlighting and column resizing
		headerBorder.PointerPressed(
			winrt::Microsoft::UI::Xaml::Input::PointerEventHandler(
				[=](IInspectable const& sender, winrt::Microsoft::UI::Xaml::Input::PointerRoutedEventArgs const& args) {
					// the last column doesn't need resize capabilities
					if (i + 1 < model->columns) {
						double width = headerBorder.ActualWidth();
						auto point = args.GetCurrentPoint(headerBorder);
						auto position = point.Position();
						if (position.X + 4 >= width) {
							this->resize = true;
							this->resizedCol = headerBorder;
						}
					}
				})
		);
		headerBorder.PointerReleased(
			winrt::Microsoft::UI::Xaml::Input::PointerEventHandler(
				[=](IInspectable const& sender, winrt::Microsoft::UI::Xaml::Input::PointerRoutedEventArgs const& args) {
					this->resize = false;
				})
		);
		headerBorder.PointerMoved(
			winrt::Microsoft::UI::Xaml::Input::PointerEventHandler(
				[=](IInspectable const& sender, winrt::Microsoft::UI::Xaml::Input::PointerRoutedEventArgs const& args) {
					if (this->resize) {
						auto point = args.GetCurrentPoint(this->resizedCol);
						auto position = point.Position();
						if (position.X > 1) {
							this->resizedCol.Width(position.X);
						}
					}
				})
		);
		headerBorder.PointerEntered(
			winrt::Microsoft::UI::Xaml::Input::PointerEventHandler(
				[=](IInspectable const& sender, winrt::Microsoft::UI::Xaml::Input::PointerRoutedEventArgs const& args) {
					// TODO: background
				})
		);
		headerBorder.PointerExited(
			winrt::Microsoft::UI::Xaml::Input::PointerEventHandler(
				[=](IInspectable const& sender, winrt::Microsoft::UI::Xaml::Input::PointerRoutedEventArgs const& args) {
					// TODO: background
				})
		);



		// add controls
		headerBorder.Child(hLabel);

		grid.SetColumn(headerBorder, i);
		grid.SetRow(headerBorder, 0);
		grid.Children().Append(headerBorder);

		UiTableColumn h;
		h.header = headerBorder;
		header.push_back(h);
	}

	maxrows = 1;
}

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.dataview = args.DataView();

						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);
						}
					}));
				cellBorder.DragOver(DragEventHandler([this](winrt::Windows::Foundation::IInspectable const& sender, DragEventArgs const& args){
					args.AcceptedOperation(winrt::Windows::ApplicationModel::DataTransfer::DataPackageOperation::Copy);
					}));
			}

			// set the cell value
			// depending on the type, we create different cell controls
			UiModelType type = model->types[col];
			switch (type) {
				case UI_STRING_FREE:
				case UI_STRING: {
					TextBlock cell = TextBlock();
					cell.Padding(cellpadding);
					cell.VerticalAlignment(VerticalAlignment::Stretch);
					char *val = (char*)getvalue(elm, model_col);
					textblock_set_str(cell, val);
					cellBorder.Child(cell);
					if (type == UI_STRING_FREE && val) {
						free(val);
					}

					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_FREE:
				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);
					if (type == UI_ICON_TEXT_FREE && str) {
						free(str);
					}
					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