Thu, 22 Feb 2024 22:25:53 +0100
implement listview (GTK)
/* * 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); } }