// This file is part of the Essence operating system. // It is released under the terms of the MIT license -- see LICENSE.md. // Written by: nakst. // TODO RMB click/drag. // TODO Consistent int64_t/intptr_t. // TODO Drag and drop. // TODO GetFirstIndex/GetLastIndex assume that every group is non-empty. // TODO Sticking to top/bottom scroll when inserting/removing space. // TODO Audit usage of MeasureItems -- it doesn't take into account the gap between items! struct ListViewItemElement : EsElement { uintptr_t index; // Index into the visible items array. }; struct ListViewItem { ListViewItemElement *element; EsListViewIndex group; int32_t size; EsListViewIndex index; uint8_t indent; bool startAtSecondColumn; bool isHeader, isFooter; bool showSearchHighlight; }; struct ListViewGroup { // TODO Empty groups. EsListViewIndex itemCount; int64_t totalSize; uint32_t flags; bool initialised; }; struct ListViewFixedString { char *string; size_t bytes; }; struct ListViewFixedItem { EsGeneric data; uint32_t iconID; }; struct ListViewFixedItemData { union { ListViewFixedString s; double d; int64_t i; }; }; struct ListViewColumn { char *title; size_t titleBytes; uint32_t id; uint32_t flags; double width; Array items; const EsListViewEnumString *enumStrings; size_t enumStringCount; }; typedef void (*ListViewSortFunction)(EsListViewIndex *, size_t, ListViewColumn *); int ListViewProcessItemMessage(EsElement *element, EsMessage *message); void ListViewSetSortAscending(EsMenu *menu, EsGeneric context); void ListViewSetSortDescending(EsMenu *menu, EsGeneric context); void ListViewPopulateActionCallback(EsElement *element, EsGeneric); void ListViewEnsureVisibleActionCallback(EsElement *element, EsGeneric); struct EsListView : EsElement { ScrollPane scroll; uint64_t totalItemCount; uint64_t totalSize; Array groups; Array visibleItems; EsStyleID itemStyle, headerItemStyle, footerItemStyle; int64_t fixedWidth, fixedHeight; int64_t fixedHeaderSize, fixedFooterSize; UIStyle *primaryCellStyle; UIStyle *secondaryCellStyle; UIStyle *selectedCellStyle; bool hasFocusedItem; EsListViewIndex focusedItemGroup; EsListViewIndex focusedItemIndex; bool hasAnchorItem; EsListViewIndex anchorItemGroup; EsListViewIndex anchorItemIndex; bool hasScrollItem; // Used to preserve the scroll position when resizing a wrapped list view. bool useScrollItem; int64_t scrollItemOffset; EsListViewIndex scrollItemGroup; EsListViewIndex scrollItemIndex; #define ENSURE_VISIBLE_ALIGN_TOP (1 << 0) #define ENSURE_VISIBLE_ALIGN_CENTER (1 << 1) #define ENSURE_VISIBLE_ALIGN_FOR_SCROLL_ITEM (1 << 2) EsListViewIndex ensureVisibleGroupIndex; EsListViewIndex ensureVisibleIndex; uint8_t ensureVisibleFlags; bool ensureVisibleQueued; bool populateQueued; // Valid only during Z-order messages. Array zOrderItems; EsElement *selectionBox; bool hasSelectionBoxAnchor; int64_t selectionBoxAnchorX, selectionBoxAnchorY, selectionBoxPositionX, selectionBoxPositionY; bool firstLayout; char searchBuffer[64]; size_t searchBufferBytes; uint64_t searchBufferLastKeyTime; char *emptyMessage; size_t emptyMessageBytes; EsElement *columnHeader; Array registeredColumns; Array activeColumns; // Indices into registeredColumns. int columnResizingOriginalWidth; int64_t totalColumnWidth; EsTextbox *inlineTextbox; EsListViewIndex inlineTextboxGroup; EsListViewIndex inlineTextboxIndex; int maximumItemsPerBand; // Fixed item storage: Array fixedItems; Array fixedItemIndices; // For sorting. Converts the actual list index into an index for fixedItems. ptrdiff_t fixedItemSelection; uint32_t fixedItemSortColumnID; #define LIST_SORT_DIRECTION_ASCENDING (1) #define LIST_SORT_DIRECTION_DESCENDING (2) uint8_t fixedItemSortDirection; inline EsRectangle GetListBounds() { EsRectangle bounds = GetBounds(); if (columnHeader) { bounds.t += columnHeader->style->preferredHeight; } return bounds; } inline void GetFirstIndex(EsMessage *message) { EsAssert(message->iterateIndex.group < (EsListViewIndex) groups.Length()); // Invalid group index. EsAssert(groups[message->iterateIndex.group].itemCount); // No items in the group. message->iterateIndex.index = 0; } inline void GetLastIndex(EsMessage *message) { EsAssert(message->iterateIndex.group < (EsListViewIndex) groups.Length()); // Invalid group index. EsAssert(groups[message->iterateIndex.group].itemCount); // No items in the group. message->iterateIndex.index = groups[message->iterateIndex.group].itemCount - 1; } inline bool IterateForwards(EsMessage *message) { if (message->iterateIndex.index == groups[message->iterateIndex.group].itemCount - 1) { if (message->iterateIndex.group == (EsListViewIndex) groups.Length() - 1) { return false; } message->iterateIndex.group++; message->iterateIndex.index = 0; } else { message->iterateIndex.index++; } return true; } inline bool IterateBackwards(EsMessage *message) { if (message->iterateIndex.index == 0) { if (message->iterateIndex.group == 0) { return false; } message->iterateIndex.group--; message->iterateIndex.index = groups[message->iterateIndex.group].itemCount - 1; } else { message->iterateIndex.index--; } return true; } int64_t MeasureItems(EsListViewIndex groupIndex, EsListViewIndex firstIndex, EsListViewIndex count) { if (count == 0) return 0; EsAssert(count > 0); bool variableSize = flags & ES_LIST_VIEW_VARIABLE_SIZE; if (!variableSize) { ListViewGroup *group = &groups[groupIndex]; int64_t normalCount = count; int64_t additionalSize = 0; if ((group->flags & ES_LIST_VIEW_GROUP_HAS_HEADER) && firstIndex == 0) { normalCount--; additionalSize += fixedHeaderSize; } if ((group->flags & ES_LIST_VIEW_GROUP_HAS_FOOTER) && firstIndex + count == group->itemCount) { normalCount--; additionalSize += fixedFooterSize; } return additionalSize + normalCount * (flags & ES_LIST_VIEW_HORIZONTAL ? fixedWidth : fixedHeight); } if (count > 1) { EsMessage m = { ES_MSG_LIST_VIEW_MEASURE_RANGE }; m.itemRange.group = groupIndex; m.itemRange.firstIndex = firstIndex; m.itemRange.count = count; if (ES_HANDLED == EsMessageSend(this, &m)) { return m.itemRange.result; } } EsMessage m = {}; m.iterateIndex.group = groupIndex; m.iterateIndex.index = firstIndex; int64_t total = 0; int64_t _count = 0; while (true) { EsMessage m2 = { ES_MSG_LIST_VIEW_MEASURE_ITEM }; m2.measureItem.group = groupIndex; m2.measureItem.index = m.iterateIndex.index; EsAssert(ES_HANDLED == EsMessageSend(this, &m2)); // Variable height list view must be able to measure items. total += m2.measureItem.result; _count++; if (count == _count) { return total; } IterateForwards(&m); EsAssert(groupIndex == m.iterateIndex.group); // Index range did not exist in group. } } void GetItemPosition(EsListViewIndex groupIndex, EsListViewIndex index, int64_t *_position, int64_t *_itemSize) { int64_t gapBetweenGroup = style->gapMajor, gapBetweenItems = (flags & ES_LIST_VIEW_TILED) ? style->gapWrap : style->gapMinor, fixedSize = (flags & ES_LIST_VIEW_VARIABLE_SIZE) ? 0 : (flags & ES_LIST_VIEW_HORIZONTAL ? fixedWidth : fixedHeight), startInset = flags & ES_LIST_VIEW_HORIZONTAL ? style->insets.l : style->insets.t; int64_t position = (flags & ES_LIST_VIEW_HORIZONTAL ? -scroll.position[0] : -scroll.position[1]) + startInset, itemSize = 0; EsListViewIndex targetIndex = index; for (EsListViewIndex i = 0; i < groupIndex; i++) { position += groups[i].totalSize + gapBetweenGroup; } ListViewGroup *group = &groups[groupIndex]; if ((group->flags & ES_LIST_VIEW_GROUP_HAS_HEADER) && index == 0) { itemSize = fixedHeaderSize; } else if ((group->flags & ES_LIST_VIEW_GROUP_HAS_HEADER) && index == group->itemCount - 1) { position += group->totalSize - fixedFooterSize; itemSize = fixedFooterSize; } else if (~flags & ES_LIST_VIEW_VARIABLE_SIZE) { intptr_t linearIndex = index; if (group->flags & ES_LIST_VIEW_GROUP_HAS_HEADER) { linearIndex--; position += fixedHeaderSize + gapBetweenItems; } linearIndex /= GetItemsPerBand(); position += (fixedSize + gapBetweenItems) * linearIndex; itemSize = fixedSize; } else { EsAssert(~flags & ES_LIST_VIEW_TILED); // Tiled list views must be fixed-size. EsMessage index = {}; index.type = ES_MSG_LIST_VIEW_FIND_POSITION; index.iterateIndex.group = groupIndex; index.iterateIndex.index = targetIndex; if (ES_HANDLED == EsMessageSend(this, &index)) { position += index.iterateIndex.position; } else { if (group->flags & ES_LIST_VIEW_GROUP_HAS_HEADER) { position += fixedHeaderSize + gapBetweenItems; } bool forwards; ListViewItem *reference = visibleItems.Length() ? visibleItems.array : nullptr; bool closerToStartThanReference = reference && targetIndex < reference->index / 2; bool closerToEndThanReference = reference && targetIndex > reference->index / 2 + (intptr_t) group->itemCount / 2; if (reference && reference->group == groupIndex && !closerToStartThanReference && !closerToEndThanReference) { index.iterateIndex.index = reference->index; position = (flags & ES_LIST_VIEW_HORIZONTAL) ? reference->element->offsetX : reference->element->offsetY; forwards = reference->index < targetIndex; EsMessage firstIndex = {}; firstIndex.iterateIndex.group = groupIndex; GetFirstIndex(&firstIndex); if (index.iterateIndex.index == firstIndex.iterateIndex.index) { forwards = true; } } else if (targetIndex > group->itemCount / 2) { GetLastIndex(&index); position += group->totalSize; position -= MeasureItems(index.iterateIndex.group, index.iterateIndex.index, 1); forwards = false; } else { GetFirstIndex(&index); forwards = true; } while (index.iterateIndex.index != targetIndex) { int64_t size = MeasureItems(index.iterateIndex.group, index.iterateIndex.index, 1); position += forwards ? (size + gapBetweenItems) : -(size + gapBetweenItems); EsAssert((forwards ? IterateForwards(&index) : IterateBackwards(&index)) && index.iterateIndex.group == groupIndex); // Could not find the item in the group. } itemSize = MeasureItems(index.iterateIndex.group, index.iterateIndex.index, 1); } } *_position = position; *_itemSize = itemSize; } void EnsureItemVisible(EsListViewIndex groupIndex, EsListViewIndex index, uint8_t visibleFlags) { ensureVisibleGroupIndex = groupIndex; ensureVisibleIndex = index; ensureVisibleFlags = visibleFlags; if (!ensureVisibleQueued) { UpdateAction action = {}; action.element = this; action.callback = ListViewEnsureVisibleActionCallback; window->updateActions.Add(action); ensureVisibleQueued = true; } } void _EnsureItemVisible(EsListViewIndex groupIndex, EsListViewIndex index, uint8_t visibleFlags) { EsRectangle contentBounds = GetListBounds(); int64_t startInset = flags & ES_LIST_VIEW_HORIZONTAL ? style->insets.l : style->insets.t, endInset = flags & ES_LIST_VIEW_HORIZONTAL ? style->insets.r : style->insets.b, contentSize = flags & ES_LIST_VIEW_HORIZONTAL ? Width(contentBounds) : Height(contentBounds); int64_t position, itemSize; GetItemPosition(groupIndex, index, &position, &itemSize); if (visibleFlags & ENSURE_VISIBLE_ALIGN_FOR_SCROLL_ITEM) { if (flags & ES_LIST_VIEW_HORIZONTAL) { scroll.SetX(scroll.position[0] + position - scrollItemOffset); } else { scroll.SetY(scroll.position[1] + position - scrollItemOffset); } useScrollItem = true; return; } if (position >= 0 && position + itemSize <= contentSize - endInset) { return; } if (visibleFlags & ENSURE_VISIBLE_ALIGN_TOP) { if (flags & ES_LIST_VIEW_HORIZONTAL) { scroll.SetX(scroll.position[0] + position - startInset); } else { scroll.SetY(scroll.position[1] + position - startInset); } } else if (visibleFlags & ENSURE_VISIBLE_ALIGN_CENTER) { if (flags & ES_LIST_VIEW_HORIZONTAL) { scroll.SetX(scroll.position[0] + position + itemSize / 2 - contentSize / 2); } else { scroll.SetY(scroll.position[1] + position + itemSize / 2 - contentSize / 2); } } else { if (flags & ES_LIST_VIEW_HORIZONTAL) { scroll.SetX(scroll.position[0] + position + itemSize - contentSize + endInset); } else { scroll.SetY(scroll.position[1] + position + itemSize - contentSize + endInset); } } } EsMessage FindFirstVisibleItem(int64_t *_position, int64_t position, ListViewItem *reference, bool *noItems) { int64_t gapBetweenGroup = style->gapMajor, gapBetweenItems = (flags & ES_LIST_VIEW_TILED) ? style->gapWrap : style->gapMinor, fixedSize = (flags & ES_LIST_VIEW_VARIABLE_SIZE) ? 0 : (flags & ES_LIST_VIEW_HORIZONTAL ? fixedWidth : fixedHeight); // Find the group. // TODO Faster searching when there are many groups. EsListViewIndex groupIndex = 0; bool foundGroup = false; for (; groupIndex < (EsListViewIndex) groups.Length(); groupIndex++) { ListViewGroup *group = &groups[groupIndex]; int64_t totalSize = group->totalSize; if (position + totalSize > 0) { foundGroup = true; break; } position += totalSize + gapBetweenGroup; } if (!foundGroup) { if (noItems) { *noItems = true; return {}; } else { EsAssert(false); // Could not find the first visible item with the given scroll. } } EsMessage index = {}; index.iterateIndex.group = groupIndex; // Can we go directly to the item? if (~flags & ES_LIST_VIEW_VARIABLE_SIZE) { index.iterateIndex.index = 0; intptr_t addHeader = 0; ListViewGroup *group = &groups[groupIndex]; if (group->flags & ES_LIST_VIEW_GROUP_HAS_HEADER) { if (position + fixedHeaderSize > 0) { *_position = position; return index; } position += fixedHeaderSize + gapBetweenItems; addHeader = 1; } EsListViewIndex band = -position / (fixedSize + gapBetweenItems); if (band < 0) band = 0; position += band * (fixedSize + gapBetweenItems); EsListViewIndex itemsPerBand = (flags & ES_LIST_VIEW_TILED) ? GetItemsPerBand() : 1; index.iterateIndex.index = band * itemsPerBand + addHeader; if (index.iterateIndex.index >= group->itemCount) { index.iterateIndex.index = group->itemCount - 1; position += (index.iterateIndex.index / itemsPerBand - band) * (fixedSize + gapBetweenItems); } *_position = position; return index; } EsAssert(~flags & ES_LIST_VIEW_TILED); // Trying to use TILED mode with VARIABLE_SIZE mode. // Try asking the application to find the item. index.type = ES_MSG_LIST_VIEW_FIND_INDEX; index.iterateIndex.position = -position; if (ES_HANDLED == EsMessageSend(this, &index)) { *_position = -index.iterateIndex.position; return index; } // Find the item within the group, manually. bool forwards; if (reference && reference->group == groupIndex) { int64_t referencePosition = (flags & ES_LIST_VIEW_HORIZONTAL) ? reference->element->offsetX : reference->element->offsetY; if (AbsoluteInteger64(referencePosition) < AbsoluteInteger64(position) && AbsoluteInteger64(referencePosition) < AbsoluteInteger64(position + groups[groupIndex].totalSize)) { index.iterateIndex.index = reference->index; position = referencePosition; // Use previous first visible item as reference. forwards = position < 0; EsMessage firstIndex = {}; firstIndex.iterateIndex.group = groupIndex; GetFirstIndex(&firstIndex); if (index.iterateIndex.index == firstIndex.iterateIndex.index) { forwards = true; } goto gotReference; } } if (position + groups[groupIndex].totalSize / 2 >= 0) { GetFirstIndex(&index); // Use start of group as reference. forwards = true; } else { GetLastIndex(&index); // Use end of group as reference position += groups[groupIndex].totalSize; position -= MeasureItems(index.iterateIndex.group, index.iterateIndex.index, 1); forwards = false; } gotReference:; if (forwards) { // Iterate forwards from reference point. while (true) { int64_t size = fixedSize ?: MeasureItems(index.iterateIndex.group, index.iterateIndex.index, 1); if (position + size > 0) { *_position = position; return index; } EsAssert(IterateForwards(&index) && index.iterateIndex.group == groupIndex); // No items in the group are visible. Maybe invalid scroll position? position += size + gapBetweenItems; } } else { // Iterate backwards from reference point. while (true) { if (position <= 0 || !IterateBackwards(&index)) { *_position = position; return index; } int64_t size = fixedSize ?: MeasureItems(index.iterateIndex.group, index.iterateIndex.index, 1); EsAssert(index.iterateIndex.group == groupIndex); // No items in the group are visible. Maybe invalid scroll position? position -= size + gapBetweenItems; } } } void _Populate() { // TODO Keep one item before and after the viewport, so tab traversal on custom elements works. // TODO Always keep an item if it has FOCUS_WITHIN. // - But maybe we shouldn't allow focusable elements in a list view. if (!totalItemCount) { return; } EsRectangle contentBounds = GetListBounds(); int64_t contentSize = flags & ES_LIST_VIEW_HORIZONTAL ? Width(contentBounds) : Height(contentBounds); int64_t scroll = EsCRTfloor(flags & ES_LIST_VIEW_HORIZONTAL ? (this->scroll.position[0] - style->insets.l) : (this->scroll.position[1] - style->insets.t)); int64_t position = 0; bool noItems = false; EsMessage currentItem = FindFirstVisibleItem(&position, -scroll, visibleItems.Length() ? visibleItems.array : nullptr, &noItems); uintptr_t visibleIndex = 0; int64_t wrapLimit = GetWrapLimit(); int64_t fixedMinorSize = (flags & ES_LIST_VIEW_HORIZONTAL) ? fixedHeight : fixedWidth; intptr_t itemsPerBand = GetItemsPerBand(); intptr_t itemInBand = 0; int64_t computedMinorGap = style->gapMinor; int64_t minorPosition = 0; int64_t centerOffset = (flags & ES_LIST_VIEW_CENTER_TILES) ? (wrapLimit - itemsPerBand * (fixedMinorSize + style->gapMinor) + style->gapMinor) / 2 : 0; while (visibleIndex < visibleItems.Length()) { // Remove visible items no longer visible, before the viewport. ListViewItem *visibleItem = &visibleItems[visibleIndex]; int64_t visibleItemPosition = flags & ES_LIST_VIEW_HORIZONTAL ? visibleItem->element->offsetX : visibleItem->element->offsetY; if (visibleItemPosition >= position) break; visibleItem->element->index = visibleIndex; visibleItem->element->Destroy(); visibleItems.Delete(visibleIndex); } while (position < contentSize && !noItems) { ListViewItem *visibleItem = visibleIndex == visibleItems.Length() ? nullptr : &visibleItems[visibleIndex]; if (visibleItem && visibleItem->index == currentItem.iterateIndex.index && visibleItem->group == currentItem.iterateIndex.group) { // This is already a visible item. if (~flags & ES_LIST_VIEW_TILED) { int64_t expectedPosition = (flags & ES_LIST_VIEW_HORIZONTAL) ? visibleItem->element->offsetX - contentBounds.l : visibleItem->element->offsetY - contentBounds.t; if (position < expectedPosition - 1 || position > expectedPosition + 1) { EsPrint("Item in unexpected position: got %d, should have been %d; index %d, scroll %d.\n", expectedPosition, position, visibleItem->index, scroll); EsAssert(false); } } } else { // Add a new visible item. ListViewItem empty = {}; visibleItems.Insert(empty, visibleIndex); visibleItem = &visibleItems[visibleIndex]; visibleItem->group = currentItem.iterateIndex.group; visibleItem->index = currentItem.iterateIndex.index; visibleItem->size = MeasureItems(visibleItem->group, visibleItem->index, 1); ListViewGroup *group = &groups[visibleItem->group]; EsStyleID style = 0; if ((group->flags & ES_LIST_VIEW_GROUP_HAS_HEADER) && visibleItem->index == 0) { style = headerItemStyle; visibleItem->isHeader = true; } else if ((group->flags & ES_LIST_VIEW_GROUP_HAS_FOOTER) && visibleItem->index == (intptr_t) group->itemCount - 1) { style = footerItemStyle; visibleItem->isFooter = true; } else { if (group->flags & ES_LIST_VIEW_GROUP_INDENT) { visibleItem->indent++; } style = itemStyle; } visibleItem->element = (ListViewItemElement *) EsHeapAllocate(sizeof(ListViewItemElement), true); visibleItem->element->Initialise(this, ES_CELL_FILL, nullptr, style); visibleItem->element->index = visibleIndex; visibleItem->element->cName = "list view item"; visibleItem->element->messageClass = ListViewProcessItemMessage; if (hasFocusedItem && visibleItem->group == focusedItemGroup && visibleItem->index == focusedItemIndex) { visibleItem->element->customStyleState |= THEME_STATE_FOCUSED_ITEM; } if (state & UI_STATE_FOCUSED) { visibleItem->element->customStyleState |= THEME_STATE_LIST_FOCUSED; } EsMessage m = {}; m.type = ES_MSG_LIST_VIEW_IS_SELECTED; m.selectItem.group = visibleItem->group; m.selectItem.index = visibleItem->index; EsMessageSend(this, &m); if (m.selectItem.isSelected) visibleItem->element->customStyleState |= THEME_STATE_SELECTED; m.type = ES_MSG_LIST_VIEW_CREATE_ITEM; m.createItem.group = visibleItem->group; m.createItem.index = visibleItem->index; m.createItem.item = visibleItem->element; EsMessageSend(this, &m); m.type = ES_MSG_LIST_VIEW_GET_INDENT; m.getIndent.group = visibleItem->group; m.getIndent.index = visibleItem->index; m.getIndent.indent = 0; EsMessageSend(this, &m); visibleItem->indent += m.getIndent.indent; SelectPreview(visibleItems.Length() - 1); visibleItem->element->MaybeRefreshStyle(); } visibleItem->element->index = visibleIndex; // Update the item's position. ListViewGroup *group = &groups[visibleItem->group]; if ((flags & ES_LIST_VIEW_TILED) && !visibleItem->isHeader && !visibleItem->isFooter) { if (flags & ES_LIST_VIEW_HORIZONTAL) { visibleItem->element->InternalMove(fixedWidth, fixedHeight, position + contentBounds.l, minorPosition + style->insets.t + contentBounds.t + centerOffset); } else { visibleItem->element->InternalMove(fixedWidth, fixedHeight, minorPosition + style->insets.l + contentBounds.l + centerOffset, position + contentBounds.t); } minorPosition += computedMinorGap + fixedMinorSize; itemInBand++; bool endOfGroup = ((group->flags & ES_LIST_VIEW_GROUP_HAS_FOOTER) && currentItem.iterateIndex.index == group->itemCount - 2) || (currentItem.iterateIndex.index == group->itemCount - 1); if (itemInBand == itemsPerBand || endOfGroup) { minorPosition = 0; itemInBand = 0; position += (flags & ES_LIST_VIEW_HORIZONTAL) ? visibleItem->element->width : visibleItem->element->height; if (!endOfGroup || (group->flags & ES_LIST_VIEW_GROUP_HAS_FOOTER)) position += style->gapWrap; } } else { if (flags & ES_LIST_VIEW_HORIZONTAL) { visibleItem->element->InternalMove( visibleItem->size, Height(contentBounds) - style->insets.t - style->insets.b - visibleItem->indent * style->gapWrap, position + contentBounds.l, style->insets.t - this->scroll.position[1] + visibleItem->indent * style->gapWrap + contentBounds.t); position += visibleItem->element->width; } else if ((flags & ES_LIST_VIEW_COLUMNS) && ((~flags & ES_LIST_VIEW_CHOICE_SELECT) || (this->scroll.enabled[0]))) { int indent = visibleItem->indent * style->gapWrap; int firstColumn = activeColumns.Length() ? (registeredColumns[activeColumns[0]].width * theming.scale + secondaryCellStyle->gapMajor) : 0; visibleItem->startAtSecondColumn = indent > firstColumn; if (indent > firstColumn) indent = firstColumn; visibleItem->element->InternalMove(totalColumnWidth - indent, visibleItem->size, indent - this->scroll.position[0] + contentBounds.l + style->insets.l, position + contentBounds.t); position += visibleItem->element->height; } else { int indent = visibleItem->indent * style->gapWrap + style->insets.l; visibleItem->element->InternalMove(Width(contentBounds) - indent - style->insets.r, visibleItem->size, indent + contentBounds.l - this->scroll.position[0], position + contentBounds.t); position += visibleItem->element->height; } if ((flags & ES_LIST_VIEW_TILED) && (group->flags & ES_LIST_VIEW_GROUP_HAS_HEADER) && currentItem.iterateIndex.index == 0) { position += style->gapWrap; } } // Go to the next item. visibleIndex++; EsListViewIndex previousGroup = currentItem.iterateIndex.group; if (!IterateForwards(¤tItem)) break; position += previousGroup == currentItem.iterateIndex.group ? (flags & ES_LIST_VIEW_TILED ? 0 : style->gapMinor) : style->gapMajor; } while (visibleIndex < visibleItems.Length()) { // Remove visible items no longer visible, after the viewport. ListViewItem *visibleItem = &visibleItems[visibleIndex]; visibleItem->element->index = visibleIndex; visibleItem->element->Destroy(); visibleItems.Delete(visibleIndex); } if (inlineTextbox) { ListViewItem *item = FindVisibleItem(inlineTextboxGroup, inlineTextboxIndex); if (item) MoveInlineTextbox(item); } if (visibleItems.Length() && (!useScrollItem || !hasScrollItem)) { scrollItemGroup = visibleItems.First().group; scrollItemIndex = visibleItems.First().index; scrollItemOffset = visibleItems.First().element->offsetY; hasScrollItem = true; } } void Populate() { if (!populateQueued) { UpdateAction action = {}; action.element = this; action.callback = ListViewPopulateActionCallback; window->updateActions.Add(action); populateQueued = true; } } void Wrap(bool sizeChanged) { if (~flags & ES_LIST_VIEW_TILED) return; totalSize = 0; intptr_t itemsPerBand = GetItemsPerBand(); for (uintptr_t i = 0; i < groups.Length(); i++) { ListViewGroup *group = &groups[i]; int64_t groupSize = 0; intptr_t itemCount = group->itemCount; if (group->flags & ES_LIST_VIEW_GROUP_HAS_HEADER) { groupSize += fixedHeaderSize + style->gapWrap; itemCount--; } if (group->flags & ES_LIST_VIEW_GROUP_HAS_FOOTER) { groupSize += fixedFooterSize + style->gapWrap; itemCount--; } intptr_t bandsInGroup = (itemCount + itemsPerBand - 1) / itemsPerBand; groupSize += (((flags & ES_LIST_VIEW_HORIZONTAL) ? fixedWidth : fixedHeight) + style->gapWrap) * bandsInGroup; groupSize -= style->gapWrap; group->totalSize = groupSize; totalSize += groupSize + (group == &groups.Last() ? 0 : style->gapMajor); } scroll.Refresh(); if (sizeChanged) { useScrollItem = true; } if (useScrollItem && hasScrollItem) { EnsureItemVisible(scrollItemGroup, scrollItemIndex, ENSURE_VISIBLE_ALIGN_FOR_SCROLL_ITEM); } } void InsertSpace(int64_t space, uintptr_t beforeItem) { if (!space) return; if (flags & ES_LIST_VIEW_TILED) { EsElementUpdateContentSize(this); return; } totalSize += space; for (uintptr_t i = beforeItem; i < visibleItems.Length(); i++) { ListViewItem *item = &visibleItems[i]; if (flags & ES_LIST_VIEW_HORIZONTAL) { item->element->offsetX += space; } else { item->element->offsetY += space; } } useScrollItem = false; scroll.Refresh(); EsElementUpdateContentSize(this); } void SetSelected(EsListViewIndex fromGroup, EsListViewIndex fromIndex, EsListViewIndex toGroup, EsListViewIndex toIndex, bool select, bool toggle, intptr_t period = 0, intptr_t periodBegin = 0, intptr_t periodEnd = 0) { if (!select && (flags & ES_LIST_VIEW_CHOICE_SELECT)) { return; } if (!select && (flags & ES_LIST_VIEW_SINGLE_SELECT)) { EsMessage m = {}; m.type = ES_MSG_LIST_VIEW_SELECT; m.selectItem.isSelected = false; fixedItemSelection = -1; EsMessageSend(this, &m); return; } if (fromGroup == toGroup && fromIndex > toIndex) { EsListViewIndex temp = fromIndex; fromIndex = toIndex; toIndex = temp; } else if (fromGroup > toGroup) { EsListViewIndex temp1 = fromGroup; fromGroup = toGroup; toGroup = temp1; EsListViewIndex temp2 = fromIndex; fromIndex = toIndex; toIndex = temp2; } EsMessage start = {}, end = {}; start.iterateIndex.group = fromGroup; end.iterateIndex.group = fromGroup; for (; start.iterateIndex.group <= toGroup; start.iterateIndex.group++, end.iterateIndex.group++) { if (start.iterateIndex.group == fromGroup) { start.iterateIndex.index = fromIndex; } else { GetFirstIndex(&start); } if (end.iterateIndex.group == toGroup) { end.iterateIndex.index = toIndex; } else { GetLastIndex(&end); } EsMessage m = { ES_MSG_LIST_VIEW_SELECT_RANGE }; m.selectRange.group = start.iterateIndex.group; m.selectRange.fromIndex = start.iterateIndex.index; m.selectRange.toIndex = end.iterateIndex.index; m.selectRange.select = select; m.selectRange.toggle = toggle; if (!period && 0 != EsMessageSend(this, &m)) { continue; } intptr_t linearIndex = 0; while (true) { m.selectItem.group = start.iterateIndex.group; m.selectItem.index = start.iterateIndex.index; m.selectItem.isSelected = select; if (period) { ListViewGroup *group = &groups[m.selectItem.group]; intptr_t i = linearIndex; if (group->flags & ES_LIST_VIEW_GROUP_HAS_HEADER) { if (linearIndex == 0) { goto process; } else { i--; } } if (group->flags & ES_LIST_VIEW_GROUP_HAS_FOOTER) { if (linearIndex == (intptr_t) group->itemCount - 1) { goto process; } } i %= period; if (i < periodBegin || i > periodEnd) { goto ignore; } } process:; if (toggle) { m.type = ES_MSG_LIST_VIEW_IS_SELECTED; EsMessageSend(this, &m); m.selectItem.isSelected = !m.selectItem.isSelected; } m.type = ES_MSG_LIST_VIEW_SELECT; if (flags & ES_LIST_VIEW_FIXED_ITEMS) { fixedItemSelection = m.selectItem.index; EsAssert((uintptr_t) m.selectItem.index < fixedItems.Length()); m.selectItem.index = fixedItemIndices[m.selectItem.index]; } EsMessageSend(this, &m); ignore:; if (start.iterateIndex.index == end.iterateIndex.index) { break; } IterateForwards(&start); linearIndex++; EsAssert(start.iterateIndex.group == end.iterateIndex.group); // The from and to selection indices in the group were incorrectly ordered. } } } void SelectPreview(intptr_t singleItem = -1) { if (!hasSelectionBoxAnchor) { return; } int64_t x1 = selectionBoxPositionX, x2 = selectionBoxAnchorX, y1 = selectionBoxPositionY, y2 = selectionBoxAnchorY; if (x1 > x2) { int64_t temp = x1; x1 = x2; x2 = temp; } if (y1 > y2) { int64_t temp = y1; y1 = y2; y2 = temp; } x1 -= scroll.position[0], x2 -= scroll.position[0]; y1 -= scroll.position[1], y2 -= scroll.position[1]; EsRectangle bounds = GetListBounds(); if (x1 < -1000) x1 = -1000; if (x2 < -1000) x2 = -1000; if (y1 < -1000) y1 = -1000; if (y2 < -1000) y2 = -1000; if (x1 > bounds.r + 1000) x1 = bounds.r + 1000; if (x2 > bounds.r + 1000) x2 = bounds.r + 1000; if (y1 > bounds.b + 1000) y1 = bounds.b + 1000; if (y2 > bounds.b + 1000) y2 = bounds.b + 1000; selectionBox->InternalMove(x2 - x1, y2 - y1, x1, y1); for (uintptr_t i = 0; i < visibleItems.Length(); i++) { if (singleItem != -1) { i = singleItem; } EsMessage m = { ES_MSG_LIST_VIEW_IS_SELECTED }; m.selectItem.index = visibleItems[i].index; m.selectItem.group = visibleItems[i].group; EsMessageSend(this, &m); EsElement *item = visibleItems[i].element; if (x1 < item->offsetX + item->width && x2 >= item->offsetX && y1 < item->offsetY + item->height && y2 >= item->offsetY) { if (EsKeyboardIsCtrlHeld()) { m.selectItem.isSelected = !m.selectItem.isSelected; } else { m.selectItem.isSelected = true; } } if (m.selectItem.isSelected) { item->customStyleState |= THEME_STATE_SELECTED; } else { item->customStyleState &= ~THEME_STATE_SELECTED; } item->MaybeRefreshStyle(); if (singleItem != -1) { break; } } } intptr_t GetItemsPerBand() { if (~flags & ES_LIST_VIEW_TILED) { return 1; } else { int64_t wrapLimit = GetWrapLimit(); int64_t fixedMinorSize = (flags & ES_LIST_VIEW_HORIZONTAL) ? fixedHeight : fixedWidth; intptr_t itemsPerBand = fixedMinorSize && ((fixedMinorSize + style->gapMinor) < wrapLimit) ? (wrapLimit / (fixedMinorSize + style->gapMinor)) : 1; return MinimumInteger(itemsPerBand, maximumItemsPerBand); } } void SelectBox(int64_t x1, int64_t x2, int64_t y1, int64_t y2, bool toggle) { if (!totalItemCount) { return; } EsRectangle contentBounds = GetListBounds(); int64_t offset = 0; EsMessage start, end; bool noItems = false; if (flags & ES_LIST_VIEW_HORIZONTAL) { if (y1 >= contentBounds.b - style->insets.b || y2 < contentBounds.t + style->insets.t) { return; } } else if (flags & ES_LIST_VIEW_COLUMNS) { if (x1 >= contentBounds.l + style->insets.l + totalColumnWidth || x2 < contentBounds.l + style->insets.l) { return; } } else { if (x1 >= contentBounds.r - style->insets.r || x2 < contentBounds.l + style->insets.l) { return; } } // TODO Use reference for FindFirstVisibleItem. bool adjustStart = false, adjustEnd = false; int r1 = (flags & ES_LIST_VIEW_HORIZONTAL) ? style->insets.l - x1 : style->insets.t - y1 + scroll.fixedViewport[1]; int r2 = (flags & ES_LIST_VIEW_HORIZONTAL) ? style->insets.l - x2 : style->insets.t - y2 + scroll.fixedViewport[1]; start = FindFirstVisibleItem(&offset, r1, nullptr, &noItems); if (noItems) return; adjustStart = -offset >= MeasureItems(start.iterateIndex.group, start.iterateIndex.index, 1); end = FindFirstVisibleItem(&offset, r2, nullptr, &noItems); adjustEnd = !noItems; if (noItems) { end.iterateIndex.group = groups.Length() - 1; GetLastIndex(&end); } if (flags & ES_LIST_VIEW_TILED) { int64_t wrapLimit = GetWrapLimit(); int64_t fixedMinorSize = (flags & ES_LIST_VIEW_HORIZONTAL) ? fixedHeight : fixedWidth; intptr_t itemsPerBand = GetItemsPerBand(); int64_t centerOffset = (flags & ES_LIST_VIEW_CENTER_TILES) ? (wrapLimit - itemsPerBand * (fixedMinorSize + style->gapMinor) + style->gapMinor) / 2 : 0; int64_t minorStartOffset = centerOffset + ((flags & ES_LIST_VIEW_HORIZONTAL) ? style->insets.t : style->insets.l); int64_t s0 = (flags & ES_LIST_VIEW_HORIZONTAL) ? y1 : x1; int64_t s1 = (flags & ES_LIST_VIEW_HORIZONTAL) ? y2 : x2; int64_t startEdge = minorStartOffset; int64_t endEdge = minorStartOffset + (fixedMinorSize + style->gapMinor) * itemsPerBand - style->gapMinor; if (s1 < startEdge || s0 >= endEdge) return; if (s0 < startEdge) s0 = startEdge; if (s1 >= endEdge) s1 = endEdge - 1; intptr_t startInBand = (s0 - startEdge + style->gapMinor /* round up if in gap */) / (fixedMinorSize + style->gapMinor); intptr_t endInBand = (s1 - startEdge) / (fixedMinorSize + style->gapMinor); if (startInBand > endInBand) return; if (adjustStart) { ListViewGroup *group = &groups[start.iterateIndex.group]; if (((group->flags & ES_LIST_VIEW_GROUP_HAS_HEADER) && start.iterateIndex.index == 0) || ((group->flags & ES_LIST_VIEW_GROUP_HAS_FOOTER) && start.iterateIndex.index == group->itemCount - 1)) { IterateForwards(&start); } else { for (intptr_t i = 0; i < itemsPerBand; i++) { if ((group->flags & ES_LIST_VIEW_GROUP_HAS_FOOTER) && start.iterateIndex.index == group->itemCount - 1) { break; } IterateForwards(&start); } } } if (adjustEnd) { ListViewGroup *group = &groups[end.iterateIndex.group]; if (((group->flags & ES_LIST_VIEW_GROUP_HAS_HEADER) && end.iterateIndex.index == 0) || ((group->flags & ES_LIST_VIEW_GROUP_HAS_FOOTER) && end.iterateIndex.index == group->itemCount - 1)) { } else { for (intptr_t i = 0; i < itemsPerBand - 1; i++) { if ((group->flags & ES_LIST_VIEW_GROUP_HAS_FOOTER) && end.iterateIndex.index == group->itemCount - 1) { IterateBackwards(&end); break; } IterateForwards(&end); } } } if ((start.iterateIndex.group == end.iterateIndex.group && start.iterateIndex.index > end.iterateIndex.index) || (start.iterateIndex.group > end.iterateIndex.group)) { return; } SetSelected(start.iterateIndex.group, start.iterateIndex.index, end.iterateIndex.group, end.iterateIndex.index, true, toggle, itemsPerBand, startInBand, endInBand); } else { if (adjustStart) { IterateForwards(&start); } SetSelected(start.iterateIndex.group, start.iterateIndex.index, end.iterateIndex.group, end.iterateIndex.index, true, toggle); } } void UpdateVisibleItemSelectionState(uintptr_t i) { EsMessage m = {}; m.type = ES_MSG_LIST_VIEW_IS_SELECTED; m.selectItem.group = visibleItems[i].group; m.selectItem.index = visibleItems[i].index; EsMessageSend(this, &m); if (m.selectItem.isSelected) { visibleItems[i].element->customStyleState |= THEME_STATE_SELECTED; } else { visibleItems[i].element->customStyleState &= ~THEME_STATE_SELECTED; } visibleItems[i].element->MaybeRefreshStyle(); } void UpdateVisibleItemsSelectionState() { for (uintptr_t i = 0; i < visibleItems.Length(); i++) { UpdateVisibleItemSelectionState(i); } } void Select(EsListViewIndex group, EsListViewIndex index, bool range, bool toggle, bool moveAnchorOnly) { if ((~flags & ES_LIST_VIEW_SINGLE_SELECT) && (~flags & ES_LIST_VIEW_MULTI_SELECT) && (~flags & ES_LIST_VIEW_CHOICE_SELECT)) { return; } if (!totalItemCount) { return; } if (!hasAnchorItem || (~flags & ES_LIST_VIEW_MULTI_SELECT)) { range = false; } bool emptySpace = false; if (group == -1) { // Clicked on empty space. if (range || toggle) return; emptySpace = true; } if (!range && !emptySpace) { hasAnchorItem = true; anchorItemGroup = group; anchorItemIndex = index; } if (moveAnchorOnly) { return; } if (!toggle) { // Clear existing selection. SetSelected(0, 0, groups.Length() - 1, groups.Last().itemCount - 1, false, false); } if (range) { // Select range. SetSelected(anchorItemGroup, anchorItemIndex, group, index, true, false); } else if (toggle) { // Toggle single item. SetSelected(group, index, group, index, false, true); } else if (!emptySpace) { // Select single item. SetSelected(group, index, group, index, true, false); } UpdateVisibleItemsSelectionState(); } int ProcessItemMessage(uintptr_t visibleIndex, EsMessage *message, ListViewItemElement *element) { ListViewItem *item = &visibleItems[visibleIndex]; if (message->type == ES_MSG_PAINT) { EsMessage m = { ES_MSG_LIST_VIEW_GET_CONTENT }; uint8_t _buffer[512]; EsBuffer buffer = { .out = _buffer, .bytes = sizeof(_buffer) }; m.getContent.buffer = &buffer; m.getContent.index = item->index; m.getContent.group = item->group; EsTextSelection selection = {}; selection.hideCaret = true; if (searchBufferBytes && item->showSearchHighlight) { // TODO We might need to store the matched bytes per item, because of case insensitivity. selection.caret1 = searchBufferBytes; selection.hideCaret = false; } if (flags & ES_LIST_VIEW_COLUMNS) { EsRectangle bounds = EsRectangleAddBorder(element->GetBounds(), element->style->insets); for (uintptr_t i = item->startAtSecondColumn ? 1 : 0; i < activeColumns.Length(); i++) { m.getContent.activeColumnIndex = i; m.getContent.columnID = registeredColumns[activeColumns[i]].id; m.getContent.icon = 0; buffer.position = 0; bounds.r = bounds.l + registeredColumns[activeColumns[i]].width * theming.scale - element->style->insets.r - element->style->insets.l; if (i == 0) { bounds.r -= item->indent * style->gapWrap; } EsRectangle drawBounds = { bounds.l + message->painter->offsetX, bounds.r + message->painter->offsetX, bounds.t + message->painter->offsetY, bounds.b + message->painter->offsetY }; if (EsRectangleClip(drawBounds, message->painter->clip, nullptr) && ES_HANDLED == EsMessageSend(this, &m)) { bool useSelectedCellStyle = (item->element->customStyleState & THEME_STATE_SELECTED) && (flags & ES_LIST_VIEW_CHOICE_SELECT); UIStyle *style = useSelectedCellStyle ? selectedCellStyle : i ? secondaryCellStyle : primaryCellStyle; style->PaintText(message->painter, element, bounds, (char *) _buffer, buffer.position, m.getContent.icon, registeredColumns[activeColumns[i]].flags, i ? nullptr : &selection); } bounds.l += registeredColumns[activeColumns[i]].width * theming.scale + secondaryCellStyle->gapMajor; if (i == 0) { bounds.l -= item->indent * style->gapWrap; } } } else { if (flags & ES_LIST_VIEW_TILED) { m.type = ES_MSG_LIST_VIEW_GET_SUMMARY; if (ES_HANDLED == EsMessageSend(this, &m)) goto standardPaint; m.type = ES_MSG_LIST_VIEW_GET_CONTENT; } if (ES_HANDLED == EsMessageSend(this, &m)) goto standardPaint; return 0; standardPaint:; if (inlineTextbox && inlineTextboxGroup == item->group && inlineTextboxIndex == item->index) { buffer.position = 0; } EsDrawContent(message->painter, element, element->GetBounds(), (char *) _buffer, buffer.position, m.getContent.icon, m.getContent.drawContentFlags, &selection); } } else if (message->type == ES_MSG_LAYOUT) { if (element->GetChildCount()) { EsElement *child = element->GetChild(0); EsRectangle bounds = element->GetBounds(); child->InternalMove(bounds.r - bounds.l, bounds.b - bounds.t, bounds.l, bounds.t); } } else if (message->type == ES_MSG_MOUSE_LEFT_DOWN || message->type == ES_MSG_MOUSE_MIDDLE_CLICK) { EsElementFocus(this); if (hasFocusedItem) { ListViewItem *oldFocus = FindVisibleItem(focusedItemGroup, focusedItemIndex); if (oldFocus) { oldFocus->element->customStyleState &= ~THEME_STATE_FOCUSED_ITEM; oldFocus->element->MaybeRefreshStyle(); } } hasFocusedItem = true; focusedItemGroup = item->group; focusedItemIndex = item->index; element->customStyleState |= THEME_STATE_FOCUSED_ITEM; if (message->type == ES_MSG_MOUSE_MIDDLE_CLICK) { Select(item->group, item->index, false, false, false); EsMessage m = { ES_MSG_LIST_VIEW_CHOOSE_ITEM }; m.chooseItem.group = item->group; m.chooseItem.index = item->index; m.chooseItem.source = ES_LIST_VIEW_CHOOSE_ITEM_MIDDLE_CLICK; EsMessageSend(this, &m); } else if (message->mouseDown.clickChainCount == 1 || (~element->customStyleState & THEME_STATE_SELECTED)) { Select(item->group, item->index, EsKeyboardIsShiftHeld(), EsKeyboardIsCtrlHeld(), false); } else if (message->mouseDown.clickChainCount == 2) { EsMessage m = { ES_MSG_LIST_VIEW_CHOOSE_ITEM }; m.chooseItem.group = item->group; m.chooseItem.index = item->index; m.chooseItem.source = ES_LIST_VIEW_CHOOSE_ITEM_DOUBLE_CLICK; EsMessageSend(this, &m); } } else if (message->type == ES_MSG_MOUSE_MIDDLE_DOWN) { } else if (message->type == ES_MSG_MOUSE_RIGHT_DOWN) { EsMessage m = { ES_MSG_LIST_VIEW_IS_SELECTED }; m.selectItem.index = item->index; m.selectItem.group = item->group; EsMessageSend(this, &m); if (!m.selectItem.isSelected) { Select(item->group, item->index, EsKeyboardIsShiftHeld(), EsKeyboardIsCtrlHeld(), false); } m.type = ES_MSG_LIST_VIEW_CONTEXT_MENU; EsMessageSend(this, &m); } else if (message->type == ES_MSG_MOUSE_LEFT_DRAG) { if (flags & ES_LIST_VIEW_CHOICE_SELECT) { window->pressed = this; ProcessMessage(message); } } else if (message->type == ES_MSG_GET_INSPECTOR_INFORMATION) { EsMessage m = { ES_MSG_LIST_VIEW_GET_CONTENT }; uint8_t _buffer[256]; EsBuffer buffer = { .out = _buffer, .bytes = sizeof(_buffer) }; m.getContent.buffer = &buffer; m.getContent.index = item->index; m.getContent.group = item->group; EsMessageSend(this, &m); EsBufferFormat(message->getContent.buffer, "index %d '%s'", item->index, buffer.position, buffer.out); } else { return 0; } return ES_HANDLED; } inline int GetWrapLimit() { EsRectangle bounds = GetListBounds(); return (flags & ES_LIST_VIEW_HORIZONTAL) ? bounds.b - bounds.t - style->insets.b - style->insets.t : bounds.r - bounds.l - style->insets.r - style->insets.l; } ListViewItem *FindVisibleItem(EsListViewIndex group, EsListViewIndex index) { for (uintptr_t i = 0; i < visibleItems.Length(); i++) { ListViewItem *item = &visibleItems[i]; if (item->group == group && item->index == index) { return item; } } return nullptr; } bool Search() { uint8_t _buffer[64]; if (!hasFocusedItem) { // Select the first item in the list. KeyInput(ES_SCANCODE_DOWN_ARROW, false, false, false, true); if (!hasFocusedItem) return false; } EsMessage m = { ES_MSG_LIST_VIEW_SEARCH }; m.searchItem.index = focusedItemIndex; m.searchItem.group = focusedItemGroup; m.searchItem.query = searchBuffer; m.searchItem.queryBytes = searchBufferBytes; int response = EsMessageSend(this, &m); if (response == ES_REJECTED) { return false; } ListViewItem *oldFocus = FindVisibleItem(focusedItemGroup, focusedItemIndex); if (oldFocus) { oldFocus->element->customStyleState &= ~THEME_STATE_FOCUSED_ITEM; oldFocus->element->MaybeRefreshStyle(); } bool found = false; if (response == ES_HANDLED) { focusedItemIndex = m.searchItem.index; focusedItemGroup = m.searchItem.group; found = true; } else { EsMessage m = { ES_MSG_LIST_VIEW_GET_CONTENT }; EsBuffer buffer = { .out = _buffer, .bytes = sizeof(_buffer) }; m.getContent.buffer = &buffer; m.getContent.index = focusedItemIndex; m.getContent.group = focusedItemGroup; EsMessage m2 = {}; m2.iterateIndex.index = focusedItemIndex; m2.iterateIndex.group = focusedItemGroup; do { buffer.position = 0; EsMessageSend(this, &m); if (StringStartsWith((char *) _buffer, buffer.position, searchBuffer, searchBufferBytes, true)) { found = true; break; } if (!IterateForwards(&m2)) { m2.iterateIndex.group = 0; GetFirstIndex(&m2); } m.getContent.index = m2.iterateIndex.index; m.getContent.group = m2.iterateIndex.group; } while (m.getContent.index != focusedItemIndex || m.getContent.group != focusedItemGroup); focusedItemIndex = m.getContent.index; focusedItemGroup = m.getContent.group; EnsureItemVisible(focusedItemGroup, focusedItemIndex, ES_FLAGS_DEFAULT); } ListViewItem *newFocus = FindVisibleItem(focusedItemGroup, focusedItemIndex); if (newFocus) { newFocus->element->customStyleState |= THEME_STATE_FOCUSED_ITEM; newFocus->element->MaybeRefreshStyle(); } { EsMessage m = { ES_MSG_LIST_VIEW_GET_CONTENT }; EsBuffer buffer = { .out = _buffer, .bytes = sizeof(_buffer) }; m.getContent.buffer = &buffer; for (uintptr_t i = 0; i < visibleItems.Length(); i++) { ListViewItem *item = &visibleItems[i]; m.getContent.index = item->index; m.getContent.group = item->group; buffer.position = 0; EsMessageSend(this, &m); bool shouldShowSearchHighlight = StringStartsWith((char *) _buffer, buffer.position, searchBuffer, searchBufferBytes, true); if (shouldShowSearchHighlight || (!shouldShowSearchHighlight && item->showSearchHighlight)) { item->showSearchHighlight = shouldShowSearchHighlight; item->element->Repaint(true); } } } Select(-1, 0, false, false, false); Select(focusedItemGroup, focusedItemIndex, false, false, false); return found; } bool KeyInput(int scancode, bool ctrl, bool alt, bool shift, bool keepSearchBuffer = false) { if (!totalItemCount || alt) { return false; } if (scancode == ES_SCANCODE_BACKSPACE && searchBufferBytes) { searchBufferBytes = 0; Search(); return true; } bool isNext = false, isPrevious = false, isNextBand = false, isPreviousBand = false, isHome = scancode == ES_SCANCODE_HOME, isEnd = scancode == ES_SCANCODE_END, isPageUp = scancode == ES_SCANCODE_PAGE_UP, isPageDown = scancode == ES_SCANCODE_PAGE_DOWN, isSpace = scancode == ES_SCANCODE_SPACE, isEnter = scancode == ES_SCANCODE_ENTER; if (flags & ES_LIST_VIEW_HORIZONTAL) { isNext = scancode == ES_SCANCODE_DOWN_ARROW; isNextBand = scancode == ES_SCANCODE_RIGHT_ARROW; isPrevious = scancode == ES_SCANCODE_UP_ARROW; isPreviousBand = scancode == ES_SCANCODE_LEFT_ARROW; } else { isNext = scancode == ES_SCANCODE_RIGHT_ARROW; isNextBand = scancode == ES_SCANCODE_DOWN_ARROW; isPrevious = scancode == ES_SCANCODE_LEFT_ARROW; isPreviousBand = scancode == ES_SCANCODE_UP_ARROW; } if (hasSelectionBoxAnchor) { if (scancode == ES_SCANCODE_UP_ARROW) scroll.SetY(scroll.position[1] - GetConstantNumber("scrollKeyMovement")); if (scancode == ES_SCANCODE_DOWN_ARROW) scroll.SetY(scroll.position[1] + GetConstantNumber("scrollKeyMovement")); if (scancode == ES_SCANCODE_LEFT_ARROW) scroll.SetX(scroll.position[0] - GetConstantNumber("scrollKeyMovement")); if (scancode == ES_SCANCODE_RIGHT_ARROW) scroll.SetX(scroll.position[0] + GetConstantNumber("scrollKeyMovement")); if (scancode == ES_SCANCODE_PAGE_UP) scroll.SetY(scroll.position[1] - Height(GetBounds())); if (scancode == ES_SCANCODE_PAGE_DOWN) scroll.SetY(scroll.position[1] + Height(GetBounds())); if (flags & ES_LIST_VIEW_HORIZONTAL) { if (scancode == ES_SCANCODE_HOME) scroll.SetX(0); if (scancode == ES_SCANCODE_END) scroll.SetX(scroll.limit[0]); } else { if (scancode == ES_SCANCODE_HOME) scroll.SetY(0); if (scancode == ES_SCANCODE_END) scroll.SetY(scroll.limit[1]); } } else if (isPrevious || isNext || isHome || isEnd || isPageUp || isPageDown || isNextBand || isPreviousBand) { ListViewItem *oldFocus = FindVisibleItem(focusedItemGroup, focusedItemIndex); if (oldFocus) { oldFocus->element->customStyleState &= ~THEME_STATE_FOCUSED_ITEM; oldFocus->element->MaybeRefreshStyle(); } EsMessage m = {}; if (hasFocusedItem && (isPrevious || isNext || isPageUp || isPageDown || isNextBand || isPreviousBand)) { m.iterateIndex.group = focusedItemGroup; m.iterateIndex.index = focusedItemIndex; uintptr_t itemsPerBand = GetItemsPerBand(); for (uintptr_t i = 0; i < ((isPageUp || isPageDown) ? (10 * itemsPerBand) : (isNextBand || isPreviousBand) ? itemsPerBand : 1); i++) { if (isNext || isPageDown || isNextBand) IterateForwards(&m); else IterateBackwards(&m); } } else { if (isNext || isNextBand || isHome) { m.iterateIndex.group = 0; GetFirstIndex(&m); } else { m.iterateIndex.group = groups.Length() - 1; GetLastIndex(&m); } } hasFocusedItem = true; focusedItemGroup = m.iterateIndex.group; focusedItemIndex = m.iterateIndex.index; ListViewItem *newFocus = FindVisibleItem(focusedItemGroup, focusedItemIndex); if (newFocus) { newFocus->element->customStyleState |= THEME_STATE_FOCUSED_ITEM; newFocus->element->MaybeRefreshStyle(); } if (!keepSearchBuffer) ClearSearchBuffer(); EnsureItemVisible(focusedItemGroup, focusedItemIndex, (isPrevious || isHome || isPageUp || isPreviousBand) ? ENSURE_VISIBLE_ALIGN_TOP : ES_FLAGS_DEFAULT); Select(focusedItemGroup, focusedItemIndex, shift, ctrl, ctrl && !shift); return true; } else if (isSpace && ctrl && !shift && hasFocusedItem) { Select(focusedItemGroup, focusedItemIndex, false, true, false); return true; } else if (isEnter && hasFocusedItem) { if (searchBufferBytes) { searchBufferLastKeyTime = 0; searchBufferBytes = 0; EsListViewInvalidateAll(this); } EsMessage m = { ES_MSG_LIST_VIEW_CHOOSE_ITEM }; m.chooseItem.group = focusedItemGroup; m.chooseItem.index = focusedItemIndex; m.chooseItem.source = ES_LIST_VIEW_CHOOSE_ITEM_ENTER; EsMessageSend(this, &m); return true; } else if (!ctrl && !alt) { uint64_t currentTime = EsTimeStampMs(); if (searchBufferLastKeyTime + GetConstantNumber("listViewSearchBufferTimeout") < currentTime) { searchBufferBytes = 0; } StartAnimating(); const char *inputString = KeyboardLayoutLookup(scancode, shift, false, false, false); size_t inputStringBytes = EsCStringLength(inputString); if (inputString && searchBufferBytes + inputStringBytes < sizeof(searchBuffer)) { searchBufferLastKeyTime = currentTime; EsMemoryCopy(searchBuffer + searchBufferBytes, inputString, inputStringBytes); size_t previousSearchBufferBytes = searchBufferBytes; searchBufferBytes += inputStringBytes; if (!Search()) searchBufferBytes = previousSearchBufferBytes; return true; } } return false; } void ClearSearchBuffer() { searchBufferBytes = 0; for (uintptr_t i = 0; i < visibleItems.Length(); i++) { if (visibleItems[i].showSearchHighlight) { visibleItems[i].showSearchHighlight = false; visibleItems[i].element->Repaint(true); } } } void DragSelect() { EsRectangle bounds = GetWindowBounds(); EsPoint mouse = EsMouseGetPosition(window); if (mouse.x < bounds.l) mouse.x = bounds.l; if (mouse.x >= bounds.r) mouse.x = bounds.r - 1; if (visibleItems.Length()) { int32_t start = visibleItems[0].element->GetWindowBounds().t; int32_t end = visibleItems.Last().element->GetWindowBounds().b; if (mouse.y < start) mouse.y = start; if (mouse.y >= end) mouse.y = end - 1; } EsElement *hoverItem = UIFindHoverElementRecursively(this, bounds.l - offsetX, bounds.t - offsetY, mouse); if (hoverItem && hoverItem->messageClass == ListViewProcessItemMessage) { EsMessage m = {}; m.type = ES_MSG_MOUSE_LEFT_DOWN; EsMessageSend(hoverItem, &m); } } void MoveInlineTextbox(ListViewItem *item) { UIStyle *style = item->element->style; if (flags & ES_LIST_VIEW_COLUMNS) { int offset = primaryCellStyle->metrics->iconSize + primaryCellStyle->gapMinor + style->insets.l - inlineTextbox->style->insets.l; inlineTextbox->InternalMove(registeredColumns[activeColumns[0]].width * theming.scale - offset, item->element->height, item->element->offsetX + offset, item->element->offsetY); } else if (flags & ES_LIST_VIEW_TILED) { if (style->metrics->layoutVertical) { int height = inlineTextbox->style->preferredHeight; int textStart = style->metrics->iconSize + style->gapMinor + style->insets.t; int textEnd = item->element->height - style->insets.b; int offset = (textStart + textEnd - height) / 2; inlineTextbox->InternalMove(item->element->width - style->insets.r - style->insets.l, height, item->element->offsetX + style->insets.l, item->element->offsetY + offset); } else { int textboxInset = inlineTextbox->style->insets.l; int offset = style->metrics->iconSize + style->gapMinor + style->insets.l - textboxInset; int height = inlineTextbox->style->preferredHeight; inlineTextbox->InternalMove(item->element->width - offset - style->insets.r + textboxInset, height, item->element->offsetX + offset, item->element->offsetY + (item->element->height - height) / 2); } } else { inlineTextbox->InternalMove(item->element->width, item->element->height, item->element->offsetX, item->element->offsetY); } } int ProcessMessage(EsMessage *message) { int response = scroll.ReceivedMessage(message); if (response) return response; if (message->type == ES_MSG_GET_WIDTH || message->type == ES_MSG_GET_HEIGHT) { if (flags & ES_LIST_VIEW_HORIZONTAL) { message->measure.width = totalSize + style->insets.l + style->insets.r; } else { message->measure.height = totalSize + style->insets.t + style->insets.b; if (flags & ES_LIST_VIEW_COLUMNS) { message->measure.width = totalColumnWidth + style->insets.l + style->insets.r; message->measure.height += columnHeader->style->preferredHeight; } } } else if (message->type == ES_MSG_LAYOUT) { firstLayout = true; Wrap(message->layout.sizeChanged); if (columnHeader) { columnHeader->InternalMove(Width(GetBounds()), columnHeader->style->preferredHeight, 0, 0); } Populate(); } else if (message->type == ES_MSG_SCROLL_X || message->type == ES_MSG_SCROLL_Y) { int64_t delta = message->scroll.scroll - message->scroll.previous; if ((message->type == ES_MSG_SCROLL_X) == ((flags & ES_LIST_VIEW_HORIZONTAL) ? true : false)) { for (uintptr_t i = 0; i < visibleItems.Length(); i++) { if (flags & ES_LIST_VIEW_HORIZONTAL) visibleItems[i].element->offsetX -= delta; else visibleItems[i].element->offsetY -= delta; } } useScrollItem = false; Populate(); Repaint(true); if (columnHeader && message->type == ES_MSG_SCROLL_X) { EsElementRelayout(columnHeader); } if (selectionBox) { EsPoint position = EsMouseGetPosition(this); selectionBoxPositionX = position.x + scroll.position[0]; selectionBoxPositionY = position.y + scroll.position[1]; SelectPreview(); } } else if (message->type == ES_MSG_DESTROY) { for (uintptr_t i = 0; i < visibleItems.Length(); i++) { visibleItems[i].element->Destroy(); } for (uintptr_t i = 0; i < registeredColumns.Length(); i++) { if ((registeredColumns[i].flags & ES_LIST_VIEW_COLUMN_DATA_MASK) == ES_LIST_VIEW_COLUMN_DATA_STRINGS) { for (uintptr_t j = 0; j < registeredColumns[i].items.Length(); j++) { EsHeapFree(registeredColumns[i].items[j].s.string); } } EsHeapFree(registeredColumns[i].title); registeredColumns[i].items.Free(); } EsHeapFree(emptyMessage); primaryCellStyle->CloseReference(); secondaryCellStyle->CloseReference(); selectedCellStyle->CloseReference(); fixedItems.Free(); fixedItemIndices.Free(); visibleItems.Free(); groups.Free(); activeColumns.Free(); registeredColumns.Free(); if (EsElementIsFocused(this)) { EsCommandSetCallback(EsCommandByID(instance, ES_COMMAND_SELECT_ALL), nullptr); } } else if (message->type == ES_MSG_KEY_UP) { if (message->keyboard.scancode == ES_SCANCODE_LEFT_CTRL || message->keyboard.scancode == ES_SCANCODE_RIGHT_CTRL) { SelectPreview(); } } else if (message->type == ES_MSG_KEY_DOWN) { if (message->keyboard.scancode == ES_SCANCODE_LEFT_CTRL || message->keyboard.scancode == ES_SCANCODE_RIGHT_CTRL) { SelectPreview(); } if (message->keyboard.modifiers & ~(ES_MODIFIER_CTRL | ES_MODIFIER_ALT | ES_MODIFIER_SHIFT)) { // Unused modifier. return 0; } return KeyInput(message->keyboard.scancode, message->keyboard.modifiers & ES_MODIFIER_CTRL, message->keyboard.modifiers & ES_MODIFIER_ALT, message->keyboard.modifiers & ES_MODIFIER_SHIFT) ? ES_HANDLED : 0; } else if (message->type == ES_MSG_FOCUSED_START) { if (!hasFocusedItem && groups.Length() && (message->focus.flags & ES_ELEMENT_FOCUS_FROM_KEYBOARD)) { hasFocusedItem = true; focusedItemGroup = 0; focusedItemIndex = 0; } for (uintptr_t i = 0; i < visibleItems.Length(); i++) { ListViewItem *item = &visibleItems[i]; item->element->customStyleState |= THEME_STATE_LIST_FOCUSED; if (hasFocusedItem && focusedItemGroup == item->group && focusedItemIndex == item->index) { item->element->customStyleState |= THEME_STATE_FOCUSED_ITEM; } item->element->MaybeRefreshStyle(); } EsCommand *command = EsCommandByID(instance, ES_COMMAND_SELECT_ALL); command->data = this; EsCommandSetCallback(command, [] (EsInstance *, EsElement *, EsCommand *command) { EsListView *list = (EsListView *) command->data.p; if (!list->groups.Length() || !list->totalItemCount) return; list->SetSelected(0, 0, list->groups.Length() - 1, list->groups.Last().itemCount - 1, true, false); list->UpdateVisibleItemsSelectionState(); }); EsCommandSetDisabled(command, false); } else if (message->type == ES_MSG_FOCUSED_END) { for (uintptr_t i = 0; i < visibleItems.Length(); i++) { ListViewItem *item = &visibleItems[i]; item->element->customStyleState &= ~(THEME_STATE_LIST_FOCUSED | THEME_STATE_FOCUSED_ITEM); item->element->MaybeRefreshStyle(); } // Also done in ES_MSG_DESTROY: EsCommandSetCallback(EsCommandByID(instance, ES_COMMAND_SELECT_ALL), nullptr); } else if (message->type == ES_MSG_MOUSE_RIGHT_DOWN) { // Make sure that right clicking will focus the list. } else if (message->type == ES_MSG_MOUSE_LEFT_DOWN) { Select(-1, 0, EsKeyboardIsShiftHeld(), EsKeyboardIsCtrlHeld(), false); } else if (message->type == ES_MSG_MOUSE_LEFT_DRAG) { if (selectionBox) { Repaint(false, ES_RECT_4(selectionBox->offsetX, selectionBox->offsetX + selectionBox->width, selectionBox->offsetY, selectionBox->offsetY + selectionBox->height)); if (!hasSelectionBoxAnchor) { hasSelectionBoxAnchor = true; selectionBoxAnchorX = message->mouseDragged.originalPositionX + scroll.position[0]; selectionBoxAnchorY = message->mouseDragged.originalPositionY + scroll.position[1]; if (gui.lastClickButton == ES_MSG_MOUSE_LEFT_DOWN) { Select(-1, 0, EsKeyboardIsShiftHeld(), EsKeyboardIsCtrlHeld(), false); } } EsElementSetDisabled(selectionBox, false); selectionBoxPositionX = message->mouseDragged.newPositionX + scroll.position[0]; selectionBoxPositionY = message->mouseDragged.newPositionY + scroll.position[1]; // Inclusive rectangle. if (selectionBoxPositionX >= selectionBoxAnchorX) selectionBoxPositionX++; if (selectionBoxPositionY >= selectionBoxAnchorY) selectionBoxPositionY++; SelectPreview(); } else if (flags & ES_LIST_VIEW_CHOICE_SELECT) { DragSelect(); } } else if (message->type == ES_MSG_MOUSE_LEFT_UP || message->type == ES_MSG_MOUSE_RIGHT_UP) { if (selectionBox) { EsElementSetDisabled(selectionBox, true); if (hasSelectionBoxAnchor) { hasSelectionBoxAnchor = false; int64_t x1 = selectionBoxPositionX, x2 = selectionBoxAnchorX, y1 = selectionBoxPositionY, y2 = selectionBoxAnchorY; if (x1 > x2) { int64_t temp = x1; x1 = x2; x2 = temp; } if (y1 > y2) { int64_t temp = y1; y1 = y2; y2 = temp; } SelectBox(x1, x2, y1, y2, EsKeyboardIsCtrlHeld()); } for (uintptr_t i = 0; i < visibleItems.Length(); i++) { EsMessage m = { ES_MSG_LIST_VIEW_IS_SELECTED }; m.selectItem.index = visibleItems[i].index; m.selectItem.group = visibleItems[i].group; EsMessageSend(this, &m); EsElement *item = visibleItems[i].element; if (m.selectItem.isSelected) { item->customStyleState |= THEME_STATE_SELECTED; } else { item->customStyleState &= ~THEME_STATE_SELECTED; } item->MaybeRefreshStyle(); } } } else if (message->type == ES_MSG_Z_ORDER) { uintptr_t index = message->zOrder.index; if (index < visibleItems.Length()) { EsAssert(zOrderItems.Length() == visibleItems.Length()); message->zOrder.child = zOrderItems[index]; return ES_HANDLED; } else { index -= visibleItems.Length(); } if (selectionBox) { if (index == 0) return message->zOrder.child = selectionBox, ES_HANDLED; else index--; } if (columnHeader) { if (index == 0) return message->zOrder.child = columnHeader, ES_HANDLED; else index--; } if (inlineTextbox) { if (index == 0) return message->zOrder.child = inlineTextbox, ES_HANDLED; else index--; } message->zOrder.child = nullptr; } else if (message->type == ES_MSG_PAINT && !totalItemCount && emptyMessageBytes) { EsDrawTextThemed(message->painter, this, EsPainterBoundsInset(message->painter), emptyMessage, emptyMessageBytes, ES_STYLE_TEXT_LABEL_SECONDARY, ES_TEXT_H_CENTER | ES_TEXT_V_CENTER | ES_TEXT_WRAP); } else if (message->type == ES_MSG_ANIMATE) { if (scroll.dragScrolling && (flags & ES_LIST_VIEW_CHOICE_SELECT)) { DragSelect(); } uint64_t currentTime = EsTimeStampMs(); int64_t remainingTime = searchBufferLastKeyTime + GetConstantNumber("listViewSearchBufferTimeout") - currentTime; if (remainingTime < 0) { ClearSearchBuffer(); } else { message->animate.waitMs = remainingTime; message->animate.complete = false; } } else if (message->type == ES_MSG_BEFORE_Z_ORDER) { EsAssert(!zOrderItems.Length()); intptr_t focused = -1, hovered = -1; for (uintptr_t i = 0; i < visibleItems.Length(); i++) { if (hasFocusedItem && visibleItems[i].index == focusedItemIndex && visibleItems[i].group == focusedItemGroup) { focused = i; } else if (window->hovered == visibleItems[i].element) { hovered = i; } else { zOrderItems.Add(visibleItems[i].element); } } if (hovered != -1) { zOrderItems.Add(visibleItems[hovered].element); } if (focused != -1) { zOrderItems.Add(visibleItems[focused].element); } } else if (message->type == ES_MSG_AFTER_Z_ORDER) { zOrderItems.Free(); } else if (message->type == ES_MSG_GET_ACCESS_KEY_HINT_BOUNDS) { AccessKeysCenterHint(this, message); } else if (message->type == ES_MSG_UI_SCALE_CHANGED) { primaryCellStyle->CloseReference(); secondaryCellStyle->CloseReference(); selectedCellStyle->CloseReference(); primaryCellStyle = GetStyle(MakeStyleKey(ES_STYLE_LIST_PRIMARY_CELL, 0), false); secondaryCellStyle = GetStyle(MakeStyleKey(ES_STYLE_LIST_SECONDARY_CELL, 0), false); selectedCellStyle = GetStyle(MakeStyleKey(ES_STYLE_LIST_SELECTED_CHOICE_CELL, 0), false); EsListViewChangeStyles(this, 0, 0, 0, 0, ES_FLAGS_DEFAULT, ES_FLAGS_DEFAULT); } else if (message->type == ES_MSG_LIST_VIEW_GET_CONTENT && (activeColumns.Length() || (flags & ES_LIST_VIEW_FIXED_ITEMS))) { uintptr_t index = message->getContent.index; ListViewFixedItemData data = {}; ListViewColumn *column = ®isteredColumns[(flags & ES_LIST_VIEW_COLUMNS) ? activeColumns[message->getContent.activeColumnIndex] : 0]; uint32_t format = column->flags & ES_LIST_VIEW_COLUMN_FORMAT_MASK; uint32_t type = column->flags & ES_LIST_VIEW_COLUMN_DATA_MASK; if (flags & ES_LIST_VIEW_FIXED_ITEMS) { EsAssert(index < fixedItems.Length()); index = fixedItemIndices[index]; ListViewFixedItem *item = &fixedItems[index]; if (index < column->items.Length()) data = column->items[index]; if (!activeColumns.Length() || message->getContent.columnID == registeredColumns[activeColumns[0]].id) { message->getContent.icon = item->iconID; } } else { EsMessage m = { .type = ES_MSG_LIST_VIEW_GET_ITEM_DATA }; m.getItemData.index = message->getContent.index; m.getItemData.group = message->getContent.group; m.getItemData.columnID = message->getContent.columnID; m.getItemData.activeColumnIndex = message->getContent.activeColumnIndex; EsMessageSend(this, &m); if (type == ES_LIST_VIEW_COLUMN_DATA_STRINGS) { data.s.string = (char *) m.getItemData.s; data.s.bytes = m.getItemData.sBytes; } else if (type == ES_LIST_VIEW_COLUMN_DATA_DOUBLES) { data.d = m.getItemData.d; } else if (type == ES_LIST_VIEW_COLUMN_DATA_INTEGERS) { data.i = m.getItemData.i; } if (!activeColumns.Length() || message->getContent.columnID == registeredColumns[activeColumns[0]].id) { message->getContent.icon = m.getItemData.icon; } } #define BOOLEAN_FORMAT(trueString, falseString) \ if (type == ES_LIST_VIEW_COLUMN_DATA_INTEGERS) { \ EsBufferFormat(message->getContent.buffer, "%z", data.i ? interfaceString_ ## trueString : interfaceString_ ## falseString); \ } else { \ EsAssert(false); \ } #define NUMBER_FORMAT(unitString) \ if (type == ES_LIST_VIEW_COLUMN_DATA_INTEGERS) { \ EsBufferFormat(message->getContent.buffer, "%d%z", data.i, interfaceString_ ## unitString); \ } else if (type == ES_LIST_VIEW_COLUMN_DATA_DOUBLES) { \ EsBufferFormat(message->getContent.buffer, "%F%z", data.d, interfaceString_ ## unitString); \ } else { \ EsAssert(false); \ } #define UNIT_FORMAT(unitString1, unitString2, unitString3) \ double d = type == ES_LIST_VIEW_COLUMN_DATA_INTEGERS ? data.i : type == ES_LIST_VIEW_COLUMN_DATA_DOUBLES ? data.d : 0; \ if (d < 10000) EsBufferFormat(message->getContent.buffer, "%F%z", d, interfaceString_ ## unitString1); \ else if (d < 10000000) EsBufferFormat(message->getContent.buffer, "%.F%z", 1, d / 1000, interfaceString_ ## unitString2); \ else EsBufferFormat(message->getContent.buffer, "%.F%z", 1, d / 1000000, interfaceString_ ## unitString3); if (format == ES_LIST_VIEW_COLUMN_FORMAT_DEFAULT) { if (type == ES_LIST_VIEW_COLUMN_DATA_STRINGS) { EsBufferFormat(message->getContent.buffer, "%s", data.s.bytes, data.s.string); } else if (type == ES_LIST_VIEW_COLUMN_DATA_DOUBLES) { EsBufferFormat(message->getContent.buffer, "%F", data.d); } else if (type == ES_LIST_VIEW_COLUMN_DATA_INTEGERS) { EsBufferFormat(message->getContent.buffer, "%d", data.i); } } else if (format == ES_LIST_VIEW_COLUMN_FORMAT_BYTES) { if (type == ES_LIST_VIEW_COLUMN_DATA_INTEGERS) { EsBufferFormat(message->getContent.buffer, "%D", data.i); } else { EsAssert(false); } } else if (format == ES_LIST_VIEW_COLUMN_FORMAT_ENUM_STRING) { if (type == ES_LIST_VIEW_COLUMN_DATA_INTEGERS) { EsAssert(data.i >= 0 && (uintptr_t) data.i < column->enumStringCount); EsBufferFormat(message->getContent.buffer, "%s", column->enumStrings[data.i].stringBytes, column->enumStrings[data.i].string); } else { EsAssert(false); } } else if (format == ES_LIST_VIEW_COLUMN_FORMAT_YES_NO) { BOOLEAN_FORMAT(CommonBooleanYes, CommonBooleanNo); } else if (format == ES_LIST_VIEW_COLUMN_FORMAT_ON_OFF) { BOOLEAN_FORMAT(CommonBooleanOn, CommonBooleanOff); } else if (format == ES_LIST_VIEW_COLUMN_FORMAT_PERCENTAGE) { NUMBER_FORMAT(CommonUnitPercent); } else if (format == ES_LIST_VIEW_COLUMN_FORMAT_BITS) { NUMBER_FORMAT(CommonUnitBits); } else if (format == ES_LIST_VIEW_COLUMN_FORMAT_PIXELS) { NUMBER_FORMAT(CommonUnitPixels); } else if (format == ES_LIST_VIEW_COLUMN_FORMAT_DPI) { NUMBER_FORMAT(CommonUnitDPI); } else if (format == ES_LIST_VIEW_COLUMN_FORMAT_SECONDS) { NUMBER_FORMAT(CommonUnitSeconds); } else if (format == ES_LIST_VIEW_COLUMN_FORMAT_HERTZ) { UNIT_FORMAT(CommonUnitHz, CommonUnitKHz, CommonUnitMHz); } else if (format == ES_LIST_VIEW_COLUMN_FORMAT_BYTE_RATE) { UNIT_FORMAT(CommonUnitBps, CommonUnitKBps, CommonUnitMBps); } else { EsAssert(false); } #undef NUMBER_FORMAT #undef BOOLEAN_FORMAT } else if (message->type == ES_MSG_LIST_VIEW_IS_SELECTED && (flags & ES_LIST_VIEW_FIXED_ITEMS)) { message->selectItem.isSelected = message->selectItem.index == fixedItemSelection; } else if (message->type == ES_MSG_LIST_VIEW_COLUMN_MENU && (flags & ES_LIST_VIEW_FIXED_ITEMS)) { EsMenu *menu = EsMenuCreate(message->columnMenu.source); menu->userData = this; ListViewColumn *column = ®isteredColumns[activeColumns[message->columnMenu.activeColumnIndex]]; uint32_t sortMode = column->flags & ES_LIST_VIEW_COLUMN_SORT_MASK; uint64_t checkAscending = (fixedItemSortDirection == LIST_SORT_DIRECTION_ASCENDING && column->id == fixedItemSortColumnID) ? ES_MENU_ITEM_CHECKED : 0; uint64_t checkDescending = (fixedItemSortDirection == LIST_SORT_DIRECTION_DESCENDING && column->id == fixedItemSortColumnID) ? ES_MENU_ITEM_CHECKED : 0; if (sortMode != ES_LIST_VIEW_COLUMN_SORT_NONE) { EsMenuAddItem(menu, ES_MENU_ITEM_HEADER, INTERFACE_STRING(CommonSortHeader)); if (sortMode == ES_LIST_VIEW_COLUMN_SORT_DEFAULT) { EsMenuAddItem(menu, checkAscending, INTERFACE_STRING(CommonSortAToZ), ListViewSetSortAscending, column->id); EsMenuAddItem(menu, checkDescending, INTERFACE_STRING(CommonSortZToA), ListViewSetSortDescending, column->id); } else if (sortMode == ES_LIST_VIEW_COLUMN_SORT_TIME) { EsMenuAddItem(menu, checkAscending, INTERFACE_STRING(CommonSortOldToNew), ListViewSetSortAscending, column->id); EsMenuAddItem(menu, checkDescending, INTERFACE_STRING(CommonSortNewToOld), ListViewSetSortDescending, column->id); } else if (sortMode == ES_LIST_VIEW_COLUMN_SORT_SIZE) { EsMenuAddItem(menu, checkAscending, INTERFACE_STRING(CommonSortSmallToLarge), ListViewSetSortAscending, column->id); EsMenuAddItem(menu, checkDescending, INTERFACE_STRING(CommonSortLargeToSmall), ListViewSetSortDescending, column->id); } } EsMenuShow(menu); } else { return 0; } return ES_HANDLED; } }; void ListViewPopulateActionCallback(EsElement *element, EsGeneric) { EsListView *view = (EsListView *) element; EsAssert(view->populateQueued); view->populateQueued = false; view->_Populate(); EsAssert(!view->populateQueued); #if 0 if (element->flags & ES_ELEMENT_DEBUG) { EsPrint("Populate complete for list %x with scroll %i:\n", view, view->scroll.position[1]); for (uintptr_t i = 0; i < view->visibleItems.Length(); i++) { EsMessage m = { ES_MSG_LIST_VIEW_GET_CONTENT }; uint8_t _buffer[512]; EsBuffer buffer = { .out = _buffer, .bytes = sizeof(_buffer) }; m.getContent.buffer = &buffer; m.getContent.index = view->visibleItems[i].index; m.getContent.group = view->visibleItems[i].group; EsMessageSend(view, &m); EsPrint("\t%d: %d '%s' at %i\n", i, view->visibleItems[i].index, buffer.position, _buffer, view->visibleItems[i].element->offsetY - view->GetListBounds().t); } } #endif } void ListViewEnsureVisibleActionCallback(EsElement *element, EsGeneric) { EsListView *view = (EsListView *) element; EsAssert(view->ensureVisibleQueued); view->ensureVisibleQueued = false; view->_EnsureItemVisible(view->ensureVisibleGroupIndex, view->ensureVisibleIndex, view->ensureVisibleFlags); EsAssert(!view->ensureVisibleQueued); } int ListViewProcessMessage(EsElement *element, EsMessage *message) { return ((EsListView *) element)->ProcessMessage(message); } int ListViewProcessItemMessage(EsElement *_element, EsMessage *message) { ListViewItemElement *element = (ListViewItemElement *) _element; return ((EsListView *) element->parent)->ProcessItemMessage(element->index, message, element); } void ListViewCalculateTotalColumnWidth(EsListView *view) { view->totalColumnWidth = -view->secondaryCellStyle->gapMajor; for (uintptr_t i = 0; i < view->activeColumns.Length(); i++) { view->totalColumnWidth += view->registeredColumns[view->activeColumns[i]].width * theming.scale + view->secondaryCellStyle->gapMajor; } } int ListViewColumnHeaderMessage(EsElement *element, EsMessage *message) { EsListView *view = (EsListView *) element->userData.p; if (message->type == ES_MSG_LAYOUT) { int x = view->style->insets.l - view->scroll.position[0]; for (uintptr_t i = 0; i < element->children.Length(); i += 2) { EsElement *item = element->children[i], *splitter = element->children[i + 1]; ListViewColumn *column = &view->registeredColumns[item->userData.u]; int splitterLeft = splitter->style->preferredWidth - view->secondaryCellStyle->gapMajor; item->InternalMove(column->width * theming.scale - splitterLeft, element->height, x, 0); splitter->InternalMove(splitter->style->preferredWidth, element->height, x + column->width * theming.scale - splitterLeft, 0); x += column->width * theming.scale + view->secondaryCellStyle->gapMajor; } } else if (message->type == ES_MSG_MOUSE_LEFT_DOWN || message->type == ES_MSG_MOUSE_RIGHT_DOWN) { return ES_HANDLED; } return 0; } void EsListViewChangeStyles(EsListView *view, EsStyleID style, EsStyleID itemStyle, EsStyleID headerItemStyle, EsStyleID footerItemStyle, uint32_t addFlags, uint32_t removeFlags) { // TODO Animating changes. bool wasTiledView = view->flags & ES_LIST_VIEW_TILED; EsAssert(!(addFlags & removeFlags)); view->flags |= addFlags; view->flags &= ~(uint64_t) removeFlags; bool horizontal = view->flags & ES_LIST_VIEW_HORIZONTAL; if (style) view->SetStyle(style, true); if (itemStyle) view->itemStyle = itemStyle; if (headerItemStyle) view->headerItemStyle = headerItemStyle; if (footerItemStyle) view->footerItemStyle = footerItemStyle; GetPreferredSizeFromStylePart(view->itemStyle, &view->fixedWidth, &view->fixedHeight); GetPreferredSizeFromStylePart(view->headerItemStyle, horizontal ? &view->fixedHeaderSize : nullptr, horizontal ? nullptr : &view->fixedHeaderSize); GetPreferredSizeFromStylePart(view->footerItemStyle, horizontal ? &view->fixedFooterSize : nullptr, horizontal ? nullptr : &view->fixedFooterSize); if ((view->flags & ES_LIST_VIEW_MULTI_SELECT) && !view->selectionBox) { view->selectionBox = EsCustomElementCreate(view, ES_CELL_FILL | ES_ELEMENT_DISABLED | ES_ELEMENT_NO_HOVER, ES_STYLE_LIST_SELECTION_BOX); view->selectionBox->cName = "selection box"; } else if ((~view->flags & ES_LIST_VIEW_MULTI_SELECT) && view->selectionBox) { EsElementDestroy(view->selectionBox); view->selectionBox = nullptr; } if ((view->flags & ES_LIST_VIEW_COLUMNS) && !view->columnHeader) { view->columnHeader = EsCustomElementCreate(view, ES_CELL_FILL, ES_STYLE_LIST_COLUMN_HEADER); view->columnHeader->cName = "column header"; view->columnHeader->userData = view; view->columnHeader->messageUser = ListViewColumnHeaderMessage; view->scroll.fixedViewport[1] = view->columnHeader->style->preferredHeight; } else if ((~view->flags & ES_LIST_VIEW_COLUMNS) && view->columnHeader) { EsElementDestroy(view->columnHeader); view->activeColumns.Free(); view->columnHeader = nullptr; view->scroll.fixedViewport[1] = 0; } // It's safe to use ES_SCROLL_MODE_AUTO even in tiled mode, // because decreasing the secondary axis can only increase the primary axis. uint8_t scrollXMode = 0, scrollYMode = 0; if (view->flags & ES_LIST_VIEW_COLUMNS) { scrollXMode = ES_SCROLL_MODE_AUTO; scrollYMode = ES_SCROLL_MODE_AUTO; } else if (view->flags & ES_LIST_VIEW_HORIZONTAL) { scrollXMode = ES_SCROLL_MODE_AUTO; } else { scrollYMode = ES_SCROLL_MODE_AUTO; } view->scroll.Setup(view, scrollXMode, scrollYMode, ES_SCROLL_X_DRAG | ES_SCROLL_Y_DRAG); ListViewCalculateTotalColumnWidth(view); // Remove existing visible items; the list will need to be repopulated. for (uintptr_t i = view->visibleItems.Length(); i > 0; i--) { view->visibleItems[i - 1].element->Destroy(); } view->visibleItems.SetLength(0); // Remeasure each group. if (wasTiledView) { view->totalSize = 0; for (uintptr_t i = 0; i < view->groups.Length(); i++) { view->groups[i].totalSize = 0; } } int64_t spaceDelta = 0; for (uintptr_t i = 0; i < view->groups.Length(); i++) { if (!view->groups[i].itemCount) continue; spaceDelta -= view->groups[i].totalSize; view->groups[i].totalSize = view->MeasureItems(i, 0, view->groups[i].itemCount); view->groups[i].totalSize += view->style->gapMinor * (view->groups[i].itemCount - 1); spaceDelta += view->groups[i].totalSize; } view->InsertSpace(spaceDelta, 0); EsElementRelayout(view); } EsListView *EsListViewCreate(EsElement *parent, uint64_t flags, EsStyleID style, EsStyleID itemStyle, EsStyleID headerItemStyle, EsStyleID footerItemStyle) { EsListView *view = (EsListView *) EsHeapAllocate(sizeof(EsListView), true); if (!view) return nullptr; view->primaryCellStyle = GetStyle(MakeStyleKey(ES_STYLE_LIST_PRIMARY_CELL, 0), false); view->secondaryCellStyle = GetStyle(MakeStyleKey(ES_STYLE_LIST_SECONDARY_CELL, 0), false); view->selectedCellStyle = GetStyle(MakeStyleKey(ES_STYLE_LIST_SELECTED_CHOICE_CELL, 0), false); // Only used for choice list views. view->Initialise(parent, flags | ES_ELEMENT_FOCUSABLE, ListViewProcessMessage, style ?: ES_STYLE_LIST_VIEW); view->cName = "list view"; view->fixedItemSelection = -1; view->maximumItemsPerBand = INT_MAX; if (!itemStyle) { if (flags & ES_LIST_VIEW_CHOICE_SELECT) itemStyle = ES_STYLE_LIST_CHOICE_ITEM; else if (flags & ES_LIST_VIEW_TILED) itemStyle = ES_STYLE_LIST_ITEM_TILE; else itemStyle = ES_STYLE_LIST_ITEM; } EsListViewChangeStyles(view, 0, itemStyle, headerItemStyle ?: ES_STYLE_LIST_ITEM_GROUP_HEADER, footerItemStyle ?: ES_STYLE_LIST_ITEM_GROUP_FOOTER, ES_FLAGS_DEFAULT, ES_FLAGS_DEFAULT); return view; } void EsListViewInsertGroup(EsListView *view, EsListViewIndex group, uint32_t flags) { EsMessageMutexCheck(); // Add the group. ListViewGroup empty = { .flags = flags }; EsAssert(group <= (EsListViewIndex) view->groups.Length()); // Invalid group index. if (!view->groups.Insert(empty, group)) { return; } // Update the group index on visible items. uintptr_t firstVisibleItemToMove = view->visibleItems.Length(); for (uintptr_t i = 0; i < view->visibleItems.Length(); i++) { ListViewItem *item = &view->visibleItems[i]; if (item->group >= group) { item->group++; if (i < firstVisibleItemToMove) { firstVisibleItemToMove = i; } } } // Insert gap between groups. view->InsertSpace(view->groups.Length() > 1 ? view->style->gapMajor : 0, firstVisibleItemToMove); // Create header and footer items. int64_t additionalItems = ((flags & ES_LIST_VIEW_GROUP_HAS_HEADER) ? 1 : 0) + ((flags & ES_LIST_VIEW_GROUP_HAS_FOOTER) ? 1 : 0); EsListViewInsert(view, group, 0, additionalItems); view->groups[group].initialised = true; } void EsListViewInsert(EsListView *view, EsListViewIndex groupIndex, EsListViewIndex firstIndex, EsListViewIndex count) { EsMessageMutexCheck(); if (!count) return; EsAssert(count > 0); // Get the group. EsAssert(groupIndex < (EsListViewIndex) view->groups.Length()); // Invalid group index. ListViewGroup *group = &view->groups[groupIndex]; if (group->initialised) { if (group->flags & ES_LIST_VIEW_GROUP_HAS_HEADER) { EsAssert(firstIndex > 0); // Cannot insert before group header. } if (group->flags & ES_LIST_VIEW_GROUP_HAS_FOOTER) { EsAssert(firstIndex < (intptr_t) group->itemCount); // Cannot insert after group footer. } } // Add the items to the group. bool addedFirstItemInGroup = !group->itemCount; group->itemCount += count; int64_t totalSizeOfItems = view->MeasureItems(groupIndex, firstIndex, count); int64_t sizeToAdd = (count - (addedFirstItemInGroup ? 1 : 0)) * view->style->gapMinor + totalSizeOfItems; group->totalSize += sizeToAdd; view->totalItemCount += count; // Update indices of visible items. uintptr_t firstVisibleItemToMove = view->visibleItems.Length(); if (view->hasFocusedItem && view->focusedItemGroup == groupIndex) { if (view->focusedItemIndex >= firstIndex) { view->focusedItemIndex += count; } } if (view->hasAnchorItem && view->anchorItemGroup == groupIndex) { if (view->anchorItemIndex >= firstIndex) { view->anchorItemIndex += count; } } for (uintptr_t i = 0; i < view->visibleItems.Length(); i++) { ListViewItem *item = &view->visibleItems[i]; if (item->group < groupIndex) { continue; } else if (item->group > groupIndex) { if (i < firstVisibleItemToMove) { firstVisibleItemToMove = i; } break; } if (item->index >= firstIndex) { item->index += count; if (i < firstVisibleItemToMove) { firstVisibleItemToMove = i; } } } // Insert the space for the items. view->InsertSpace(sizeToAdd, firstVisibleItemToMove); } void EsListViewRemove(EsListView *view, EsListViewIndex groupIndex, EsListViewIndex firstIndex, EsListViewIndex count) { EsMessageMutexCheck(); if (!count) return; EsAssert(count > 0); // Get the group. EsAssert(groupIndex < (EsListViewIndex) view->groups.Length()); // Invalid group index. ListViewGroup *group = &view->groups[groupIndex]; if (group->initialised) { if (group->flags & ES_LIST_VIEW_GROUP_HAS_HEADER) { EsAssert(firstIndex > 0); // Cannot remove the group header. } if (group->flags & ES_LIST_VIEW_GROUP_HAS_FOOTER) { EsAssert(firstIndex + count < (intptr_t) group->itemCount); // Cannot remove the group footer. } } // Remove the items from the group. int64_t totalSizeOfItems = view->MeasureItems(groupIndex, firstIndex, count); int64_t sizeToRemove = (int64_t) group->itemCount == count ? group->totalSize : (count * view->style->gapMinor + totalSizeOfItems); group->itemCount -= count; group->totalSize -= sizeToRemove; view->totalItemCount -= count; // Update indices of visible items, // and remove deleted items. uintptr_t firstVisibleItemToMove = view->visibleItems.Length(); if (view->hasFocusedItem && view->focusedItemGroup == groupIndex) { if (view->focusedItemIndex >= firstIndex && view->focusedItemIndex < firstIndex + count) { view->hasFocusedItem = false; } else { view->focusedItemIndex -= count; } } if (view->hasAnchorItem && view->anchorItemGroup == groupIndex) { if (view->focusedItemIndex >= firstIndex && view->anchorItemIndex < firstIndex + count) { view->hasAnchorItem = false; } else { view->anchorItemIndex -= count; } } for (uintptr_t i = 0; i < view->visibleItems.Length(); i++) { ListViewItem *item = &view->visibleItems[i]; if (item->group < groupIndex) { continue; } else if (item->group > groupIndex) { if (i < firstVisibleItemToMove) { firstVisibleItemToMove = i; } break; } if (item->index >= firstIndex + count) { item->index -= count; if (i < firstVisibleItemToMove) { firstVisibleItemToMove = i; } } else if (item->index >= firstIndex && item->index < firstIndex + count) { item->element->index = i; item->element->Destroy(); view->visibleItems.Delete(i); i--; } } // Remove the space of the items. view->InsertSpace(-sizeToRemove, firstVisibleItemToMove); } void EsListViewRemoveAll(EsListView *view, EsListViewIndex group) { EsMessageMutexCheck(); if (view->groups[group].itemCount) { EsListViewRemove(view, group, 0, view->groups[group].itemCount); } } int ListViewColumnHeaderItemMessage(EsElement *element, EsMessage *message) { EsListView *view = (EsListView *) element->parent->parent; if (message->type == ES_MSG_DESTROY) { return 0; } ListViewColumn *column = &view->registeredColumns[element->userData.u]; if (message->type == ES_MSG_PAINT) { EsMessage m = { ES_MSG_LIST_VIEW_GET_COLUMN_SORT }; m.getColumnSort.index = element->userData.u; int sort = EsMessageSend(view, &m); EsDrawContent(message->painter, element, element->GetBounds(), column->title, column->titleBytes, 0, sort == ES_LIST_VIEW_COLUMN_SORT_ASCENDING ? ES_DRAW_CONTENT_MARKER_UP_ARROW : sort == ES_LIST_VIEW_COLUMN_SORT_DESCENDING ? ES_DRAW_CONTENT_MARKER_DOWN_ARROW : ES_FLAGS_DEFAULT); } else if (message->type == ES_MSG_MOUSE_LEFT_CLICK && (column->flags & ES_LIST_VIEW_COLUMN_HAS_MENU)) { EsMessage m = { ES_MSG_LIST_VIEW_COLUMN_MENU }; m.columnMenu.source = element; m.columnMenu.activeColumnIndex = element->userData.u; m.columnMenu.columnID = view->registeredColumns[element->userData.u].id; EsMessageSend(view, &m); } else if (message->type == ES_MSG_MOUSE_LEFT_DOWN) { } else { return 0; } return ES_HANDLED; } int ListViewColumnSplitterMessage(EsElement *element, EsMessage *message) { EsListView *view = (EsListView *) element->parent->parent; if (message->type == ES_MSG_DESTROY) { return 0; } ListViewColumn *column = &view->registeredColumns[element->userData.u]; if (message->type == ES_MSG_MOUSE_LEFT_DOWN) { view->columnResizingOriginalWidth = column->width * theming.scale; } else if (message->type == ES_MSG_MOUSE_LEFT_DRAG) { int width = message->mouseDragged.newPositionX - message->mouseDragged.originalPositionX + view->columnResizingOriginalWidth; int minimumWidth = element->style->metrics->minimumWidth; if (width < minimumWidth) width = minimumWidth; column->width = width / theming.scale; ListViewCalculateTotalColumnWidth(view); EsElementRelayout(element->parent); EsElementRelayout(view); } else { return 0; } return ES_HANDLED; } void EsListViewAddAllColumns(EsListView *view) { EsElementDestroyContents(view->columnHeader); EsAssert(view->flags & ES_LIST_VIEW_COLUMNS); // List view does not have columns flag set. view->activeColumns.Free(); for (uintptr_t i = 0; i < view->registeredColumns.Length(); i++) { view->activeColumns.Add(i); EsStyleID style = (view->registeredColumns[i].flags & ES_LIST_VIEW_COLUMN_HAS_MENU) ? ES_STYLE_LIST_COLUMN_HEADER_ITEM_HAS_MENU : ES_STYLE_LIST_COLUMN_HEADER_ITEM; EsElement *columnHeaderItem = EsCustomElementCreate(view->columnHeader, ES_CELL_FILL, style); columnHeaderItem->messageUser = ListViewColumnHeaderItemMessage; columnHeaderItem->cName = "column header item"; columnHeaderItem->userData = i; EsElement *splitter = EsCustomElementCreate(view->columnHeader, ES_CELL_FILL, ES_STYLE_LIST_COLUMN_HEADER_SPLITTER); splitter->messageUser = ListViewColumnSplitterMessage; splitter->cName = "column header splitter"; splitter->userData = i; } ListViewCalculateTotalColumnWidth(view); view->scroll.Refresh(); } void EsListViewRegisterColumn(EsListView *view, uint32_t id, const char *title, ptrdiff_t titleBytes, uint32_t flags, double initialWidth) { EsMessageMutexCheck(); if (!initialWidth) { initialWidth = (view->registeredColumns.Length() ? view->secondaryCellStyle : view->primaryCellStyle)->preferredWidth / theming.scale; } ListViewColumn column = {}; column.id = id; column.flags = flags; column.width = initialWidth; if (titleBytes == -1) titleBytes = EsCStringLength(title); HeapDuplicate((void **) &column.title, &column.titleBytes, title, titleBytes); view->registeredColumns.Add(column); } void EsListViewContentChanged(EsListView *view) { EsMessageMutexCheck(); view->searchBufferLastKeyTime = 0; view->searchBufferBytes = 0; view->scroll.SetX(0); view->scroll.SetY(0); view->hasScrollItem = false; view->useScrollItem = false; EsListViewInvalidateAll(view); } void EsListViewFocusItem(EsListView *view, EsListViewIndex group, EsListViewIndex index) { ListViewItem *oldFocus = view->FindVisibleItem(view->focusedItemGroup, view->focusedItemIndex); if (oldFocus) { oldFocus->element->customStyleState &= ~THEME_STATE_FOCUSED_ITEM; oldFocus->element->MaybeRefreshStyle(); } view->hasFocusedItem = true; view->focusedItemGroup = group; view->focusedItemIndex = index; ListViewItem *newFocus = view->FindVisibleItem(view->focusedItemGroup, view->focusedItemIndex); if (newFocus) { newFocus->element->customStyleState |= THEME_STATE_FOCUSED_ITEM; newFocus->element->MaybeRefreshStyle(); } view->EnsureItemVisible(group, index, ES_FLAGS_DEFAULT); } bool EsListViewGetFocusedItem(EsListView *view, EsListViewIndex *group, EsListViewIndex *index) { if (view->hasFocusedItem) { if (group) *group = view->focusedItemGroup; if (index) *index = view->focusedItemIndex; } return view->hasFocusedItem; } void EsListViewSelectNone(EsListView *view) { EsMessageMutexCheck(); view->Select(-1, 0, false, false, false); } void EsListViewSelect(EsListView *view, EsListViewIndex group, EsListViewIndex index, bool addToExistingSelection) { EsMessageMutexCheck(); if (addToExistingSelection) { view->SetSelected(group, index, group, index, true, false); view->UpdateVisibleItemsSelectionState(); } else { view->Select(group, index, false, false, false); } } void EsListViewSetEmptyMessage(EsListView *view, const char *message, ptrdiff_t messageBytes) { EsMessageMutexCheck(); if (messageBytes == -1) messageBytes = EsCStringLength(message); HeapDuplicate((void **) &view->emptyMessage, &view->emptyMessageBytes, message, messageBytes); if (!view->totalItemCount) { view->Repaint(true); } } EsListViewIndex EsListViewGetIndexFromItem(EsElement *_element, EsListViewIndex *group) { ListViewItemElement *element = (ListViewItemElement *) _element; EsListView *view = (EsListView *) element->parent; EsAssert(element->index < view->visibleItems.Length()); if (group) *group = view->visibleItems[element->index].group; return view->visibleItems[element->index].index; } void EsListViewInvalidateAll(EsListView *view) { view->UpdateVisibleItemsSelectionState(); view->Repaint(true); } void EsListViewInvalidateContent(EsListView *view, EsListViewIndex group, EsListViewIndex index) { for (uintptr_t i = 0; i < view->visibleItems.Length(); i++) { if (view->visibleItems[i].group == group && view->visibleItems[i].index == index) { view->UpdateVisibleItemSelectionState(i); view->visibleItems[i].element->Repaint(true); break; } } } #define LIST_VIEW_SORT_FUNCTION(_name, _line) \ ES_MACRO_SORT(_name, EsListViewIndex, { \ ListViewFixedItemData *left = (ListViewFixedItemData *) &context->items[*_left]; \ ListViewFixedItemData *right = (ListViewFixedItemData *) &context->items[*_right]; \ result = _line; \ }, ListViewColumn *) LIST_VIEW_SORT_FUNCTION(ListViewSortByStringsAscending, EsStringCompare(left->s.string, left->s.bytes, right->s.string, right->s.bytes)); LIST_VIEW_SORT_FUNCTION(ListViewSortByStringsDescending, -EsStringCompare(left->s.string, left->s.bytes, right->s.string, right->s.bytes)); LIST_VIEW_SORT_FUNCTION(ListViewSortByEnumsAscending, EsStringCompare(context->enumStrings[left->i].string, context->enumStrings[left->i].stringBytes, context->enumStrings[right->i].string, context->enumStrings[right->i].stringBytes)); LIST_VIEW_SORT_FUNCTION(ListViewSortByEnumsDescending, -EsStringCompare(context->enumStrings[left->i].string, context->enumStrings[left->i].stringBytes, context->enumStrings[right->i].string, context->enumStrings[right->i].stringBytes)); LIST_VIEW_SORT_FUNCTION(ListViewSortByIntegersAscending, left->i > right->i ? 1 : left->i == right->i ? 0 : -1); LIST_VIEW_SORT_FUNCTION(ListViewSortByIntegersDescending, left->i < right->i ? 1 : left->i == right->i ? 0 : -1); LIST_VIEW_SORT_FUNCTION(ListViewSortByDoublesAscending, left->d > right->d ? 1 : left->d == right->d ? 0 : -1); LIST_VIEW_SORT_FUNCTION(ListViewSortByDoublesDescending, left->d < right->d ? 1 : left->d == right->d ? 0 : -1); ListViewSortFunction ListViewGetSortFunction(ListViewColumn *column, uint8_t direction) { if ((column->flags & ES_LIST_VIEW_COLUMN_DATA_MASK) == ES_LIST_VIEW_COLUMN_DATA_STRINGS) { return (direction == LIST_SORT_DIRECTION_DESCENDING ? ListViewSortByStringsDescending : ListViewSortByStringsAscending); } else if ((column->flags & ES_LIST_VIEW_COLUMN_DATA_MASK) == ES_LIST_VIEW_COLUMN_DATA_INTEGERS) { if ((column->flags & ES_LIST_VIEW_COLUMN_FORMAT_MASK) == ES_LIST_VIEW_COLUMN_FORMAT_ENUM_STRING) { return (direction == LIST_SORT_DIRECTION_DESCENDING ? ListViewSortByEnumsDescending : ListViewSortByEnumsAscending); } else { return (direction == LIST_SORT_DIRECTION_DESCENDING ? ListViewSortByIntegersDescending : ListViewSortByIntegersAscending); } } else if ((column->flags & ES_LIST_VIEW_COLUMN_DATA_MASK) == ES_LIST_VIEW_COLUMN_DATA_DOUBLES) { return (direction == LIST_SORT_DIRECTION_DESCENDING ? ListViewSortByDoublesDescending : ListViewSortByDoublesAscending); } else { EsAssert(false); } return nullptr; } EsListViewIndex EsListViewFixedItemInsert(EsListView *view, EsGeneric data, EsListViewIndex index, uint32_t iconID) { EsAssert(view->flags & ES_LIST_VIEW_FIXED_ITEMS); if (!view->groups.Length()) { EsListViewInsertGroup(view, 0, ES_FLAGS_DEFAULT); } if (!view->registeredColumns.Length()) { EsListViewRegisterColumn(view, 0, nullptr, 0); } if (index == -1) { index = view->fixedItems.Length(); } EsAssert(index >= 0 && index <= (intptr_t) view->fixedItems.Length()); ListViewFixedItem item = {}; item.data = data; item.iconID = iconID; view->fixedItems.Insert(item, index); view->fixedItemIndices.Insert(index, index); ListViewFixedItemData emptyData = {}; for (uintptr_t i = 0; i < view->registeredColumns.Length(); i++) { ListViewColumn *column = &view->registeredColumns[i]; if (column->items.Length() >= (uintptr_t) index) { column->items.InsertPointer(&emptyData, index); } } EsListViewInsert(view, 0, index, 1); return index; } void ListViewFixedItemSetInternal(EsListView *view, EsListViewIndex index, uint32_t columnID, ListViewFixedItemData data, uint32_t dataType) { EsAssert(view->flags & ES_LIST_VIEW_FIXED_ITEMS); EsMessageMutexCheck(); EsAssert(index >= 0 && index < (intptr_t) view->fixedItems.Length()); ListViewColumn *column = nullptr; for (uintptr_t i = 0; i < view->registeredColumns.Length(); i++) { if (view->registeredColumns[i].id == columnID) { column = &view->registeredColumns[i]; break; } } EsAssert(column); EsAssert((column->flags & ES_LIST_VIEW_COLUMN_DATA_MASK) == dataType); // Make sure that the column's array of items has been updated to match to the size of fixedItems. if (column->items.Length() < view->fixedItems.Length()) { uintptr_t oldLength = column->items.Length(); column->items.SetLength(view->fixedItems.Length()); EsMemoryZero(&column->items[oldLength], (view->fixedItems.Length() - oldLength) * sizeof(column->items[0])); } bool changed = false; if (dataType == ES_LIST_VIEW_COLUMN_DATA_STRINGS) { changed = EsStringCompareRaw(column->items[index].s.string, column->items[index].s.bytes, data.s.string, data.s.bytes); } else if (dataType == ES_LIST_VIEW_COLUMN_DATA_DOUBLES) { changed = column->items[index].d != data.d; } else if (dataType == ES_LIST_VIEW_COLUMN_DATA_INTEGERS) { changed = column->items[index].i != data.i; } else { EsAssert(false); } if (dataType == ES_LIST_VIEW_COLUMN_DATA_STRINGS) { EsHeapFree(column->items[index].s.string); } column->items[index] = data; if (changed) { for (uintptr_t i = 0; i < view->fixedItemIndices.Length(); i++) { if (view->fixedItemIndices[i] == index) { EsListViewInvalidateContent(view, 0, i); break; } } } } void EsListViewFixedItemSetString(EsListView *view, EsListViewIndex index, uint32_t columnID, const char *string, ptrdiff_t stringBytes) { ListViewFixedString fixedString = {}; fixedString.bytes = stringBytes == -1 ? EsCStringLength(string) : stringBytes; size_t outBytes; HeapDuplicate((void **) &fixedString, &outBytes, string, fixedString.bytes); if (outBytes == fixedString.bytes) { ListViewFixedItemSetInternal(view, index, columnID, { .s = fixedString }, ES_LIST_VIEW_COLUMN_DATA_STRINGS); } } void EsListViewFixedItemSetDouble(EsListView *view, EsListViewIndex index, uint32_t columnID, double number) { ListViewFixedItemSetInternal(view, index, columnID, { .d = number }, ES_LIST_VIEW_COLUMN_DATA_DOUBLES); } void EsListViewFixedItemSetInteger(EsListView *view, EsListViewIndex index, uint32_t columnID, int64_t number) { ListViewFixedItemSetInternal(view, index, columnID, { .i = number }, ES_LIST_VIEW_COLUMN_DATA_INTEGERS); } bool EsListViewFixedItemFindIndex(EsListView *view, EsGeneric data, EsListViewIndex *index) { EsAssert(view->flags & ES_LIST_VIEW_FIXED_ITEMS); EsMessageMutexCheck(); for (uintptr_t i = 0; i < view->fixedItemIndices.Length(); i++) { if (view->fixedItems[view->fixedItemIndices[i]].data == data) { *index = i; return true; } } return false; } bool EsListViewFixedItemSelect(EsListView *view, EsGeneric data) { EsAssert(view->flags & ES_LIST_VIEW_FIXED_ITEMS); EsMessageMutexCheck(); EsListViewIndex index; bool found = EsListViewFixedItemFindIndex(view, data, &index); if (found) { EsListViewSelect(view, 0, index); // TODO Maybe you should have to separately call EsListViewFocusItem to get this behaviour? EsListViewFocusItem(view, 0, index); view->EnsureItemVisible(0, index, ENSURE_VISIBLE_ALIGN_CENTER); } return found; } bool EsListViewFixedItemRemove(EsListView *view, EsGeneric data) { EsAssert(view->flags & ES_LIST_VIEW_FIXED_ITEMS); EsMessageMutexCheck(); EsListViewIndex index; bool found = EsListViewFixedItemFindIndex(view, data, &index); if (found) { EsListViewRemove(view, 0, index, 1); EsListViewIndex fixedIndex = view->fixedItemIndices[index]; for (uintptr_t i = 0; i < view->registeredColumns.Length(); i++) { ListViewColumn *column = &view->registeredColumns[i]; if ((uintptr_t) fixedIndex < column->items.Length()) { if ((column->flags & ES_LIST_VIEW_COLUMN_DATA_MASK) == ES_LIST_VIEW_COLUMN_DATA_STRINGS) { EsHeapFree(column->items[fixedIndex].s.string); } column->items.Delete(fixedIndex); if (!column->items.Length()) { column->items.Free(); } } } view->fixedItems.Delete(fixedIndex); view->fixedItemIndices.Delete(index); for (uintptr_t i = 0; i < view->fixedItemIndices.Length(); i++) { if (view->fixedItemIndices[i] > fixedIndex) { view->fixedItemIndices[i]--; } } } return found; } bool EsListViewFixedItemGetSelected(EsListView *view, EsGeneric *data) { EsAssert(view->flags & ES_LIST_VIEW_FIXED_ITEMS); EsMessageMutexCheck(); if (view->fixedItemSelection == -1 || view->fixedItemSelection >= (intptr_t) view->fixedItems.Length()) { return false; } else { *data = view->fixedItems[view->fixedItemIndices[view->fixedItemSelection]].data; return true; } } void EsListViewFixedItemSetEnumStringsForColumn(EsListView *view, uint32_t columnID, const EsListViewEnumString *strings, size_t stringCount) { for (uintptr_t i = 0; i < view->registeredColumns.Length(); i++) { if (view->registeredColumns[i].id == columnID) { view->registeredColumns[i].enumStrings = strings; view->registeredColumns[i].enumStringCount = stringCount; EsListViewInvalidateAll(view); return; } } EsAssert(false); } void EsListViewFixedItemSortAll(EsListView *view) { ListViewColumn *column = nullptr; for (uintptr_t i = 0; i < view->registeredColumns.Length(); i++) { if (view->registeredColumns[i].id == view->fixedItemSortColumnID) { column = &view->registeredColumns[i]; break; } } EsAssert(column); EsAssert(view->fixedItems.Length() == view->fixedItemIndices.Length()); EsListViewIndex previousSelectionIndex = view->fixedItemSelection >= 0 && (uintptr_t) view->fixedItemSelection < view->fixedItemIndices.Length() ? view->fixedItemIndices[view->fixedItemSelection] : -1; ListViewSortFunction sortFunction = ListViewGetSortFunction(column, view->fixedItemSortDirection); sortFunction(view->fixedItemIndices.array, view->fixedItems.Length(), column); EsListViewInvalidateAll(view); if (previousSelectionIndex != -1) { for (uintptr_t i = 0; i < view->fixedItemIndices.Length(); i++) { if (view->fixedItemIndices[i] == previousSelectionIndex) { EsListViewSelect(view, 0, i); EsListViewFocusItem(view, 0, i); view->EnsureItemVisible(0, i, ENSURE_VISIBLE_ALIGN_CENTER); break; } } } } void ListViewSetSortDirection(EsListView *view, uint32_t columnID, uint8_t direction) { if (view->fixedItemSortColumnID != columnID || view->fixedItemSortDirection != direction) { view->fixedItemSortColumnID = columnID; view->fixedItemSortDirection = direction; EsListViewFixedItemSortAll(view); } } void ListViewSetSortAscending(EsMenu *menu, EsGeneric context) { ListViewSetSortDirection((EsListView *) menu->userData.p, context.u, LIST_SORT_DIRECTION_ASCENDING); } void ListViewSetSortDescending(EsMenu *menu, EsGeneric context) { ListViewSetSortDirection((EsListView *) menu->userData.p, context.u, LIST_SORT_DIRECTION_DESCENDING); } int ListViewInlineTextboxMessage(EsElement *element, EsMessage *message) { int response = ProcessTextboxMessage(element, message); if (message->type == ES_MSG_DESTROY) { EsListView *view = (EsListView *) EsElementGetLayoutParent(element); view->inlineTextbox = nullptr; ListViewItem *item = view->FindVisibleItem(view->inlineTextboxGroup, view->inlineTextboxIndex); if (item) EsElementRepaint(item->element); EsElementFocus(view); } return response; } EsTextbox *EsListViewCreateInlineTextbox(EsListView *view, EsListViewIndex group, EsListViewIndex index, uint32_t flags) { if (view->inlineTextbox) { view->inlineTextbox->Destroy(); } view->inlineTextboxGroup = group; view->inlineTextboxIndex = index; view->EnsureItemVisible(group, index, ENSURE_VISIBLE_ALIGN_TOP); uint64_t textboxFlags = ES_CELL_FILL | ES_TEXTBOX_EDIT_BASED | ES_TEXTBOX_ALLOW_TABS; if (flags & ES_LIST_VIEW_INLINE_TEXTBOX_REJECT_EDIT_IF_FOCUS_LOST) { textboxFlags |= ES_TEXTBOX_REJECT_EDIT_IF_LOST_FOCUS; } view->inlineTextbox = EsTextboxCreate(view, textboxFlags, ES_STYLE_TEXTBOX_INLINE); if (!view->inlineTextbox) { return nullptr; } EsAssert(view->inlineTextbox->messageClass == ProcessTextboxMessage); view->inlineTextbox->messageClass = ListViewInlineTextboxMessage; if (flags & ES_LIST_VIEW_INLINE_TEXTBOX_COPY_EXISTING_TEXT) { EsMessage m = { ES_MSG_LIST_VIEW_GET_CONTENT }; uint8_t _buffer[256]; EsBuffer buffer = { .out = _buffer, .bytes = sizeof(_buffer) }; m.getContent.buffer = &buffer; m.getContent.index = index; m.getContent.group = group; EsMessageSend(view, &m); EsTextboxInsert(view->inlineTextbox, (char *) _buffer, buffer.position); EsTextboxSelectAll(view->inlineTextbox); } if (view->searchBufferBytes) { view->searchBufferBytes = 0; EsElementRepaint(view); } EsElementRelayout(view); EsElementFocus(view->inlineTextbox); EsTextboxStartEdit(view->inlineTextbox); return view->inlineTextbox; } void EsListViewScrollToEnd(EsListView *view) { if (view->flags & ES_LIST_VIEW_HORIZONTAL) { view->scroll.SetX(view->scroll.limit[0]); } else { view->scroll.SetY(view->scroll.limit[1]); } } EsListViewEnumeratedVisibleItem *EsListViewEnumerateVisibleItems(EsListView *view, size_t *count) { EsMessageMutexCheck(); *count = view->visibleItems.Length(); EsListViewEnumeratedVisibleItem *result = (EsListViewEnumeratedVisibleItem *) EsHeapAllocate(sizeof(EsListViewEnumeratedVisibleItem) * *count, true); if (!result) { *count = 0; return nullptr; } for (uintptr_t i = 0; i < *count; i++) { result[i].element = view->visibleItems[i].element; result[i].group = view->visibleItems[i].group; result[i].index = view->visibleItems[i].index; } return result; } void EsListViewSetMaximumItemsPerBand(EsListView *view, int maximumItemsPerBand) { view->maximumItemsPerBand = maximumItemsPerBand; } EsPoint EsListViewGetAnnouncementPointForSelection(EsListView *view) { EsRectangle viewWindowBounds = EsElementGetWindowBounds(view); EsRectangle bounding = viewWindowBounds; bool first = true; for (uintptr_t i = 0; i < view->visibleItems.Length(); i++) { if (~view->visibleItems[i].element->customStyleState & THEME_STATE_SELECTED) continue; EsRectangle bounds = EsElementGetWindowBounds(view->visibleItems[i].element); if (first) bounding = bounds; else bounding = EsRectangleBounding(bounding, bounds); first = false; } bounding = EsRectangleIntersection(bounding, viewWindowBounds); return ES_POINT((bounding.l + bounding.r) / 2, (bounding.t + bounding.b) / 2); }