Last active
October 13, 2023 23:19
-
-
Save Flix01/94b0bf3069476a1344ac to your computer and use it in GitHub Desktop.
Minimal ListView implementation for ImGui version v1.31
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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