Merge branch '4238-fix-grid-multicell-editing' of https://github.com/discnl/wxWidgets
Fix grid multicell integrity after inserting/deleting rows/columns. See https://github.com/wxWidgets/wxWidgets/pull/2192
This commit is contained in:
@@ -2511,9 +2511,6 @@ public:
|
||||
/**
|
||||
Delete rows from the table.
|
||||
|
||||
Notice that currently deleting a row intersecting a multi-cell (see
|
||||
SetCellSize()) is not supported and will result in a crash.
|
||||
|
||||
@param pos
|
||||
The first row to delete.
|
||||
@param numRows
|
||||
|
@@ -502,10 +502,10 @@ GridFrame::GridFrame()
|
||||
colMenu->Append( ID_SET_CELL_BG_COLOUR, "Set cell &background colour..." );
|
||||
|
||||
wxMenu *editMenu = new wxMenu;
|
||||
editMenu->Append( ID_INSERTROW, "Insert &row" );
|
||||
editMenu->Append( ID_INSERTCOL, "Insert &column" );
|
||||
editMenu->Append( ID_DELETEROW, "Delete selected ro&ws" );
|
||||
editMenu->Append( ID_DELETECOL, "Delete selected co&ls" );
|
||||
editMenu->Append( ID_INSERTROW, "Insert &rows\tCtrl+I" );
|
||||
editMenu->Append( ID_INSERTCOL, "Insert &columns\tCtrl+Shift+I" );
|
||||
editMenu->Append( ID_DELETEROW, "Delete selected ro&ws\tCtrl+D" );
|
||||
editMenu->Append( ID_DELETECOL, "Delete selected co&ls\tCtrl+Shift+D" );
|
||||
editMenu->Append( ID_CLEARGRID, "Cl&ear grid cell contents" );
|
||||
editMenu->Append( ID_EDITCELL, "&Edit current cell" );
|
||||
editMenu->Append( ID_SETCORNERLABEL, "&Set corner label..." );
|
||||
@@ -1235,32 +1235,115 @@ void GridFrame::SetGridLineColour( wxCommandEvent& WXUNUSED(ev) )
|
||||
}
|
||||
}
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
// Helper used to insert/delete rows/columns. Allows changing by more than
|
||||
// one row/column at once.
|
||||
void HandleEdits(wxGrid* grid, wxGridDirection direction, bool isInsertion)
|
||||
{
|
||||
const bool isRow = (direction == wxGRID_ROW);
|
||||
|
||||
const wxGridBlocks selected = grid->GetSelectedBlocks();
|
||||
wxGridBlocks::iterator it = selected.begin();
|
||||
|
||||
if ( isInsertion )
|
||||
{
|
||||
int pos, count;
|
||||
// Only do multiple insertions if we have a single consecutive
|
||||
// selection (mimicking LibreOffice), otherwise do a single insertion
|
||||
// at cursor.
|
||||
if ( it != selected.end() && ++it == selected.end() )
|
||||
{
|
||||
const wxGridBlockCoords& b = *selected.begin();
|
||||
pos = isRow ? b.GetTopRow() : b.GetLeftCol();
|
||||
count = (isRow ? b.GetBottomRow() : b.GetRightCol()) - pos + 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
pos = isRow ? grid->GetGridCursorRow() : grid->GetGridCursorCol();
|
||||
count = 1;
|
||||
}
|
||||
|
||||
if ( isRow )
|
||||
grid->InsertRows(pos, count);
|
||||
else
|
||||
grid->InsertCols(pos, count);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
wxGridUpdateLocker locker(grid);
|
||||
|
||||
wxVector<int> deletions;
|
||||
|
||||
for (; it != selected.end(); ++it )
|
||||
{
|
||||
const wxGridBlockCoords& b = *it;
|
||||
const int begin = isRow ? b.GetTopRow() : b.GetLeftCol();
|
||||
const int end = isRow ? b.GetBottomRow() : b.GetRightCol();
|
||||
|
||||
for ( int n = begin; n <= end; ++n )
|
||||
{
|
||||
if ( !wxVectorContains(deletions, n) )
|
||||
deletions.push_back(n);
|
||||
}
|
||||
}
|
||||
|
||||
wxVectorSort(deletions);
|
||||
|
||||
const size_t deletionCount = deletions.size();
|
||||
|
||||
if ( !deletionCount )
|
||||
return;
|
||||
|
||||
int seqEnd = 0, seqCount = 0;
|
||||
for ( size_t i = deletionCount; i > 0; --i )
|
||||
{
|
||||
const int n = deletions[i - 1];
|
||||
|
||||
if ( n != seqEnd - seqCount )
|
||||
{
|
||||
if (i != deletionCount)
|
||||
{
|
||||
const int seqStart = seqEnd - seqCount + 1;
|
||||
if ( isRow )
|
||||
grid->DeleteRows(seqStart, seqCount);
|
||||
else
|
||||
grid->DeleteCols(seqStart, seqCount);
|
||||
}
|
||||
|
||||
seqEnd = n;
|
||||
seqCount = 0;
|
||||
}
|
||||
|
||||
seqCount++;
|
||||
}
|
||||
|
||||
const int seqStart = seqEnd - seqCount + 1;
|
||||
if ( isRow )
|
||||
grid->DeleteRows(seqStart, seqCount);
|
||||
else
|
||||
grid->DeleteCols(seqStart, seqCount);
|
||||
}
|
||||
|
||||
} // anoymous namespace
|
||||
|
||||
void GridFrame::InsertRow( wxCommandEvent& WXUNUSED(ev) )
|
||||
{
|
||||
grid->InsertRows( grid->GetGridCursorRow(), 1 );
|
||||
HandleEdits(grid, wxGRID_ROW, /* isInsertion = */ true);
|
||||
}
|
||||
|
||||
|
||||
void GridFrame::InsertCol( wxCommandEvent& WXUNUSED(ev) )
|
||||
{
|
||||
grid->InsertCols( grid->GetGridCursorCol(), 1 );
|
||||
HandleEdits(grid, wxGRID_COLUMN, /* isInsertion = */ true);
|
||||
}
|
||||
|
||||
|
||||
void GridFrame::DeleteSelectedRows( wxCommandEvent& WXUNUSED(ev) )
|
||||
{
|
||||
if ( grid->IsSelection() )
|
||||
{
|
||||
wxGridUpdateLocker locker(grid);
|
||||
for ( int n = 0; n < grid->GetNumberRows(); )
|
||||
{
|
||||
if ( grid->IsInSelection( n , 0 ) )
|
||||
grid->DeleteRows( n, 1 );
|
||||
else
|
||||
n++;
|
||||
}
|
||||
}
|
||||
HandleEdits(grid, wxGRID_ROW, /* isInsertion = */ false);
|
||||
}
|
||||
|
||||
|
||||
@@ -1323,17 +1406,7 @@ void GridFrame::AutoSizeTable(wxCommandEvent& WXUNUSED(event))
|
||||
|
||||
void GridFrame::DeleteSelectedCols( wxCommandEvent& WXUNUSED(ev) )
|
||||
{
|
||||
if ( grid->IsSelection() )
|
||||
{
|
||||
wxGridUpdateLocker locker(grid);
|
||||
for ( int n = 0; n < grid->GetNumberCols(); )
|
||||
{
|
||||
if ( grid->IsInSelection( 0 , n ) )
|
||||
grid->DeleteCols( n, 1 );
|
||||
else
|
||||
n++;
|
||||
}
|
||||
}
|
||||
HandleEdits(grid, wxGRID_COLUMN, /* isInsertion = */ false);
|
||||
}
|
||||
|
||||
|
||||
|
@@ -806,72 +806,183 @@ wxGridCellAttr *wxGridCellAttrData::GetAttr(int row, int col) const
|
||||
return attr;
|
||||
}
|
||||
|
||||
void wxGridCellAttrData::UpdateAttrRows( size_t pos, int numRows )
|
||||
namespace
|
||||
{
|
||||
size_t count = m_attrs.GetCount();
|
||||
|
||||
void UpdateCellAttrRowsOrCols(wxGridCellWithAttrArray& attrs, int editPos,
|
||||
int editRowCount, int editColCount)
|
||||
{
|
||||
wxASSERT( !editRowCount || !editColCount );
|
||||
|
||||
const bool isEditingRows = (editRowCount != 0);
|
||||
const int editCount = (isEditingRows ? editRowCount : editColCount);
|
||||
|
||||
size_t count = attrs.GetCount();
|
||||
for ( size_t n = 0; n < count; n++ )
|
||||
{
|
||||
wxGridCellCoords& coords = m_attrs[n].coords;
|
||||
wxCoord row = coords.GetRow();
|
||||
if ((size_t)row >= pos)
|
||||
wxGridCellAttr* cellAttr = attrs[n].attr;
|
||||
int cellRows, cellCols;
|
||||
cellAttr->GetSize(&cellRows, &cellCols);
|
||||
|
||||
wxGridCellCoords& coords = attrs[n].coords;
|
||||
const wxCoord cellRow = coords.GetRow(),
|
||||
cellCol = coords.GetCol(),
|
||||
cellPos = (isEditingRows ? cellRow : cellCol);
|
||||
|
||||
if ( cellPos < editPos )
|
||||
{
|
||||
if (numRows > 0)
|
||||
// This cell's coords aren't influenced by the editing, however
|
||||
// do adjust a multicell's main size, if needed.
|
||||
if ( GetCellSpan(cellRows, cellCols) == wxGrid::CellSpan_Main )
|
||||
{
|
||||
// If rows inserted, increment row counter where necessary
|
||||
coords.SetRow(row + numRows);
|
||||
int mainSize = isEditingRows ? cellRows : cellCols;
|
||||
if ( cellPos + mainSize > editPos )
|
||||
{
|
||||
// Multicell is within affected range:
|
||||
// Adjust its size.
|
||||
|
||||
if ( editCount >= 0 )
|
||||
{
|
||||
mainSize += editCount;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Reduce multicell size by number of deletions, but
|
||||
// never more than the multicell's size minus one:
|
||||
// cellPos (the main cell) is always less than editPos
|
||||
// at this point, then with the below code a multicell
|
||||
// with size 7 is at most reduced by:
|
||||
// cellPos + 7 - (cellPos + 1) = 7 - 1 = 6.
|
||||
mainSize -= wxMin(-editCount,
|
||||
cellPos + mainSize - editPos);
|
||||
/*
|
||||
The above was derived from:
|
||||
first_del = edit
|
||||
last_del = min(edit - count - 1, cell + size - 1)
|
||||
size -= (last_del + 1 - first_del)
|
||||
|
||||
eliminating the 1's:
|
||||
|
||||
first_del = edit
|
||||
last_del = min(edit - count, cell + size)
|
||||
size -= (last_del - first_del)
|
||||
|
||||
reducing each by edit:
|
||||
|
||||
first_del = 0
|
||||
last_del_plus_1 = min(0 - count, cell + size - edit)
|
||||
size -= (last_del_plus_1 - 0)
|
||||
|
||||
after eliminating the 0's and substitution, leaving:
|
||||
|
||||
size -= min(-count, cell + size - edit)
|
||||
|
||||
E.g. with a multicell of size 7 and at 2 positions
|
||||
after the main cell 100 positions are deleted then
|
||||
the size will not (/can't) be reduced by 100 cells
|
||||
but by:
|
||||
|
||||
cellPos + 7 - editPos = # editPos = cellPos + 2
|
||||
cellPos + 7 - (cellPos + 2) = # eliminate cellPos
|
||||
7 - 2 =
|
||||
5 cells, making the final size 7 - 5 = 2.
|
||||
*/
|
||||
}
|
||||
|
||||
cellAttr->SetSize(isEditingRows ? mainSize : cellRows,
|
||||
isEditingRows ? cellCols : mainSize);
|
||||
}
|
||||
}
|
||||
else if (numRows < 0)
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( editCount < 0 && cellPos < editPos - editCount )
|
||||
{
|
||||
// This row/col is deleted and the cell doesn't exist any longer:
|
||||
// Remove the attribute.
|
||||
attrs.RemoveAt(n);
|
||||
n--;
|
||||
count--;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( GetCellSpan(cellRows, cellCols) != wxGrid::CellSpan_Inside )
|
||||
{
|
||||
// Rows/cols inserted or deleted (and this cell still exists):
|
||||
// Adjust cell coords.
|
||||
coords.Set(cellRow + editRowCount, cellCol + editColCount);
|
||||
|
||||
// Nothing more to do: cell is not an inside cell of a multicell.
|
||||
continue;
|
||||
}
|
||||
|
||||
// Handle inside cell's existence, coords, and size.
|
||||
|
||||
const int mainPos = cellPos + (isEditingRows ? cellRows : cellCols);
|
||||
|
||||
if ( editCount < 0
|
||||
&& mainPos >= editPos && mainPos < editPos - editCount )
|
||||
{
|
||||
// On a position that still exists after deletion but main cell
|
||||
// of multicell is within deletion range so the multicell is gone:
|
||||
// Remove the attribute.
|
||||
attrs.RemoveAt(n);
|
||||
n--;
|
||||
count--;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// Rows/cols inserted or deleted (and this inside cell still exists):
|
||||
// Adjust (inside) cell coords.
|
||||
coords.Set(cellRow + editRowCount, cellCol + editColCount);
|
||||
|
||||
if ( mainPos >= editPos )
|
||||
{
|
||||
// Nothing more to do: the multicell that this inside cell is part
|
||||
// of is moving its main cell as well so offsets to the main cell
|
||||
// don't change and there are no edits changing the multicell size.
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( editCount > 0 && cellPos == editPos )
|
||||
{
|
||||
// At an (old) position that is newly inserted: this is the only
|
||||
// opportunity to add required inside cells that point to
|
||||
// the main cell. E.g. with a 2x1 multicell that increases in size
|
||||
// there's only one inside cell that will be visited while there
|
||||
// can be multiple insertions.
|
||||
for ( int i = 0; i < editCount; ++i )
|
||||
{
|
||||
// If rows deleted ...
|
||||
if ((size_t)row >= pos - numRows)
|
||||
{
|
||||
// ...either decrement row counter (if row still exists)...
|
||||
coords.SetRow(row + numRows);
|
||||
}
|
||||
else
|
||||
{
|
||||
// ...or remove the attribute
|
||||
m_attrs.RemoveAt(n);
|
||||
n--;
|
||||
count--;
|
||||
}
|
||||
const int adjustRows = i * isEditingRows,
|
||||
adjustCols = i * !isEditingRows;
|
||||
|
||||
wxGridCellAttr* attr = new wxGridCellAttr;
|
||||
attr->SetSize(cellRows - adjustRows, cellCols - adjustCols);
|
||||
|
||||
attrs.Add(new wxGridCellWithAttr(cellRow + adjustRows,
|
||||
cellCol + adjustCols,
|
||||
attr));
|
||||
}
|
||||
}
|
||||
|
||||
// Let this inside cell's size point to the main cell of the multicell.
|
||||
cellAttr->SetSize(cellRows - editRowCount, cellCols - editColCount);
|
||||
}
|
||||
}
|
||||
|
||||
} // anonymous namespace
|
||||
|
||||
void wxGridCellAttrData::UpdateAttrRows( size_t pos, int numRows )
|
||||
{
|
||||
UpdateCellAttrRowsOrCols(m_attrs, static_cast<int>(pos), numRows, 0);
|
||||
}
|
||||
|
||||
void wxGridCellAttrData::UpdateAttrCols( size_t pos, int numCols )
|
||||
{
|
||||
size_t count = m_attrs.GetCount();
|
||||
for ( size_t n = 0; n < count; n++ )
|
||||
{
|
||||
wxGridCellCoords& coords = m_attrs[n].coords;
|
||||
wxCoord col = coords.GetCol();
|
||||
if ( (size_t)col >= pos )
|
||||
{
|
||||
if ( numCols > 0 )
|
||||
{
|
||||
// If cols inserted, increment col counter where necessary
|
||||
coords.SetCol(col + numCols);
|
||||
}
|
||||
else if (numCols < 0)
|
||||
{
|
||||
// If cols deleted ...
|
||||
if ((size_t)col >= pos - numCols)
|
||||
{
|
||||
// ...either decrement col counter (if col still exists)...
|
||||
coords.SetCol(col + numCols);
|
||||
}
|
||||
else
|
||||
{
|
||||
// ...or remove the attribute
|
||||
m_attrs.RemoveAt(n);
|
||||
n--;
|
||||
count--;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
UpdateCellAttrRowsOrCols(m_attrs, static_cast<int>(pos), 0, numCols);
|
||||
}
|
||||
|
||||
int wxGridCellAttrData::FindIndex(int row, int col) const
|
||||
|
@@ -31,7 +31,61 @@
|
||||
namespace
|
||||
{
|
||||
|
||||
// Derive a new class inheriting from wxGrid just to get access to its
|
||||
wxString CellCoordsToString(int row, int col)
|
||||
{
|
||||
return wxString::Format("R%dC%d", col + 1, row + 1);
|
||||
}
|
||||
|
||||
wxString CellSizeToString(int rows, int cols)
|
||||
{
|
||||
return wxString::Format("%dx%d", rows, cols);
|
||||
}
|
||||
|
||||
struct Multicell
|
||||
{
|
||||
int row, col, rows, cols;
|
||||
|
||||
Multicell(int row, int col, int rows, int cols)
|
||||
: row(row), col(col), rows(rows), cols(cols) { }
|
||||
|
||||
wxString Coords() const { return CellCoordsToString(row, col); }
|
||||
|
||||
wxString Size() const { return CellSizeToString(rows, cols); }
|
||||
|
||||
wxString ToString() const
|
||||
{
|
||||
wxString s;
|
||||
|
||||
if ( rows == 1 && cols == 1 )
|
||||
{
|
||||
s = "cell";
|
||||
}
|
||||
else
|
||||
{
|
||||
s = Size() + " ";
|
||||
if ( rows > 1 || cols > 1 )
|
||||
s += "multicell";
|
||||
else
|
||||
s += "inside cell";
|
||||
}
|
||||
|
||||
return wxString::Format("%s at %s", s, Coords());
|
||||
}
|
||||
};
|
||||
|
||||
// Stores insertion/deletion info of rows or columns.
|
||||
struct EditInfo
|
||||
{
|
||||
int pos, count;
|
||||
wxGridDirection direction;
|
||||
|
||||
EditInfo(int pos = 0,
|
||||
int count = 0,
|
||||
wxGridDirection direction = wxGRID_COLUMN)
|
||||
: pos(pos), count(count), direction(direction) { }
|
||||
};
|
||||
|
||||
// Derive a new class inheriting from wxGrid, also to get access to its
|
||||
// protected GetCellAttr(). This is not pretty, but we don't have any other way
|
||||
// of testing this function.
|
||||
class TestableGrid : public wxGrid
|
||||
@@ -46,10 +100,116 @@ public:
|
||||
{
|
||||
return GetCellAttr(row, col);
|
||||
}
|
||||
|
||||
bool HasAttr(int row, int col,
|
||||
wxGridCellAttr::wxAttrKind kind = wxGridCellAttr::Cell) const
|
||||
{
|
||||
// Can't use GetCellAttr() here as it always returns an attr.
|
||||
wxGridCellAttr* attr = GetTable()->GetAttr(row, col, kind);
|
||||
if ( attr )
|
||||
attr->DecRef();
|
||||
|
||||
return attr != NULL;
|
||||
}
|
||||
|
||||
size_t GetCellAttrCount() const
|
||||
{
|
||||
// Note that only attributes in grid range can easily be checked
|
||||
// and this function only counts those, not any outside of
|
||||
// the grid (e.g. with invalid negative coords).
|
||||
size_t count = 0;
|
||||
for ( int row = 0; row < GetNumberRows(); ++row )
|
||||
{
|
||||
for ( int col = 0; col < GetNumberCols(); ++col )
|
||||
count += HasAttr(row, col);
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
void SetMulticell(const Multicell& multi)
|
||||
{
|
||||
SetCellSize(multi.row, multi.col, multi.rows, multi.cols);
|
||||
}
|
||||
|
||||
// Performs given insertions/deletions on either rows or columns.
|
||||
void DoEdit(const EditInfo& edit);
|
||||
|
||||
// Returns annotated grid represented as a string.
|
||||
wxString ToString() const;
|
||||
|
||||
// Used when drawing annotated grid to know what happens to it.
|
||||
EditInfo m_edit;
|
||||
|
||||
// Grid as string before editing, with edit info annotated.
|
||||
wxString m_beforeGridAnnotated;
|
||||
};
|
||||
|
||||
// Compares two grids, checking for differences with attribute presence and
|
||||
// cell sizes.
|
||||
class GridAttrMatcher : public Catch::MatcherBase<TestableGrid>
|
||||
{
|
||||
public:
|
||||
GridAttrMatcher(const TestableGrid& grid);
|
||||
|
||||
bool match(const TestableGrid& other) const wxOVERRIDE;
|
||||
|
||||
std::string describe() const wxOVERRIDE;
|
||||
|
||||
private:
|
||||
const TestableGrid* m_grid;
|
||||
|
||||
mutable wxString m_diffDesc;
|
||||
mutable wxString m_expectedGridDesc;
|
||||
};
|
||||
|
||||
// Helper function for recreating a grid to fit (only) a multicell.
|
||||
void FitGridToMulticell(TestableGrid* grid, const Multicell& multi)
|
||||
{
|
||||
const int oldRowCount = grid->GetNumberRows();
|
||||
const int oldColCount = grid->GetNumberCols();
|
||||
|
||||
const int margin = 1;
|
||||
const int newRowCount = multi.row + multi.rows + margin;
|
||||
const int newColCount = multi.col + multi.cols + margin;
|
||||
|
||||
if ( !oldRowCount && !oldColCount )
|
||||
{
|
||||
grid->CreateGrid(newRowCount, newColCount);
|
||||
}
|
||||
else
|
||||
{
|
||||
grid->DeleteRows(0, oldRowCount);
|
||||
grid->DeleteCols(0, oldColCount);
|
||||
grid->AppendRows(newRowCount);
|
||||
grid->AppendCols(newColCount);
|
||||
}
|
||||
}
|
||||
|
||||
} // anonymous namespace
|
||||
|
||||
namespace Catch
|
||||
{
|
||||
|
||||
template <> struct StringMaker<TestableGrid>
|
||||
{
|
||||
static std::string convert(const TestableGrid& grid)
|
||||
{
|
||||
return ("Content before edit:\n" + grid.m_beforeGridAnnotated
|
||||
+ "\nContent after edit:\n" + grid.ToString()).ToStdString();
|
||||
}
|
||||
};
|
||||
|
||||
template <> struct StringMaker<Multicell>
|
||||
{
|
||||
static std::string convert(const Multicell& multi)
|
||||
{
|
||||
return multi.ToString().ToStdString();
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace Catch
|
||||
|
||||
class GridTestCase
|
||||
{
|
||||
public:
|
||||
@@ -57,6 +217,26 @@ public:
|
||||
~GridTestCase();
|
||||
|
||||
protected:
|
||||
void InsertRows(int pos = 0, int count = 1)
|
||||
{
|
||||
m_grid->DoEdit(EditInfo(pos, count, wxGRID_ROW));
|
||||
}
|
||||
|
||||
void InsertCols(int pos = 0, int count = 1)
|
||||
{
|
||||
m_grid->DoEdit(EditInfo(pos, count, wxGRID_COLUMN));
|
||||
}
|
||||
|
||||
void DeleteRows(int pos = 0, int count = 1)
|
||||
{
|
||||
m_grid->DoEdit(EditInfo(pos, -count, wxGRID_ROW));
|
||||
}
|
||||
|
||||
void DeleteCols(int pos = 0, int count = 1)
|
||||
{
|
||||
m_grid->DoEdit(EditInfo(pos, -count, wxGRID_COLUMN));
|
||||
}
|
||||
|
||||
// The helper function to determine the width of the column label depending
|
||||
// on whether the native column header is used.
|
||||
int GetColumnLabelWidth(wxClientDC& dc, int col, int margin) const
|
||||
@@ -106,12 +286,62 @@ protected:
|
||||
}
|
||||
}
|
||||
|
||||
bool HasCellAttr(int row, int col) const
|
||||
{
|
||||
return m_grid->HasAttr(row, col, wxGridCellAttr::Cell);
|
||||
}
|
||||
|
||||
void SetCellAttr(int row, int col)
|
||||
{
|
||||
m_grid->SetAttr(row, col, new wxGridCellAttr);
|
||||
}
|
||||
|
||||
// Fills temp. grid with a multicell and returns a matcher with it.
|
||||
GridAttrMatcher HasMulticellOnly(const Multicell& multi)
|
||||
{
|
||||
return CheckMulticell(multi);
|
||||
}
|
||||
|
||||
// Returns a matcher with empty (temp.) grid.
|
||||
GridAttrMatcher HasEmptyGrid()
|
||||
{
|
||||
return CheckMulticell(Multicell(0, 0, 1, 1));
|
||||
}
|
||||
|
||||
// Helper function used by the previous two functions.
|
||||
GridAttrMatcher CheckMulticell(const Multicell& multi)
|
||||
{
|
||||
TestableGrid* grid = GetTempGrid();
|
||||
|
||||
FitGridToMulticell(grid, multi);
|
||||
|
||||
if ( multi.rows > 1 || multi.cols > 1 )
|
||||
grid->SetMulticell(multi);
|
||||
|
||||
return GridAttrMatcher(*grid);
|
||||
}
|
||||
|
||||
TestableGrid* GetTempGrid()
|
||||
{
|
||||
if ( !m_tempGrid )
|
||||
{
|
||||
m_tempGrid = new TestableGrid(wxTheApp->GetTopWindow());
|
||||
m_tempGrid->Hide();
|
||||
}
|
||||
|
||||
return m_tempGrid;
|
||||
}
|
||||
|
||||
TestableGrid *m_grid;
|
||||
|
||||
// Temporary/scratch grid filled with expected content, used when
|
||||
// comparing against m_grid.
|
||||
TestableGrid *m_tempGrid;
|
||||
|
||||
wxDECLARE_NO_COPY_CLASS(GridTestCase);
|
||||
};
|
||||
|
||||
GridTestCase::GridTestCase()
|
||||
GridTestCase::GridTestCase() : m_tempGrid(NULL)
|
||||
{
|
||||
m_grid = new TestableGrid(wxTheApp->GetTopWindow());
|
||||
m_grid->CreateGrid(10, 2);
|
||||
@@ -140,6 +370,7 @@ GridTestCase::~GridTestCase()
|
||||
win->ReleaseMouse();
|
||||
|
||||
wxDELETE(m_grid);
|
||||
delete m_tempGrid;
|
||||
}
|
||||
|
||||
TEST_CASE_METHOD(GridTestCase, "Grid::CellEdit", "[grid]")
|
||||
@@ -1446,6 +1677,387 @@ TEST_CASE_METHOD(GridTestCase, "Grid::DrawInvalidCell", "[grid][multicell]")
|
||||
wxYield();
|
||||
}
|
||||
|
||||
#define CHECK_ATTR_COUNT(n) CHECK( m_grid->GetCellAttrCount() == n )
|
||||
|
||||
TEST_CASE_METHOD(GridTestCase, "Grid::CellAttribute", "[attr][cell][grid]")
|
||||
{
|
||||
SECTION("Overwrite")
|
||||
{
|
||||
CHECK_ATTR_COUNT( 0 );
|
||||
|
||||
m_grid->SetAttr(0, 0, NULL);
|
||||
CHECK_ATTR_COUNT( 0 );
|
||||
|
||||
SetCellAttr(0, 0);
|
||||
CHECK_ATTR_COUNT( 1 );
|
||||
|
||||
m_grid->SetAttr(0, 0, NULL);
|
||||
CHECK_ATTR_COUNT( 0 );
|
||||
|
||||
SetCellAttr(0, 0);
|
||||
m_grid->SetCellBackgroundColour(0, 1, *wxGREEN);
|
||||
CHECK_ATTR_COUNT( 2 );
|
||||
|
||||
m_grid->SetAttr(0, 1, NULL);
|
||||
m_grid->SetAttr(0, 0, NULL);
|
||||
CHECK_ATTR_COUNT( 0 );
|
||||
}
|
||||
|
||||
|
||||
// Fill the grid with attributes for next sections.
|
||||
|
||||
const int numRows = m_grid->GetNumberRows();
|
||||
const int numCols = m_grid->GetNumberCols();
|
||||
|
||||
for ( int row = 0; row < numRows; ++row )
|
||||
{
|
||||
for ( int col = 0; col < numCols; ++col )
|
||||
SetCellAttr(row, col);
|
||||
}
|
||||
|
||||
size_t numAttrs = static_cast<size_t>(numRows * numCols);
|
||||
|
||||
CHECK_ATTR_COUNT( numAttrs );
|
||||
|
||||
SECTION("Expanding")
|
||||
{
|
||||
CHECK( !HasCellAttr(numRows, numCols) );
|
||||
|
||||
m_grid->InsertCols();
|
||||
CHECK_ATTR_COUNT( numAttrs );
|
||||
CHECK( !HasCellAttr(0, 0) );
|
||||
CHECK( HasCellAttr(0, numCols) );
|
||||
|
||||
m_grid->InsertRows();
|
||||
CHECK_ATTR_COUNT( numAttrs );
|
||||
CHECK( HasCellAttr(numRows, numCols) );
|
||||
}
|
||||
|
||||
SECTION("Shrinking")
|
||||
{
|
||||
CHECK( HasCellAttr(numRows - 1 , numCols - 1) );
|
||||
CHECK( HasCellAttr(0, numCols - 1) );
|
||||
|
||||
m_grid->DeleteCols();
|
||||
numAttrs -= m_grid->GetNumberRows();
|
||||
CHECK_ATTR_COUNT( numAttrs );
|
||||
CHECK( HasCellAttr(0, 0) );
|
||||
CHECK( !HasCellAttr(0, numCols - 1) );
|
||||
|
||||
m_grid->DeleteRows();
|
||||
numAttrs -= m_grid->GetNumberCols();
|
||||
CHECK_ATTR_COUNT( numAttrs );
|
||||
CHECK( !HasCellAttr(numRows - 1 , numCols - 1) );
|
||||
}
|
||||
}
|
||||
|
||||
#define CHECK_MULTICELL() CHECK_THAT( *m_grid, HasMulticellOnly(multi) )
|
||||
|
||||
#define CHECK_NO_MULTICELL() CHECK_THAT( *m_grid, HasEmptyGrid() )
|
||||
|
||||
#define WHEN_N(s, n) WHEN(wxString::Format(s, n).ToStdString())
|
||||
|
||||
TEST_CASE_METHOD(GridTestCase,
|
||||
"Grid::InsertionsWithMulticell",
|
||||
"[attr][cell][grid][insert][multicell]")
|
||||
{
|
||||
int insertions = 0, offset = 0;
|
||||
|
||||
Multicell multi(1, 1, 3, 5);
|
||||
|
||||
SECTION("Sanity checks")
|
||||
{
|
||||
FitGridToMulticell(m_grid, multi);
|
||||
m_grid->SetMulticell(multi);
|
||||
|
||||
REQUIRE( static_cast<int>(m_grid->GetCellAttrCount())
|
||||
== multi.rows * multi.cols );
|
||||
|
||||
int row, col, rows, cols;
|
||||
|
||||
// Check main cell.
|
||||
row = multi.row,
|
||||
col = multi.col;
|
||||
wxGrid::CellSpan span = m_grid->GetCellSize(row, col, &rows, &cols);
|
||||
|
||||
REQUIRE( span == wxGrid::CellSpan_Main );
|
||||
REQUIRE( rows == multi.rows );
|
||||
REQUIRE( cols == multi.cols );
|
||||
|
||||
// Check inside cell at opposite of main.
|
||||
row = multi.row + multi.rows - 1;
|
||||
col = multi.col + multi.cols - 1;
|
||||
span = m_grid->GetCellSize(row, col, &rows, &cols);
|
||||
|
||||
REQUIRE( span == wxGrid::CellSpan_Inside );
|
||||
REQUIRE( rows == multi.row - row );
|
||||
REQUIRE( cols == multi.col - col );
|
||||
}
|
||||
|
||||
// Do some basic testing with column insertions first and do more tests
|
||||
// with edge cases later on just with rows. It's not really needed to
|
||||
// repeat the same tests for both rows and columns as the code for
|
||||
// updating them works symmetrically.
|
||||
|
||||
GIVEN(Catch::toString(multi))
|
||||
{
|
||||
FitGridToMulticell(m_grid, multi);
|
||||
m_grid->SetMulticell(multi);
|
||||
|
||||
insertions = 2;
|
||||
|
||||
WHEN("inserting any columns in multicell, at main")
|
||||
{
|
||||
InsertCols(multi.col + 0, insertions);
|
||||
|
||||
THEN("the position changes but not the size")
|
||||
{
|
||||
multi.col += insertions;
|
||||
CHECK_MULTICELL();
|
||||
}
|
||||
}
|
||||
|
||||
WHEN("inserting any columns in multicell, just after main")
|
||||
{
|
||||
InsertCols(multi.col + 1, insertions);
|
||||
|
||||
THEN("the size changes but not the position")
|
||||
{
|
||||
multi.cols += insertions;
|
||||
CHECK_MULTICELL();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Do more extensive testing with rows.
|
||||
|
||||
wxSwap(multi.rows, multi.cols);
|
||||
|
||||
GIVEN(Catch::toString(multi))
|
||||
{
|
||||
FitGridToMulticell(m_grid, multi);
|
||||
m_grid->SetMulticell(multi);
|
||||
|
||||
const int insertionCounts[] = {1, 2, multi.rows};
|
||||
|
||||
for ( size_t i = 0; i < WXSIZEOF(insertionCounts); ++i )
|
||||
{
|
||||
insertions = insertionCounts[i];
|
||||
|
||||
WHEN_N("inserting %d row(s), just before main", insertions)
|
||||
{
|
||||
InsertRows(multi.row - 1, insertions);
|
||||
|
||||
THEN("the position changes but not the size")
|
||||
{
|
||||
multi.row += insertions;
|
||||
CHECK_MULTICELL();
|
||||
}
|
||||
}
|
||||
|
||||
WHEN_N("inserting %d row(s) in multicell, at main", insertions)
|
||||
{
|
||||
InsertRows(multi.row + 0, insertions);
|
||||
|
||||
THEN("the position changes but not the size")
|
||||
{
|
||||
multi.row += insertions;
|
||||
CHECK_MULTICELL();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
insertions = multi.rows / 2;
|
||||
|
||||
// Check insertions within multicell, at and near edges.
|
||||
const int insertionOffsets[] = {1, 2, multi.rows - 2, multi.rows - 1};
|
||||
|
||||
for ( size_t i = 0; i < WXSIZEOF(insertionOffsets); ++i )
|
||||
{
|
||||
offset = insertionOffsets[i];
|
||||
|
||||
WHEN_N("inserting rows in multicell, %d row(s) after main", offset)
|
||||
{
|
||||
InsertRows(multi.row + offset, insertions);
|
||||
|
||||
THEN("the size changes but not the position")
|
||||
{
|
||||
multi.rows += insertions;
|
||||
CHECK_MULTICELL();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check at least one case of inserting after multicell.
|
||||
WHEN("inserting rows, just after multicell")
|
||||
{
|
||||
insertions = 2;
|
||||
InsertRows(multi.row + multi.rows, insertions);
|
||||
|
||||
THEN("neither size nor position change")
|
||||
{
|
||||
CHECK_MULTICELL();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
TEST_CASE_METHOD(GridTestCase,
|
||||
"GridMulticell::DeletionsWithMulticell",
|
||||
"[cellattr][delete][grid][multicell]")
|
||||
{
|
||||
int deletions = 0, offset = 0;
|
||||
|
||||
// Same as with the previous (insertions) test case but instead of some
|
||||
// basic testing with columns first, this time use rows for that and do more
|
||||
// extensive testing with columns.
|
||||
|
||||
Multicell multi(1, 1, 5, 3);
|
||||
|
||||
GIVEN(Catch::toString(multi))
|
||||
{
|
||||
FitGridToMulticell(m_grid, multi);
|
||||
m_grid->SetMulticell(multi);
|
||||
|
||||
WHEN("deleting any rows, at main")
|
||||
{
|
||||
deletions = 1;
|
||||
DeleteRows(multi.row + 0, deletions);
|
||||
|
||||
THEN("the multicell is deleted")
|
||||
{
|
||||
CHECK_NO_MULTICELL();
|
||||
}
|
||||
}
|
||||
|
||||
WHEN("deleting more rows than length of multicell,"
|
||||
" at end of multicell")
|
||||
{
|
||||
deletions = multi.rows + 2;
|
||||
offset = multi.rows - 1;
|
||||
DeleteRows(multi.row + offset, deletions);
|
||||
|
||||
THEN("the size changes but not the position")
|
||||
{
|
||||
multi.rows = offset;
|
||||
CHECK_MULTICELL();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Do more extensive testing with columns.
|
||||
|
||||
wxSwap(multi.rows, multi.cols);
|
||||
|
||||
GIVEN(Catch::toString(multi))
|
||||
{
|
||||
FitGridToMulticell(m_grid, multi);
|
||||
m_grid->SetMulticell(multi);
|
||||
|
||||
WHEN("deleting one column, just before main")
|
||||
{
|
||||
DeleteCols(multi.col - 1);
|
||||
|
||||
THEN("the position changes but not the size")
|
||||
{
|
||||
multi.col--;
|
||||
CHECK_MULTICELL();
|
||||
}
|
||||
}
|
||||
|
||||
WHEN("deleting multiple columns, just before main")
|
||||
{
|
||||
deletions = 2; // Must be at least 2 to affect main.
|
||||
DeleteCols(multi.col - 1, wxMax(2, deletions));
|
||||
|
||||
THEN("the multicell is deleted")
|
||||
{
|
||||
CHECK_NO_MULTICELL();
|
||||
}
|
||||
}
|
||||
|
||||
WHEN("deleting any columns, at main")
|
||||
{
|
||||
deletions = 1;
|
||||
DeleteCols(multi.col + 0, deletions);
|
||||
|
||||
THEN("the multicell is deleted")
|
||||
{
|
||||
CHECK_NO_MULTICELL();
|
||||
}
|
||||
}
|
||||
|
||||
WHEN("deleting one column within multicell, after main")
|
||||
{
|
||||
offset = 1;
|
||||
offset = wxClip(offset, 1, multi.cols - 1);
|
||||
DeleteCols(multi.col + offset, 1);
|
||||
|
||||
THEN("the size changes but not the position")
|
||||
{
|
||||
multi.cols--;
|
||||
CHECK_MULTICELL();
|
||||
}
|
||||
}
|
||||
|
||||
deletions = 2;
|
||||
|
||||
// Check deletions within multicell, at and near edges.
|
||||
const int offsets[] = {1, 2, multi.cols - deletions};
|
||||
|
||||
for ( size_t i = 0; i < WXSIZEOF(offsets); ++i )
|
||||
{
|
||||
offset = offsets[i];
|
||||
|
||||
WHEN_N("deleting columns only within multicell,"
|
||||
" %d column(s) after main", offset)
|
||||
{
|
||||
DeleteCols(multi.col + offset, deletions);
|
||||
|
||||
THEN("the size changes but not the position")
|
||||
{
|
||||
multi.cols -= deletions;
|
||||
CHECK_MULTICELL();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Instead of stuffing the multicell's length logic in the above test,
|
||||
// separately check at least two cases of starting many deletions
|
||||
// within multicell.
|
||||
|
||||
WHEN("deleting more columns than length of multicell, just after main")
|
||||
{
|
||||
deletions = multi.cols + 2;
|
||||
offset = 1;
|
||||
DeleteCols(multi.col + offset, deletions);
|
||||
|
||||
THEN("the size changes but not the position")
|
||||
{
|
||||
multi.cols = offset;
|
||||
CHECK_MULTICELL();
|
||||
}
|
||||
}
|
||||
|
||||
WHEN("deleting more columns than length of multicell,"
|
||||
" at end of multicell")
|
||||
{
|
||||
deletions = multi.cols + 2;
|
||||
offset = multi.cols - 1;
|
||||
DeleteCols(multi.col + offset, deletions);
|
||||
|
||||
THEN("the size changes but not the position")
|
||||
{
|
||||
multi.cols = offset;
|
||||
CHECK_MULTICELL();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Test wxGridBlockCoords here because it'a a part of grid sources.
|
||||
|
||||
std::ostream& operator<<(std::ostream& os, const wxGridBlockCoords& block) {
|
||||
@@ -1632,4 +2244,219 @@ TEST_CASE("GridBlockCoords::SymDifference", "[grid]")
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// TestableGrid
|
||||
//
|
||||
|
||||
void TestableGrid::DoEdit(const EditInfo& edit)
|
||||
{
|
||||
m_edit = edit;
|
||||
m_beforeGridAnnotated = ToString();
|
||||
|
||||
switch ( edit.direction )
|
||||
{
|
||||
case wxGRID_COLUMN:
|
||||
if ( edit.count < 0 )
|
||||
DeleteCols(edit.pos, -edit.count);
|
||||
else
|
||||
InsertCols(edit.pos, edit.count);
|
||||
break;
|
||||
|
||||
case wxGRID_ROW:
|
||||
if ( edit.count < 0 )
|
||||
DeleteRows(edit.pos, -edit.count);
|
||||
else
|
||||
InsertRows(edit.pos, edit.count);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
wxString TestableGrid::ToString() const
|
||||
{
|
||||
const int numRows = GetNumberRows();
|
||||
const int numCols = GetNumberCols();
|
||||
|
||||
const int colMargin = GetRowLabelValue(numRows - 1).length();
|
||||
const wxString leftIndent = wxString(' ', colMargin + 1);
|
||||
|
||||
// String s contains the rendering of the grid, start with drawing
|
||||
// the header columns.
|
||||
wxString s = leftIndent;
|
||||
|
||||
const int base = 10;
|
||||
// Draw the multiples of 10.
|
||||
for ( int col = base; col <= numCols; col += base)
|
||||
{
|
||||
s += wxString(' ', base - 1);
|
||||
s += ('0' + (col / base));
|
||||
}
|
||||
|
||||
if ( numCols >= base )
|
||||
s += "\n" + leftIndent;
|
||||
|
||||
// Draw the single digits.
|
||||
for ( int col = 1; col <= numCols; ++col )
|
||||
s += '0' + (col % base);
|
||||
s += "\n";
|
||||
|
||||
// Draw horizontal divider.
|
||||
s += wxString(' ', colMargin) + '+' + wxString('-', numCols) + "\n";
|
||||
|
||||
const int absEditCount = abs(m_edit.count);
|
||||
wxString action;
|
||||
action.Printf(" %s: %d",
|
||||
m_edit.count < 0 ? "deletions" : "insertions",
|
||||
absEditCount);
|
||||
|
||||
// Will contain summary of grid (only multicells mentioned).
|
||||
wxString content;
|
||||
|
||||
// Draw grid content.
|
||||
for ( int row = 0; row < numRows; ++row )
|
||||
{
|
||||
const wxString label = GetRowLabelValue(row);
|
||||
s += wxString(' ', colMargin - label.length());
|
||||
s += label + '|';
|
||||
|
||||
for ( int col = 0; col < numCols; ++col )
|
||||
{
|
||||
char c = 'x';
|
||||
int rows, cols;
|
||||
switch ( GetCellSize(row, col, &rows, &cols) )
|
||||
{
|
||||
case wxGrid::CellSpan_None:
|
||||
c = HasAttr(row, col) ? '*' : '.';
|
||||
break;
|
||||
|
||||
case wxGrid::CellSpan_Main:
|
||||
c = 'M';
|
||||
content += Multicell(row, col, rows, cols).ToString() + "\n";
|
||||
break;
|
||||
|
||||
case wxGrid::CellSpan_Inside:
|
||||
// Check if the offset to main cell is correct.
|
||||
c = (GetCellSize(row + rows, col + cols, &rows, &cols)
|
||||
== wxGrid::CellSpan_Main) ? 'm' : '?';
|
||||
break;
|
||||
}
|
||||
|
||||
s += c;
|
||||
}
|
||||
|
||||
// If applicable draw annotated row edits.
|
||||
if ( m_edit.count && m_edit.direction == wxGRID_ROW
|
||||
&& row >= m_edit.pos && row < m_edit.pos + absEditCount)
|
||||
{
|
||||
s += (m_edit.count < 0 ? " ^" : " v");
|
||||
|
||||
if ( row == m_edit.pos )
|
||||
s += action;
|
||||
}
|
||||
|
||||
s += "\n";
|
||||
}
|
||||
|
||||
// Draw annotated footer if columns edited.
|
||||
if ( m_edit.count && m_edit.direction == wxGRID_COLUMN )
|
||||
{
|
||||
s += leftIndent;
|
||||
|
||||
for ( int col = 0; col < m_edit.pos + absEditCount; ++col )
|
||||
{
|
||||
if ( col < m_edit.pos )
|
||||
s += " ";
|
||||
else
|
||||
s += (m_edit.count < 0 ? "<" : ">");
|
||||
}
|
||||
|
||||
s += action + "\n";
|
||||
}
|
||||
|
||||
s += "\n";
|
||||
|
||||
if ( content.empty() )
|
||||
content = "Empty grid\n";
|
||||
|
||||
return content + s;
|
||||
}
|
||||
|
||||
//
|
||||
// GridAttrMatcher
|
||||
//
|
||||
|
||||
GridAttrMatcher::GridAttrMatcher(const TestableGrid& grid) : m_grid(&grid)
|
||||
{
|
||||
m_expectedGridDesc = m_grid->ToString();
|
||||
}
|
||||
|
||||
bool GridAttrMatcher::match(const TestableGrid& other) const
|
||||
{
|
||||
const int rows = wxMax(m_grid->GetNumberRows(), other.GetNumberRows());
|
||||
const int cols = wxMax(m_grid->GetNumberCols(), other.GetNumberCols());
|
||||
|
||||
for ( int row = 0; row < rows; ++row )
|
||||
{
|
||||
for ( int col = 0; col < cols; ++col )
|
||||
{
|
||||
const bool hasAttr = m_grid->HasAttr(row, col);
|
||||
if ( hasAttr != other.HasAttr(row, col) )
|
||||
{
|
||||
m_diffDesc.Printf("%s: attribute presence (%d, expected %d)",
|
||||
CellCoordsToString(row, col),
|
||||
!hasAttr, hasAttr);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
int thisRows, thisCols;
|
||||
const wxGrid::CellSpan thisSpan
|
||||
= m_grid->GetCellSize(row, col, &thisRows, &thisCols);
|
||||
|
||||
int otherRows, otherCols;
|
||||
(void) other.GetCellSize(row, col, &otherRows, &otherCols);
|
||||
|
||||
if ( thisRows != otherRows || thisCols != otherCols )
|
||||
{
|
||||
wxString mismatchKind;
|
||||
switch ( thisSpan )
|
||||
{
|
||||
case wxGrid::CellSpan_None:
|
||||
mismatchKind = "different cell size";
|
||||
break;
|
||||
|
||||
case wxGrid::CellSpan_Main:
|
||||
mismatchKind = "different multicell size";
|
||||
break;
|
||||
|
||||
case wxGrid::CellSpan_Inside:
|
||||
mismatchKind = "main offset mismatch";
|
||||
break;
|
||||
}
|
||||
|
||||
m_diffDesc.Printf( "%s: %s (%s, expected %s)",
|
||||
CellCoordsToString(row, col),
|
||||
mismatchKind,
|
||||
CellSizeToString(otherRows, otherCols),
|
||||
CellSizeToString(thisRows, thisCols)
|
||||
);
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string GridAttrMatcher::describe() const
|
||||
{
|
||||
std::string desc = (m_diffDesc.empty() ? "Matches" : "Doesn't match");
|
||||
desc += " expected content:\n" + m_expectedGridDesc.ToStdString();
|
||||
|
||||
if ( !m_diffDesc.empty() )
|
||||
desc += + "first difference at " + m_diffDesc.ToStdString();
|
||||
|
||||
return desc;
|
||||
}
|
||||
|
||||
#endif //wxUSE_GRID
|
||||
|
Reference in New Issue
Block a user