diff -r 000000000000 -r 2483f517c562 ui/winui/table.cpp --- /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 +#include +#include +#include +#include +#include + +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 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(); + 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(); + int child_row = grid.GetRow(elm); + if (child_row == row) { + Border b = elm.as(); + b.Background(brush); + b.BorderBrush(borderBrush); + } + } +} + +void UiTable::change_rows_bg(std::vector 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); + } +}