Skip to content

Instantly share code, notes, and snippets.

@Flix01
Last active October 13, 2023 23:19
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Flix01/94b0bf3069476a1344ac to your computer and use it in GitHub Desktop.
Save Flix01/94b0bf3069476a1344ac to your computer and use it in GitHub Desktop.
Minimal ListView implementation for ImGui version v1.31
// This software is provided 'as-is', without any express or implied
// warranty. In no event will the authors be held liable for any damages
// arising from the use of this software.
// Permission is granted to anyone to use this software for any purpose,
// including commercial applications, and to alter it and redistribute it
// freely, subject to the following restrictions:
// 1. The origin of this software must not be misrepresented; you must not
// claim that you wrote the original software. If you use this software
// in a product, an acknowledgment in the product documentation would be
// appreciated but is not required.
// 2. Altered source versions must be plainly marked as such, and must not be
// misrepresented as being the original software.
// 3. This notice may not be removed or altered from any source distribution.
#ifndef IMGUILISTVIEW_H_
#define IMGUILISTVIEW_H_
// USAGE:
/*
#include <imguilistview.h>
#include <new>
Then inside an ImGui window just type:
ImGui::TestListView(); // (and see the code inside this method for further info)
*/
// WHAT'S THIS?
/*
-> It works on ImGui library v1.31. Please see this topics: https://github.com/ocornut/imgui/issues/124 and https://github.com/ocornut/imgui/issues/125 for further info
-> It just display a table with some fields, NOTHING MORE THAN THAT!
-> No editing
-> No sorting
-> No item text formatting
-> Even no row selecting!
-> And no interaction too!
*/
// THAT'S BAD! SO, WHY SHOULD I USE THIS CODE INSTEAD OF DIRECTLY USING IMGUI COLUMNS ?
/*
Ehm... good question!
At the moment of writing the only possible answer is "automatic clipping":
If you have many items, only the visible ones will be fetched.
However, to be honest, the test code here creates all the items at init time:
that's not always possible in many cases. If your items aren't available, the best that you can do
is to extend your class directly form ListViewBase (and that might require a lot of work!)
UPDATE: I've just found another reason for using this code:
if you want to extend the code and add sorting, formatting and editing support!
*/
// CHANGELOG - REVISIONS
/*
LAST REVISION:
--------------
New features:
-> added (single) row selection support (ImGui::listViewBase methods: getSelectedRow(),selectRow(),updateSelectedRow(),scrollToSelectedRow())
-> now ImGui::ListView::render(...) returns true when the user changes the row selection by clicking on another row.
-> added ImGui::listViewBase::getSelectedColumn() that returns the index of the last column that has been clicked (although the visible selection encompasses all the columns of the selected row).
-> If you need to extend directly from ImGui::ListViewBase (most likely because you can't instantiate all your row-items at init time),
now the API is much clearer (you must implement only 4 pure virtual methods).
-> added basic editing support (for all types, except HT_CUSTOM). Unlike sorting support, this is disabled by befault and can be enabled by passing an ImGui::listViewHeaderEditable struct to the ListView::Header::ctr(...).
Please see TestListView() for further info.
-> added (ImGui::listViewBase method isInEditingMode())
-> changed the way HT_ENUM works to make them compliant with the ImGui::Combo callback (Please see TestListView() for further info)
Still missing some things (but most of the work has been done):
-> programmatically column width formatting (at the moment of writing ImGui does not support it).
-> Better alignment of the cells in the selected row when editing a cell (but how to do it?).
-> (optional) row-based contex menu (when ImGui will support them).
-> add other HT_TYPES (it shouldn't be too difficult to add HT_FLOAT2/3/4 HT_INT2/3/4 HT_COLOR and so on: but that would take some time...).
Note. I'll NEVER add multiple row selection support.
-------------------------------------------------------------------------------------------------------------------
PREVIOUS REVISION:
------------------
Changed almost all the code syntax/classes!
New features:
-> support for programmatically (=by writing code) column reordering/hiding, through the additional optional arguments in ListViewBase::render(...). Not shown in ImGui::TestListView(), but it's commented out inside its code.
-> support for basic cell text formatting through the new optional arguments in ListView::Header::ctr(...): _precision,_prefix,_suffix. (_precision works for strings too, affecting the number of displayed characters).
-> support for basic column sorting (by clicking the correspondent column header). Sorting can be disabled through the new optional argument '_sortable' in ListView::Header::ctr(...).
Please note that, unlike column reordering, row sorting happens IN PLACE (there is no concept of VIEW: that means that once you sort your items they will lose their original ordering forever).
Still missing a lot of things:
-> editing support (although I've starting adding some fields for future development).
-> some kind of user interaction (i.e. ListViewBase::render(...) currently does not pass back to the user any piece of information: that should be changed).
-> programmatically column width formatting (at the moment of writing ImGui does not support it).
-> (row) selecting support (how to process mouse events to do it ?).
-> (optional) row-based contex menu (when ImGui will support them).
TODO: Header columns with type HT_CUSTOM have never been tested.
*/
#include <imgui.h>
namespace ImGui {
// Base class that should be used (=extended) only by people that can't use the ListView class.
// Otherwise just skip it
class ListViewBase {
public:
// enum that defines the variable types that each ListView column can have
enum HeaderType {
HT_INT=0,
HT_UNSIGNED,
HT_FLOAT,
HT_DOUBLE,
HT_STRING,
HT_ENUM, // like HT_INT, but the text is retrieved through HeaderData::textFromEnumFunctionPointer function ptr
HT_BOOL,
HT_CUSTOM // By default not editable
};
// struct that is used in the "void getHeaderData(size_t column,HeaderData& headerDataOut)" virtual method
struct HeaderData {
const char* name; // make it point to the name of your column header. Do not allocate it.
// type
struct Type {
HeaderType headerType; // The type of the variable this column contains.
int numEnumElements;
typedef bool (*TextFromEnumDelegate)(void*, int, const char**);
TextFromEnumDelegate textFromEnumFunctionPointer; // used only when type==HT_ENUM, otherwise set it to NULL. The method is used to convert an int to a char*.
void* textFromEnumFunctionPointerUserData; // used only when type==HT_ENUM, if you want to share the same TextFromEnumDelegate for multiple enums. Otherwise set it to NULL.
Type(HeaderType _headerType,int _numEnumElements=0, TextFromEnumDelegate _textFromEnumFunctionPointer=NULL, void* _textFromEnumFunctionPointerUserData=NULL)
: headerType(_headerType),numEnumElements(_numEnumElements),textFromEnumFunctionPointer(_textFromEnumFunctionPointer),textFromEnumFunctionPointerUserData(_textFromEnumFunctionPointerUserData){}
};
Type type;
// display formatting
struct Formatting {
int precision; // in case of HT_STRING max number of displayed characters, in case of HT_FLOAT or HT_DOUBLE the number of decimals to be displayed (experiment for other types and see)
const char* prefix; // make it point to a string that must be displayed BEFORE the text in each column cell, or just set it to NULL or to "".Do not allocate it.
const char* suffix; // make it point to a string that must be displayed AFTER the text in each column cell, or just set it to NULL or to "".Do not allocate it.
Formatting(int _precision=-1,const char* _prefix=NULL,const char* _suffix=NULL) : precision(_precision),prefix(_prefix),suffix(_suffix){}
};
Formatting formatting;
// sortable properties
struct Sorting {
bool sortable; // true by default. It enables row sorting by clicking on this column header
mutable bool sortingAscending; // used internally (AFAIR). Do not touch
unsigned short sortableElementOfPossibleArray; // internal usage for now: MUST BE 0!
Sorting(bool _sortable=true,unsigned short _sortableElementOfPossibleArray=0) : sortable(_sortable),sortingAscending(false),sortableElementOfPossibleArray(_sortableElementOfPossibleArray) {}
};
Sorting sorting;
// editing properties
struct Editing {
bool editable;
int precisionOrStringBufferSize; // for HT_STRING this must be the size of the string buffer in bytes (it can't be left to -1), for HT_FLOAT or HT_DOUBLE the number of decimals
double minValue;
double maxValue;
Editing(bool _editable=false,int _precisionOrStringBufferSize=-1,double _minValue=0,double _maxValue=100) :editable(_editable),precisionOrStringBufferSize(_precisionOrStringBufferSize),minValue(_minValue),maxValue(_maxValue) {}
};
Editing editing;
HeaderData() : name(NULL),type(HT_STRING),formatting(),sorting(),editing() {}
void reset() {*this=HeaderData();}
};
// struct that is used in the "void getCellData(size_t row,size_t column,CellData& cellDataOut)" virtual method
struct CellData {
const void* fieldPtr; // make it point to the variable of the cell of type "HeaderType". >>>> CANNOT BE NULL!!!! <<<<
const char* customText; // (only for HT_CUSTOM only; otherwise set it to NULL) make it point to the string you want the cell to display. Do not allocate the string!
bool* selectedRowPtr; // a pointer to a mutable bool variable that states whether the cell ROW is selected or not (note that the bool variable it refers is a ROW data, not strictly a CELL data(= row,col) )
CellData() : fieldPtr(NULL),customText(NULL),selectedRowPtr(NULL) {}
void reset() {*this=CellData();}
};
// virtual methods that can/must be implemented by derived classes:--------------
virtual size_t getNumColumns() const=0;
virtual size_t getNumRows() const=0;
protected:
virtual void getHeaderData(size_t column,HeaderData& headerDataOut) const=0; // Just fill as many fields as you can in your implementation: string fields are not intended to be allocated! Just make them point your copies!
virtual void getCellData(size_t row,size_t column,CellData& cellDataOut) const=0; // Just fill cellDataOut. string fields are not intended to be allocated! Just make them point your copies!
public:
virtual bool sort(size_t column) {return false;} // This must be implemented to perform sorting ('selectedRow' is going to change after sorting: that's why it's a good practice to call updateSelectedRow(...) at the end of its implementation)
// end virtual methods that can/must be implemented by derived classes:-----------
// ctr dctr
ListViewBase() : selectedRow(-1),selectedColumn(-1),editingModePresent(false),editorAllowed(false),scrollToRow(-1) {}
virtual ~ListViewBase() {}
// (single) selection API
inline int getSelectedRow() const {return selectedRow;}
inline int getSelectedColumn() const {return selectedColumn;}
inline void selectRow(int row) {
if (selectedRow!=row && getNumColumns()>0) {
const size_t numRows = getNumRows();
CellData cd;
if (selectedRow>=0 && selectedRow<(int)numRows) {
// remove old selection
getCellData((size_t)selectedRow,0,cd);
if (cd.selectedRowPtr) *cd.selectedRowPtr = false;
cd.reset();
}
selectedRow = row;
if (selectedRow>=0 && selectedRow<(int)numRows) {
// add new selection
getCellData((size_t)selectedRow,0,cd);
if (cd.selectedRowPtr) *cd.selectedRowPtr = true;
//cd.reset();
}
else selectedRow = -1;
}
//return selectedRow;
}
//protected:
// This methods can be called to retrieve the selected row index after sorting, or after some item is inserted before the selected row.
// Otherwise the selection will still look correct (it points to a row item field), but the 'selectedRow' field will retain the old value.
int updateSelectedRow() {
const size_t numRows = getNumRows();
if (selectedRow>=0 && getNumColumns()>0) {
selectedRow = -1;CellData cd;
for (size_t row = 0; row < numRows; ++row) {
cd.reset();
getCellData((size_t)row,0,cd);
if (cd.selectedRowPtr && *cd.selectedRowPtr) {
selectedRow = (int) row;
break;
}
}
}
return selectedRow;
}
inline bool isInEditingMode() const {return editingModePresent;} // true if cell(getSelectedRow(),getSelectedColumn()) is being edited
void scrollToSelectedRow() const {
const int numRows = (int) getNumRows();
if (numRows==0) return;
scrollToRow = selectedRow;
if (scrollToRow<0) scrollToRow=0;
else if (scrollToRow>=numRows) scrollToRow = numRows-1;
// Next time render() is called we'll try to scroll to it
}
private:
mutable int selectedRow;mutable int selectedColumn;mutable bool editingModePresent;mutable bool editorAllowed;mutable int scrollToRow;
static const char* GetTextFromCellFieldDataPtr(HeaderData& hd,const void*& cellFieldDataPtr) {
if (hd.type.headerType==HT_CUSTOM || !cellFieldDataPtr) return "";
static const int bufferSize = 1024;static char buf[bufferSize];buf[0]='\0';
static const int precisionStrSize = 16;static char precisionStr[precisionStrSize];int precisionLastCharIndex;
const int precision = hd.formatting.precision;
if (precision>0) {
strcpy(precisionStr,"%.");
snprintf(&precisionStr[2], precisionStrSize-2,"%ds",precision);
precisionLastCharIndex = strlen(precisionStr)-1;
}
else {
strcpy(precisionStr,"%s");
precisionLastCharIndex = 1;
}
size_t bufid = 0;int pbufsz = bufferSize;
const char* prefix = hd.formatting.prefix;
const char* suffix = hd.formatting.suffix;
const bool hasPrefix = prefix && strlen(prefix)>0;
const bool hasSuffix = suffix && strlen(suffix)>0;
// prefix:
if (hasPrefix) {
snprintf(&buf[bufid], pbufsz,"%s",prefix);
bufid = strlen(buf);
pbufsz-=bufid;
}
// value:
const bool allowDirectStringForwarding = !hasPrefix && !hasSuffix && precision<=0;
switch (hd.type.headerType) {
case HT_STRING: if (allowDirectStringForwarding) return (const char*) cellFieldDataPtr;
else {
precisionStr[precisionLastCharIndex]='s';
snprintf(&buf[bufid], pbufsz,precisionStr,(const char*) cellFieldDataPtr);
}
break;
case HT_ENUM: if (allowDirectStringForwarding) {
const char * txt = NULL;
hd.type.textFromEnumFunctionPointer(hd.type.textFromEnumFunctionPointerUserData,*((const int*)cellFieldDataPtr),&txt);
return txt;
}
else {
precisionStr[precisionLastCharIndex]='s';
const char * txt = NULL;
if (hd.type.textFromEnumFunctionPointer(hd.type.textFromEnumFunctionPointerUserData,*((const int*)cellFieldDataPtr),&txt) &&
txt) snprintf(&buf[bufid], pbufsz,precisionStr,txt);
}
break;
case HT_BOOL: if (allowDirectStringForwarding) return (*((const bool*)cellFieldDataPtr)) ? "true" : "false";
else {
precisionStr[precisionLastCharIndex]='s';
if (*((const bool*)cellFieldDataPtr)) snprintf(&buf[bufid], pbufsz,precisionStr,"true");
else snprintf(&buf[bufid], pbufsz,precisionStr,"false");
}
break;
case HT_INT: precisionStr[precisionLastCharIndex]='d';
snprintf(&buf[bufid], pbufsz,precisionStr,*((const int*)cellFieldDataPtr));
break;
case HT_UNSIGNED: precisionStr[precisionLastCharIndex]='u';
snprintf(&buf[bufid], pbufsz,precisionStr,*((unsigned int*)cellFieldDataPtr));
break;
case HT_FLOAT: precisionStr[precisionLastCharIndex]='f';
snprintf(&buf[bufid], pbufsz,precisionStr,*((const float*)cellFieldDataPtr));
break;
case HT_DOUBLE: precisionStr[precisionLastCharIndex]='f';
snprintf(&buf[bufid], pbufsz,precisionStr,*((const double*)cellFieldDataPtr));
break;
default: return "";
}
// suffix:
if (hasSuffix) {
bufid = strlen(buf);
pbufsz-=bufid;
snprintf(&buf[bufid], pbufsz,"%s",suffix);
}
return buf;
}
public:
// main method.
// pOptionalColumnReorderVector: can be used to reorder columns in the view (but 'real' column indices won't be changed)
// maxNumColumnToDisplay: can be used to reduce the number of columns that are displayed.
virtual bool render(const ImVector<int> *pOptionalColumnReorderVector=NULL, int maxNumColumnToDisplay=-1) const {
ImGui::PushID(this);
const int numColumns = (int) getNumColumns();
const size_t numRows = getNumRows();
if (maxNumColumnToDisplay<0) maxNumColumnToDisplay = numColumns;
if (pOptionalColumnReorderVector && (int)pOptionalColumnReorderVector->size()<maxNumColumnToDisplay) maxNumColumnToDisplay = (int)pOptionalColumnReorderVector->size();
int col = 0;
ImVector<HeaderData> headerData; // We can remove this ImVector, if we call getHeaderData(...) 2X times (not sure if it's faster)
headerData.resize(maxNumColumnToDisplay);
int columnSortingIndex = -1;
static ImColor transparentColor(1,1,1,0);
// Column headers
ImGui::Columns(maxNumColumnToDisplay);
ImGui::PushStyleColor(ImGuiCol_Button,transparentColor);
for (int colID=0;colID<maxNumColumnToDisplay;colID++) {
col = pOptionalColumnReorderVector ? (*pOptionalColumnReorderVector)[colID] : colID;
ImGui::Separator();
HeaderData& hd = headerData[colID];
hd.reset();
getHeaderData(col,hd);
if (!hd.sorting.sortable) ImGui::Text(hd.name);
else if (ImGui::SmallButton(hd.name)) columnSortingIndex = col;
ImGui::Separator();
if (colID!=maxNumColumnToDisplay-1) ImGui::NextColumn();
}
ImGui::PopStyleColor();
ImGui::Columns(1);
// Rows
float itemHeight = ImGui::GetTextLineHeightWithSpacing();
int displayStart = 0, displayEnd = (int) numRows;
ImGui::CalcListClipping(numRows, itemHeight, &displayStart, &displayEnd);
if (scrollToRow>=0) {
if (displayStart>scrollToRow) displayStart = scrollToRow;
else if (displayEnd<=scrollToRow) displayEnd = scrollToRow+1;
else scrollToRow = -1; // we reset it now
}
ImGui::SetCursorPosY(ImGui::GetCursorPosY() + (displayStart * itemHeight));
bool rowSelectionChanged = false;bool colSelectionChanged = false; // The latter is not exposed but might turn useful
bool isThisRowSelected = false;const char* txt=NULL;bool mustDisplayEditor = false;
editingModePresent = false;
const ImVec4 ImGuiColHeader = ImGui::GetStyle().Colors[ImGuiCol_Header];
HeaderData* hd;CellData cd;
ImGui::Columns(maxNumColumnToDisplay);
for (int colID=0;colID<maxNumColumnToDisplay;colID++) {
col = pOptionalColumnReorderVector ? (*pOptionalColumnReorderVector)[colID] : colID;
hd = &headerData[colID];
const HeaderData::Type& hdType = hd->type;
//const HeaderData::Formatting& hdFormatting = hd->formatting;
//const HeaderData::Editing& hdEditing = hd->editing;
const bool hdEditable = hd->editing.editable;
if (!hdEditable) {
ImGui::PushStyleColor(ImGuiCol_HeaderHovered,transparentColor);
ImGui::PushStyleColor(ImGuiCol_HeaderActive,transparentColor);
}
for (int row = displayStart; row < displayEnd; ++row) {
isThisRowSelected = (selectedRow == row);
mustDisplayEditor = isThisRowSelected && hdEditable && selectedColumn==col && hd->type.headerType!=HT_CUSTOM && editorAllowed;
if (colID==0 && row==scrollToRow) ImGui::SetScrollPosHere();
cd.reset();
getCellData((size_t)row,col,cd);
ImGui::PushID(cd.fieldPtr);
if (mustDisplayEditor) {
editingModePresent = true;
const HeaderData::Editing& hdEditing = hd->editing;
// Draw editor here--------------------------------------------
const int hdPrecision = hdEditing.precisionOrStringBufferSize;
static const int precisionStrSize = 16;static char precisionStr[precisionStrSize];int precisionLastCharIndex;
if (hdPrecision>0) {
strcpy(precisionStr,"%.");
snprintf(&precisionStr[2], precisionStrSize-2,"%ds",hdPrecision);
precisionLastCharIndex = strlen(precisionStr)-1;
}
else {
strcpy(precisionStr,"%s");
precisionLastCharIndex = 1;
}
switch (hdType.headerType) {
case HT_DOUBLE: {
const float minValue = (float) hdEditing.minValue;
const float maxValue = (float) hdEditing.maxValue;
double* pField = (double*)cd.fieldPtr;
float value = (float) *pField;
precisionStr[precisionLastCharIndex]='f';
if (ImGui::SliderFloat("##SliderDoubleEditor",&value,minValue,maxValue,precisionStr)) {
*pField = (double) value;
}
}
break;
case HT_FLOAT: {
const float minValue = (float) hdEditing.minValue;
const float maxValue = (float) hdEditing.maxValue;
float* pField = (float*) cd.fieldPtr;
float value = (float) *pField;
precisionStr[precisionLastCharIndex]='f';
if (ImGui::SliderFloat("##SliderFloatEditor",&value,minValue,maxValue,precisionStr)) {
*pField = (float) value;
}
}
break;
case HT_UNSIGNED: {
const int minValue = (int) hdEditing.minValue;
const int maxValue = (int) hdEditing.maxValue;
unsigned* pField = (unsigned*) cd.fieldPtr;
int value = (int) *pField;
//precisionStr[precisionLastCharIndex]='d';
if (ImGui::SliderInt("##SliderUnsignedEditor",&value,minValue,maxValue))//,precisionStr))
{
*pField = (unsigned) value;
}
}
break;
case HT_INT: {
const int minValue = (int) hdEditing.minValue;
const int maxValue = (int) hdEditing.maxValue;
int* pField = (int*) cd.fieldPtr;
int value = (int) *pField;
//precisionStr[precisionLastCharIndex]='d';
if (ImGui::SliderInt("##SliderIntEditor",&value,minValue,maxValue)) //,precisionStr))
{
*pField = (int) value;
}
}
break;
case HT_BOOL: {
bool * boolPtr = (bool*) cd.fieldPtr;
if (*boolPtr) ImGui::Checkbox("true##CheckboxBoolEditor",boolPtr); // returns true when pressed
else ImGui::Checkbox("false##CheckboxBoolEditor",boolPtr); // returns true when pressed
}
break;
case HT_ENUM: {
ImGui::Combo("##ComboEnumEditor",(int*) cd.fieldPtr,hdType.textFromEnumFunctionPointer,hdType.textFromEnumFunctionPointerUserData,hdType.numEnumElements);
}
break;
case HT_STRING: {
char* txtField = (char*) cd.fieldPtr;
ImGui::InputText("##InputTextEditor",txtField,hdPrecision,ImGuiInputTextFlags_EnterReturnsTrue);
}
break;
default: break;
}
// End Draw Editor here----------------------------------------
}
else {
txt = NULL;
if (hdType.headerType==HT_CUSTOM) txt = cd.customText;
else txt = GetTextFromCellFieldDataPtr(*hd,cd.fieldPtr);
if (txt) {
if (isThisRowSelected && !hdEditable) {
ImGui::PushStyleColor(ImGuiCol_HeaderHovered,ImGuiColHeader);
ImGui::PushStyleColor(ImGuiCol_HeaderActive,ImGuiColHeader);
}
if (ImGui::Selectable(txt,cd.selectedRowPtr)) {
if (!*cd.selectedRowPtr) {
*cd.selectedRowPtr = true;
rowSelectionChanged = true;
editorAllowed = (selectedColumn==col);
}
else editorAllowed = false;
if (selectedRow!=row && selectedRow>=0 && selectedRow<(int)numRows) {
// remove old selection
CellData cdOld;getCellData((size_t)selectedRow,0,cdOld); // Note that we use column 0 (since we retrieve a row data it makes no difference)
if (cdOld.selectedRowPtr) *cdOld.selectedRowPtr = false;
}
selectedRow = row;
if (selectedColumn!=col) colSelectionChanged = true;
selectedColumn = col;
}
if (isThisRowSelected && !hdEditable) {
// must be the same colors as (*)
ImGui::PopStyleColor();
ImGui::PopStyleColor();
}
}
}
ImGui::PopID();
}
if (!hdEditable) {
ImGui::PopStyleColor();
ImGui::PopStyleColor();
}
if (colID!=maxNumColumnToDisplay-1) ImGui::NextColumn();
}
ImGui::Columns(1);
ImGui::Separator();
ImGui::SetCursorPosY(ImGui::GetCursorPosY() + ((numRows - displayEnd) * itemHeight));
ImGui::PopID();
scrollToRow = -1; // we must reset it
// Sorting:
if (columnSortingIndex>=0) const_cast<ListViewBase*>(this)->sort((size_t) columnSortingIndex);
return rowSelectionChanged; // Optional data we might want to expose: local variable: 'colSelectionChanged' and class variable: 'isInEditingMode'.
}
};
class ListView : public ListViewBase {
public:
static const int MaxHeaderSizeInBytes = 256;
class Header {
public:
char name[MaxHeaderSizeInBytes];
char prefix[MaxHeaderSizeInBytes];
char suffix[MaxHeaderSizeInBytes];
HeaderData hd; // Not necesssary. It's here just to cut code length
void* userPtr; // user responsibility
Header(const char* _name,const HeaderData::Type& _type,const int _precision=-1,const char* _prefix="",const char* _suffix="",const HeaderData::Sorting& _sorting = HeaderData::Sorting(),const HeaderData::Editing& _editing = HeaderData::Editing()) {
init(_name,_type,_precision,_prefix,_suffix,_sorting,_editing);
}
Header(const char* _name,const HeaderType _type,const int _precision,const char* _prefix="",const char* _suffix="",const HeaderData::Sorting& _sorting = HeaderData::Sorting(),const HeaderData::Editing& _editing = HeaderData::Editing()) {
init(_name,HeaderData::Type(_type),_precision,_prefix,_suffix,_sorting,_editing);
}
Header(const char* _name,const HeaderData::Type& _type,const int _precision,const char* _prefix,const char* _suffix,const bool _sorting,const HeaderData::Editing& _editing = HeaderData::Editing()) {
init(_name,_type,_precision,_prefix,_suffix,HeaderData::Sorting(_sorting),_editing);
}
Header(const char* _name,const HeaderType _type,const int _precision,const char* _prefix,const char* _suffix,const bool _sorting,const HeaderData::Editing& _editing = HeaderData::Editing()) {
init(_name,HeaderData::Type(_type),_precision,_prefix,_suffix,HeaderData::Sorting(_sorting),_editing);
}
protected:
void init(const char* _name,const HeaderData::Type& _type,const int _precision=-1,const char* _prefix="",const char* _suffix="",const HeaderData::Sorting& _sorting = HeaderData::Sorting(),const HeaderData::Editing& _editing = HeaderData::Editing())
{
IM_ASSERT(_name && strlen(_name)<MaxHeaderSizeInBytes);
IM_ASSERT(_type.headerType!=HT_ENUM || _type.textFromEnumFunctionPointer);
IM_ASSERT(_prefix && strlen(_prefix)<MaxHeaderSizeInBytes);
IM_ASSERT(_suffix && strlen(_suffix)<MaxHeaderSizeInBytes);
IM_ASSERT(!(_type.headerType==HT_STRING && _editing.editable && _editing.precisionOrStringBufferSize<=0)); // _editing.precisionOrStringBufferSize must be >=0 (the size of the string buffer in bytes)
strcpy(name,_name);
strcpy(prefix,_prefix);
strcpy(suffix,_suffix);
hd.type = _type;
hd.formatting.precision = _precision;
hd.sorting = _sorting;
hd.editing = _editing;
userPtr = NULL;
}
};
class ItemBase {
public:
virtual const char* getCustomText(size_t column) const {return "";} // Must be implemented only for columns with type HT_CUSTOM
virtual const void* getDataPtr(size_t column) const=0; // Must be implemented for all fields
ItemBase() : selected(false) {}
virtual ~ItemBase() {}
public:
class SortingHelper {
inline static int& getColumn() {static int column=0;return column;}
inline static int& getArrayIndex() {static int arrayIndex=0;return arrayIndex;}
inline static bool& getAscendingOrder() {static bool ascendingOrder=true;return ascendingOrder;}
public:
SortingHelper(int _column=0,bool _ascendingOrder=true,int _arrayIndex=0) {
getColumn()=_column;
getArrayIndex()=_arrayIndex;
getAscendingOrder()=_ascendingOrder;
}
template <typename T> inline static int Compare(const void* item0,const void* item1) {
const ItemBase* it0 = *((const ItemBase**) item0);
const ItemBase* it1 = *((const ItemBase**) item1);
const T& v0 = *((const T*)(it0->getDataPtr(getColumn()))+getArrayIndex());
const T& v1 = *((const T*)(it1->getDataPtr(getColumn()))+getArrayIndex());
return getAscendingOrder() ? ((v0<v1)?-1:(v0>v1)?1:0) : ((v0>v1)?-1:(v0<v1)?1:0);
}
inline static int Compare_HT_BOOL(const void* item0,const void* item1) {
const ItemBase* it0 = *((const ItemBase**) item0);
const ItemBase* it1 = *((const ItemBase**) item1);
const bool& v0 = *((const bool*)(it0->getDataPtr(getColumn()))+getArrayIndex());
const bool& v1 = *((const bool*)(it1->getDataPtr(getColumn()))+getArrayIndex());
return (v0==v1) ? 0 : (getAscendingOrder() ? (v0?-1:1) : (v0?1:-1));
}
inline static int Compare_HT_CUSTOM(const void* item0,const void* item1) {
const ItemBase* it0 = *((const ItemBase**) item0);
const ItemBase* it1 = *((const ItemBase**) item1);
const char* v0 = it0->getCustomText(getColumn());
const char* v1 = it1->getCustomText(getColumn());
return getAscendingOrder() ? ((v0<v1)?-1:(v0>v1)?1:0) : ((v0>v1)?-1:(v0<v1)?1:0);
}
};
private:
mutable bool selected; // true selects the item row, false deselects it.
friend class ListView;
};
ImVector<Header> headers; // one per column
ImVector<ItemBase*> items; // one per row
public:
virtual ~ListView() {
for (size_t i=0,isz=items.size();i<isz;i++) {
ItemBase*& item = items[i];
item->~ItemBase(); // ImVector does not call it
ImGui::MemFree(item); // items MUST be allocated by the user using ImGui::MemAlloc(...)
item=NULL;
}
items.clear();
}
// overridden methods:
void getHeaderData(size_t column,HeaderData& headerDataOut) const {
// Here we just have to fill as many headerDataOut fields as we can. IMPORTANT: headerDataOut strings are only references (i.e. don't use strcpy(...)!)
if (column>=headers.size()) return;
const Header& h = headers[column];
headerDataOut = h.hd; // To speed up this code I've added hd inside h, but this is not necessary.
// Mandatory: headerDataOut just stores the string references:
headerDataOut.name = h.name;
headerDataOut.formatting.prefix = h.prefix;
headerDataOut.formatting.suffix = h.suffix;
}
void getCellData(size_t row,size_t column,CellData& cellDataOut) const {
if (row>=items.size() || column>=headers.size()) return;
const ItemBase& it = *(items[row]);
cellDataOut.fieldPtr = it.getDataPtr(column);
cellDataOut.selectedRowPtr = &it.selected;
if (headers[column].hd.type.headerType==HT_CUSTOM) cellDataOut.customText = it.getCustomText(column);
else cellDataOut.customText = NULL;
}
bool sort(size_t column) {
if (column>=headers.size()) return false;
Header& h = headers[column];
HeaderData::Sorting& hds = h.hd.sorting;
if (!hds.sortable) return false;
// void qsort( void *ptr, size_t count, size_t size,int (*comp)(const void *, const void *) );
bool& sortingOrder = hds.sortingAscending;
ItemBase::SortingHelper sorter((int)column,sortingOrder,hds.sortableElementOfPossibleArray); // This IS actually used!
typedef int (*CompareDelegate)(const void *, const void *);
CompareDelegate compareFunction = NULL;
switch (h.hd.type.headerType) {
case HT_BOOL:
compareFunction = ItemBase::SortingHelper::Compare_HT_BOOL;
break;
case HT_CUSTOM:
compareFunction = ItemBase::SortingHelper::Compare_HT_CUSTOM;
break;
case HT_INT:
case HT_ENUM:
compareFunction = ItemBase::SortingHelper::Compare<int>;
break;
case HT_UNSIGNED:
compareFunction = ItemBase::SortingHelper::Compare<unsigned>;
break;
case HT_FLOAT:
compareFunction = ItemBase::SortingHelper::Compare<float>;
break;
case HT_DOUBLE:
compareFunction = ItemBase::SortingHelper::Compare<double>;
break;
case HT_STRING:
compareFunction = ItemBase::SortingHelper::Compare<char*>;
break;
default:
return false;
}
if (!compareFunction) return false;
qsort((void *) &items[0],items.size(),sizeof(ItemBase*),compareFunction);
sortingOrder = !sortingOrder; // next time it sorts backwards
updateSelectedRow(); // rows get shuffled after sorting: the visible selection is still correct (the boolean flag ItemBase::selected is stored in our row-item),
// but the 'selectedRow' field is not updated and must be adjusted
return true;
}
size_t getNumColumns() const {return headers.size();}
size_t getNumRows() const {return items.size();}
protected:
};
typedef ListView::Header ListViewHeader;
typedef ListViewBase::HeaderData::Type ListViewHeaderType;
typedef ListViewBase::HeaderData::Formatting ListViewHeaderFormatting;
typedef ListViewBase::HeaderData::Sorting ListViewHeaderSorting;
typedef ListViewBase::HeaderData::Editing ListViewHeaderEditing;
// A handy method just to test the classes above. Can be removed otherwise.
inline void TestListView() {
ImGui::Spacing();
static ImGui::ListView lv;
if (lv.headers.size()==0) {
lv.headers.push_back(ImGui::ListViewHeader("Index",ImGui::ListView::HT_INT));
lv.headers.push_back(ImGui::ListViewHeader("Path",ImGui::ListView::HT_STRING,-1,"","",true,ImGui::ListViewHeaderEditing(true,1024)));
lv.headers.push_back(ImGui::ListViewHeader("Offset",ImGui::ListView::HT_INT,-1,"","",true));
lv.headers.push_back(ImGui::ListViewHeader("Bytes",ImGui::ListView::HT_UNSIGNED));
lv.headers.push_back(ImGui::ListViewHeader("Valid",ImGui::ListView::HT_BOOL,-1,"Flag: ","!",true,ImGui::ListViewHeaderEditing(true)));
lv.headers.push_back(ImGui::ListViewHeader("Length",ImGui::ListView::HT_DOUBLE,2,""," mt",true,ImGui::ListViewHeaderEditing(true,3,0.0,10.0))); // Note that here we use 2 decimals (precision), but 3 when editing
// Warning: old compilers don't like defining classes inside function scopes
class MyListViewItem : public ImGui::ListView::ItemBase {
public:
// Support static method for enum1 (the signature is the same used by ImGui::Combo(...))
static bool GetTextFromEnum1(void* ,int value,const char** pTxt) {
if (!pTxt) return false;
static const char* values[] = {"APPLE","LEMON","ORANGE"};
static int numValues = (int)(sizeof(values)/sizeof(values[0]));
if (value>=0 && value<numValues) *pTxt = values[value];
else *pTxt = "UNKNOWN";
return true;
}
// Fields and their pointers (MANDATORY!)
int index;
char path[1024]; // Note that if this column is editable, we must specify: ImGui::ListViewHeaderEditing(true,1024); in the ImGui::ListViewHeader::ctr().
int offset;
unsigned bytes;
bool valid;
double length;
int enum1; // Note that it's an enum!
const void* getDataPtr(size_t column) const {
switch (column) {
case 0: return (const void*) &index;
case 1: return (const void*) path;
case 2: return (const void*) &offset;
case 3: return (const void*) &bytes;
case 4: return (const void*) &valid;
case 5: return (const void*) &length;
case 6: return (const void*) &enum1;
}
return NULL;
// Please note that we can easily try to speed up this method by adding a new field like:
// const void* fieldPointers[number of fields]; // and assigning them in our ctr
// Then here we can just use:
// IM_ASSERT(column<number of fields);
// return fieldPointers[column];
}
// (Optional) ctr for setting values faster later
MyListViewItem(int _index,const char* _path,int _offset,unsigned _bytes,bool _valid,double _length,int _enum1)
: index(_index),offset(_offset),bytes(_bytes),valid(_valid),length(_length),enum1(_enum1) {
IM_ASSERT(_path && strlen(_path)<1024);
strcpy(path,_path);
}
virtual ~MyListViewItem() {}
};
// for enums we must use the ctr that takes an ImGui::ListViewHeaderType, so we can pass the additional params to bind the enum:
lv.headers.push_back(ImGui::ListViewHeader("Enum1",ImGui::ListViewHeaderType(ImGui::ListView::HT_ENUM,3,&MyListViewItem::GetTextFromEnum1),-1,"","",true,ImGui::ListViewHeaderEditing(true)));
// Just a test: 10000 items
lv.items.resize(10000);
MyListViewItem* item;
for (int i=0;i<(int)lv.items.size();i++) {
item = (MyListViewItem*) ImGui::MemAlloc(sizeof(MyListViewItem)); // MANDATORY (ImGuiListView::~ImGuiListView() will delete these with ImGui::MemFree(...))
new (item) MyListViewItem(i,"My ' ' Dummy Path",i*3,(unsigned)i*4,(i%3==0)?true:false,(double)(i*30)/2.7345672,i%3); // MANDATORY even with blank ctrs. Requires: #include <new>. Reason: ImVector does not call ctrs/dctrs on items.
item->path[4]=(char) (33+(i%64)); //just to test sorting on strings
item->path[5]=(char) (33+(i/127)); //just to test sorting on strings
lv.items[i] = item;
}
}
// 2 lines just to have some feedback
if (ImGui::Button("Scroll to selected row")) lv.scrollToSelectedRow();ImGui::SameLine();
ImGui::Text("selectedRow:%d selectedColumn:%d isInEditingMode:%s",lv.getSelectedRow(),lv.getSelectedColumn(),lv.isInEditingMode() ? "true" : "false");
/*
static ImVector<int> optionalColumnReorder;
if (optionalColumnReorder.size()==0) {
const int numColumns = lv.headers.size();
optionalColumnReorder.resize(numColumns);
for (int i=0;i<numColumns;i++) optionalColumnReorder[i] = numColumns-i-1;
}
*/
lv.render();//&optionalColumnReorder,-1); // This method returns true when the selectedRow is changed by the user (however when selectedRow gets changed because of sorting it still returns false, because the pointed row-item does not change)
}
} // namespace ImGui
#endif //IMGUILISTVIEW_H_
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment