Files
wxWidgets/src/generic/gridsel.cpp
Vadim Zeitlin 51ea80b701 Allow selecting rows/columns in row-or-column selection mode
Don't blankly forbid selecting any blocks at all in this mode, this
didn't really make any sense.

Moreover, SelectBlock() not doing anything prevented wxGrid code
handling {Ctrl,Shift}-Space from doing anything in this mode and, worse,
broke the logic of DeselectBlock() which relied on using SelectBlock()
to select the remaining parts of the selection, so this commit fixes
using Ctrl-click for deselecting rows or columns in this selection mode,
which was previously completely broken.
2020-04-15 00:03:37 +02:00

848 lines
27 KiB
C++

///////////////////////////////////////////////////////////////////////////
// Name: src/generic/gridsel.cpp
// Purpose: wxGridSelection
// Author: Stefan Neis
// Modified by:
// Created: 20/02/1999
// Copyright: (c) Stefan Neis (Stefan.Neis@t-online.de)
// Licence: wxWindows licence
/////////////////////////////////////////////////////////////////////////////
// For compilers that support precompilation, includes "wx/wx.h".
#include "wx/wxprec.h"
#ifdef __BORLANDC__
#pragma hdrstop
#endif
// ============================================================================
// declarations
// ============================================================================
// ----------------------------------------------------------------------------
// headers
// ----------------------------------------------------------------------------
#if wxUSE_GRID
#include "wx/generic/gridsel.h"
#include "wx/dynarray.h"
namespace
{
// The helper function to compare wxIntSortedArray elements.
int CompareInts(int n1, int n2)
{
return n1 - n2;
}
}
WX_DEFINE_SORTED_ARRAY(int, wxIntSortedArray);
wxGridSelection::wxGridSelection( wxGrid * grid,
wxGrid::wxGridSelectionModes sel )
{
m_grid = grid;
m_selectionMode = sel;
}
bool wxGridSelection::IsSelection()
{
return !m_selection.empty();
}
bool wxGridSelection::IsInSelection( int row, int col ) const
{
// Check whether the given cell is contained in one of the selected blocks.
//
// Note that this algorithm is O(N) in number of selected blocks, not in
// number of cells in the grid, so it should be reasonably efficient even
// for very large grids, as the user shouldn't be able to select too many
// blocks. If we still run into problems with this, we should find a more
// efficient way of storing the selection, e.g. using k-d trees.
const size_t count = m_selection.size();
for ( size_t n = 0; n < count; n++ )
{
if ( m_selection[n].ContainsCell(wxGridCellCoords(row, col)) )
return true;
}
return false;
}
// Change the selection mode
void wxGridSelection::SetSelectionMode( wxGrid::wxGridSelectionModes selmode )
{
// if selection mode is unchanged return immediately
if (selmode == m_selectionMode)
return;
if ( m_selectionMode != wxGrid::wxGridSelectCells )
{
// if changing form row to column selection
// or vice versa, clear the selection.
if ( selmode != wxGrid::wxGridSelectCells )
ClearSelection();
m_selectionMode = selmode;
}
else
{
// Preserve only fully selected rows/columns when switching from cell
// selection mode and discard the selected blocks that are invalid in
// the new selection mode.
const int lastCol = m_grid->GetNumberCols() - 1;
const int lastRow = m_grid->GetNumberRows() - 1;
for ( size_t n = m_selection.size(); n > 0; )
{
n--;
const wxGridBlockCoords& block = m_selection[n];
const int topRow = block.GetTopRow();
const int leftCol = block.GetLeftCol();
const int bottomRow = block.GetBottomRow();
const int rightCol = block.GetRightCol();
bool valid = false;
switch ( selmode )
{
case wxGrid::wxGridSelectCells:
wxFAIL_MSG("unreachable");
break;
case wxGrid::wxGridSelectRows:
valid = leftCol == 0 && rightCol == lastCol;
break;
case wxGrid::wxGridSelectColumns:
valid = topRow == 0 && bottomRow == lastRow;
break;
case wxGrid::wxGridSelectRowsOrColumns:
valid = (leftCol == 0 && rightCol == lastCol) ||
(topRow == 0 && bottomRow == lastRow);
break;
}
if ( !valid )
{
if ( !m_grid->GetBatchCount() )
{
m_grid->RefreshBlock(block.GetTopLeft(), block.GetBottomRight());
}
m_selection.erase(m_selection.begin() + n);
}
}
m_selectionMode = selmode;
}
}
void wxGridSelection::SelectRow(int row, const wxKeyboardState& kbd)
{
if ( m_selectionMode == wxGrid::wxGridSelectColumns )
return;
Select(wxGridBlockCoords(row, 0, row, m_grid->GetNumberCols() - 1),
kbd, true);
}
void wxGridSelection::SelectCol(int col, const wxKeyboardState& kbd)
{
if ( m_selectionMode == wxGrid::wxGridSelectRows )
return;
Select(wxGridBlockCoords(0, col, m_grid->GetNumberRows() - 1, col),
kbd, true);
}
void wxGridSelection::SelectBlock( int topRow, int leftCol,
int bottomRow, int rightCol,
const wxKeyboardState& kbd,
bool sendEvent )
{
// Fix the coordinates of the block if needed.
int allowed = -1;
switch ( m_selectionMode )
{
case wxGrid::wxGridSelectCells:
// In this mode arbitrary blocks can be selected.
allowed = 1;
break;
case wxGrid::wxGridSelectRows:
leftCol = 0;
rightCol = m_grid->GetNumberCols() - 1;
allowed = 1;
break;
case wxGrid::wxGridSelectColumns:
topRow = 0;
bottomRow = m_grid->GetNumberRows() - 1;
allowed = 1;
break;
case wxGrid::wxGridSelectRowsOrColumns:
// Arbitrary block selection doesn't make sense for this mode, as
// we could only select the entire grid, which wouldn't be useful,
// but we do allow selecting blocks that are already composed of
// only rows or only columns.
if ( topRow == 0 && bottomRow == m_grid->GetNumberRows() - 1 )
allowed = 1;
else if ( leftCol == 0 && rightCol == m_grid->GetNumberCols() - 1 )
allowed = 1;
else
allowed = 0;
break;
}
wxASSERT_MSG(allowed != -1, "unknown selection mode?");
if ( !allowed )
return;
Select(wxGridBlockCoords(topRow, leftCol, bottomRow, rightCol).Canonicalize(),
kbd, sendEvent);
}
void
wxGridSelection::SelectAll()
{
// There is no need to refresh anything, as Select() will do it anyhow, and
// no need to generate any events, so do not call ClearSelection() here.
m_selection.clear();
const int numRows = m_grid->GetNumberRows();
const int numCols = m_grid->GetNumberCols();
if ( numRows && numCols )
{
Select(wxGridBlockCoords(0, 0, numRows - 1, numCols - 1),
wxKeyboardState(), true /* send event */);
}
}
void
wxGridSelection::DeselectBlock(const wxGridBlockCoords& block,
const wxKeyboardState& kbd,
bool sendEvent)
{
const wxGridBlockCoords canonicalizedBlock = block.Canonicalize();
size_t count, n;
// If the selected block intersects with the deselection block, split it
// in up to 4 new parts, that don't contain the block to be selected, like
// this (for rows):
// |---------------------------|
// | |
// | part 1 |
// | |
// |---------------------------|
// | part 3 | x | part 4 |
// |---------------------------|
// | |
// | part 2 |
// | |
// |---------------------------|
// And for columns:
// |---------------------------|
// | | | |
// | | part 3 | |
// | | | |
// | |---------| |
// | part 1 | x | part 2 |
// | |---------| |
// | | | |
// | | part 4 | |
// | | | |
// |---------------------------|
// (The x marks the newly deselected block).
// Note: in row/column selection mode, we only need part1 and part2
// Blocks to refresh.
wxVectorGridBlockCoords refreshBlocks;
// Add the deselected block.
refreshBlocks.push_back(canonicalizedBlock);
count = m_selection.size();
for ( n = 0; n < count; n++ )
{
const wxGridBlockCoords& selBlock = m_selection[n];
// Whether blocks intersect.
if ( !m_selection[n].Intersects(canonicalizedBlock) )
continue;
int splitOrientation = -1;
switch ( m_selectionMode )
{
case wxGrid::wxGridSelectRows:
splitOrientation = wxHORIZONTAL;
break;
case wxGrid::wxGridSelectColumns:
splitOrientation = wxVERTICAL;
break;
case wxGrid::wxGridSelectCells:
case wxGrid::wxGridSelectRowsOrColumns:
if ( selBlock.GetLeftCol() == 0 &&
selBlock.GetRightCol() == m_grid->GetNumberCols() - 1 )
splitOrientation = wxHORIZONTAL;
else
splitOrientation = wxVERTICAL;
break;
}
wxASSERT_MSG( splitOrientation != -1, "unknown selection mode" );
const wxGridBlockDiffResult result =
selBlock.Difference(canonicalizedBlock, splitOrientation);
// remove the block (note that selBlock, being a reference, is
// invalidated here and can't be used any more below)
m_selection.erase(m_selection.begin() + n);
n--;
count--;
for ( int i = 0; i < 2; ++i )
{
const wxGridBlockCoords& part = result.m_parts[i];
if ( part != wxGridNoBlockCoords )
SelectBlockNoEvent(part);
}
for ( int i = 2; i < 4; ++i )
{
const wxGridBlockCoords& part = result.m_parts[i];
if ( part != wxGridNoBlockCoords )
{
// Add part[2] and part[3] only in the cells selection mode.
if ( m_selectionMode == wxGrid::wxGridSelectCells )
SelectBlockNoEvent(part);
else
MergeOrAddBlock(refreshBlocks, part);
}
}
}
// Refresh the screen and send events.
count = refreshBlocks.size();
for ( n = 0; n < count; n++ )
{
const wxGridBlockCoords& refBlock = refreshBlocks[n];
if ( !m_grid->GetBatchCount() )
{
m_grid->RefreshBlock(refBlock.GetTopRow(), refBlock.GetLeftCol(),
refBlock.GetBottomRow(), refBlock.GetRightCol());
}
if ( sendEvent )
{
wxGridRangeSelectEvent gridEvt(m_grid->GetId(),
wxEVT_GRID_RANGE_SELECT,
m_grid,
refBlock.GetTopLeft(),
refBlock.GetBottomRight(),
false,
kbd);
m_grid->GetEventHandler()->ProcessEvent(gridEvt);
}
}
}
void wxGridSelection::ClearSelection()
{
size_t n;
wxRect r;
wxGridCellCoords coords1, coords2;
// deselect all blocks and update the screen
while ( ( n = m_selection.size() ) > 0)
{
n--;
const wxGridBlockCoords& block = m_selection[n];
coords1 = block.GetTopLeft();
coords2 = block.GetBottomRight();
m_selection.erase(m_selection.begin() + n);
if ( !m_grid->GetBatchCount() )
{
m_grid->RefreshBlock(coords1, coords2);
#ifdef __WXMAC__
m_grid->UpdateGridWindows();
#endif
}
}
// One deselection event, indicating deselection of _all_ cells.
// (No finer grained events for each of the smaller regions
// deselected above!)
wxGridRangeSelectEvent gridEvt( m_grid->GetId(),
wxEVT_GRID_RANGE_SELECT,
m_grid,
wxGridCellCoords( 0, 0 ),
wxGridCellCoords(
m_grid->GetNumberRows() - 1,
m_grid->GetNumberCols() - 1 ),
false );
m_grid->GetEventHandler()->ProcessEvent(gridEvt);
}
void wxGridSelection::UpdateRows( size_t pos, int numRows )
{
size_t count = m_selection.size();
size_t n;
for ( n = 0; n < count; n++ )
{
wxGridBlockCoords& block = m_selection[n];
wxCoord row1 = block.GetTopRow();
wxCoord row2 = block.GetBottomRow();
if ((size_t)row2 >= pos)
{
if (numRows > 0)
{
// If rows inserted, increase row counter where necessary
block.SetBottomRow( row2 + numRows );
if ((size_t)row1 >= pos)
block.SetTopRow( row1 + numRows );
}
else if (numRows < 0)
{
// If rows deleted ...
if ((size_t)row2 >= pos - numRows)
{
// ...either decrement row counter (if row still exists)...
block.SetBottomRow( row2 + numRows );
if ((size_t)row1 >= pos)
block.SetTopRow( wxMax(row1 + numRows, (int)pos) );
}
else
{
if ((size_t)row1 >= pos)
{
// ...or remove the attribute
m_selection.erase(m_selection.begin() + n);
n--;
count--;
}
else
block.SetBottomRow( pos );
}
}
}
}
}
void wxGridSelection::UpdateCols( size_t pos, int numCols )
{
size_t count = m_selection.size();
size_t n;
for ( n = 0; n < count; n++ )
{
wxGridBlockCoords& block = m_selection[n];
wxCoord col1 = block.GetLeftCol();
wxCoord col2 = block.GetRightCol();
if ((size_t)col2 >= pos)
{
if (numCols > 0)
{
// If rows inserted, increase row counter where necessary
block.SetRightCol(col2 + numCols);
if ((size_t)col1 >= pos)
block.SetLeftCol(col1 + numCols);
}
else if (numCols < 0)
{
// If cols deleted ...
if ((size_t)col2 >= pos - numCols)
{
// ...either decrement col counter (if col still exists)...
block.SetRightCol(col2 + numCols);
if ( (size_t) col1 >= pos)
block.SetLeftCol( wxMax(col1 + numCols, (int)pos) );
}
else
{
if ((size_t)col1 >= pos)
{
// ...or remove the attribute
m_selection.erase(m_selection.begin() + n);
n--;
count--;
}
else
block.SetRightCol(pos);
}
}
}
}
}
bool wxGridSelection::ExtendCurrentBlock(const wxGridCellCoords& blockStart,
const wxGridCellCoords& blockEnd,
const wxKeyboardState& kbd)
{
wxASSERT( blockStart.GetRow() != -1 && blockStart.GetCol() != -1 &&
blockEnd.GetRow() != -1 && blockEnd.GetCol() != -1 );
// If selection doesn't contain the current cell (which also covers the
// special case of nothing being selected yet), we have to create a new
// block containing it because it doesn't make sense to extend any existing
// block to non-selected current cell.
if ( !IsInSelection(m_grid->GetGridCursorCoords()) )
{
SelectBlock(blockStart, blockEnd);
return true;
}
const wxGridBlockCoords& block = *m_selection.rbegin();
wxGridBlockCoords newBlock = block;
// Determine if we should try to extend the current block rows and/or
// columns at all.
bool canChangeRow = false,
canChangeCol = false;
switch ( m_selectionMode )
{
case wxGrid::wxGridSelectCells:
// Nothing prevents us from doing it in this case.
canChangeRow =
canChangeCol = true;
break;
case wxGrid::wxGridSelectColumns:
// Rows are always fixed, so prevent us from ever selecting only
// part of a column in this case by leaving canChangeRow false.
canChangeCol = true;
break;
case wxGrid::wxGridSelectRows:
// Same as above but mirrored.
canChangeRow = true;
break;
case wxGrid::wxGridSelectRowsOrColumns:
// In this case we may only change component which is not fixed.
if ( block.GetTopRow() != 0 ||
block.GetBottomRow() != m_grid->GetNumberRows() - 1 )
{
// This is a row block, so we can extend it in row direction.
canChangeRow = true;
}
else if ( block.GetLeftCol() != 0 ||
block.GetRightCol() != m_grid->GetNumberCols() - 1 )
{
canChangeCol = true;
}
else // The entire grid is selected.
{
// In this case we can shrink it in either direction.
canChangeRow =
canChangeCol = true;
}
break;
}
if ( canChangeRow )
{
// If the new block starts at the same top row as the current one, the
// end block coordinates must correspond to the new bottom row -- and
// vice versa, if the new block starts at the bottom, its other end
// must correspond to the top.
if ( blockStart.GetRow() == block.GetTopRow() )
{
newBlock.SetBottomRow(blockEnd.GetRow());
}
else if ( blockStart.GetRow() == block.GetBottomRow() )
{
newBlock.SetTopRow(blockEnd.GetRow());
}
else // current and new block don't have common row boundary
{
// This can happen when mixing entire column and cell selection, e.g.
// by Shift-clicking on the column header. In this case, the right
// thing to do is to just expand the current block to the new one
// boundaries, extending the selection to the entire column height when
// a column is selected. However notice that we should not shrink the
// current block here, in order to allow Shift-Left/Right (which don't
// know anything about the column selection and so just use single row
// blocks) to keep the full column selection.
int top = blockStart.GetRow(),
bottom = blockEnd.GetRow();
if ( top > bottom )
wxSwap(top, bottom);
if ( top < newBlock.GetTopRow() )
newBlock.SetTopRow(top);
if ( bottom > newBlock.GetBottomRow() )
newBlock.SetBottomRow(bottom);
}
}
// Same as above but mirrored for columns.
if ( canChangeCol )
{
if ( blockStart.GetCol() == block.GetLeftCol() )
{
newBlock.SetRightCol(blockEnd.GetCol());
}
else if ( blockStart.GetCol() == block.GetRightCol() )
{
newBlock.SetLeftCol(blockEnd.GetCol());
}
else
{
int left = blockStart.GetCol(),
right = blockEnd.GetCol();
if ( left > right )
wxSwap(left, right);
if ( left < newBlock.GetLeftCol() )
newBlock.SetLeftCol(left);
if ( right > newBlock.GetRightCol() )
newBlock.SetRightCol(right);
}
}
newBlock = newBlock.Canonicalize();
if ( newBlock == block )
return false;
// Update View.
if ( !m_grid->GetBatchCount() )
{
wxGridBlockDiffResult refreshBlocks = block.SymDifference(newBlock);
for ( int i = 0; i < 4; ++i )
{
const wxGridBlockCoords& refreshBlock = refreshBlocks.m_parts[i];
m_grid->RefreshBlock(refreshBlock.GetTopLeft(),
refreshBlock.GetBottomRight());
}
}
// Update the current block in place.
*m_selection.rbegin() = newBlock;
// Send Event.
wxGridRangeSelectEvent gridEvt(m_grid->GetId(),
wxEVT_GRID_RANGE_SELECT,
m_grid,
newBlock.GetTopLeft(),
newBlock.GetBottomRight(),
true,
kbd);
m_grid->GetEventHandler()->ProcessEvent(gridEvt);
return true;
}
wxGridCellCoords wxGridSelection::GetExtensionAnchor() const
{
wxGridCellCoords coords = m_grid->m_currentCellCoords;
// If the current cell isn't selected (which also covers the special case
// of nothing being selected yet), we have to use it as anchor as we need
// to ensure that it will get selected.
if ( !IsInSelection(coords) )
return coords;
const wxGridBlockCoords& block = *m_selection.rbegin();
if ( block.GetTopRow() == coords.GetRow() )
coords.SetRow(block.GetBottomRow());
else if ( block.GetBottomRow() == coords.GetRow() )
coords.SetRow(block.GetTopRow());
if ( block.GetLeftCol() == coords.GetCol() )
coords.SetCol(block.GetRightCol());
else if ( block.GetRightCol() == coords.GetCol() )
coords.SetCol(block.GetLeftCol());
return coords;
}
wxGridCellCoordsArray wxGridSelection::GetCellSelection() const
{
if ( m_selectionMode != wxGrid::wxGridSelectCells )
return wxGridCellCoordsArray();
wxGridCellCoordsArray cells;
const size_t count = m_selection.size();
cells.reserve(count);
for ( size_t n = 0; n < count; n++ )
{
const wxGridBlockCoords& block = m_selection[n];
if ( block.GetTopRow() == block.GetBottomRow() &&
block.GetLeftCol() == block.GetRightCol() )
{
cells.push_back(block.GetTopLeft());
}
}
return cells;
}
wxGridCellCoordsArray wxGridSelection::GetBlockSelectionTopLeft() const
{
// return blocks only in wxGridSelectCells selection mode
if ( m_selectionMode != wxGrid::wxGridSelectCells )
return wxGridCellCoordsArray();
wxGridCellCoordsArray coords;
const size_t count = m_selection.size();
coords.reserve(count);
for ( size_t n = 0; n < count; n++ )
{
coords.push_back(m_selection[n].GetTopLeft());
}
return coords;
}
wxGridCellCoordsArray wxGridSelection::GetBlockSelectionBottomRight() const
{
if ( m_selectionMode != wxGrid::wxGridSelectCells )
return wxGridCellCoordsArray();
wxGridCellCoordsArray coords;
const size_t count = m_selection.size();
coords.reserve(count);
for ( size_t n = 0; n < count; n++ )
{
coords.push_back(m_selection[n].GetBottomRight());
}
return coords;
}
// For compatibility with the existing code, try reconstructing the selected
// rows/columns from the selected blocks we store internally. Of course, this
// only works well in the corresponding selection mode in which the user can
// only select the entire lines in the first place, as otherwise it's difficult to
// efficiently determine that a line is selected because all of its cells
// were selected one by one. But this should work well enough in practice and
// is, anyhow, the best we can do.
wxArrayInt wxGridSelection::GetRowSelection() const
{
if ( m_selectionMode == wxGrid::wxGridSelectColumns )
return wxArrayInt();
wxIntSortedArray uniqueRows(&CompareInts);
const size_t count = m_selection.size();
for ( size_t n = 0; n < count; ++n )
{
const wxGridBlockCoords& block = m_selection[n];
if ( block.GetLeftCol() == 0 &&
block.GetRightCol() == m_grid->GetNumberCols() - 1 )
{
for ( int r = block.GetTopRow(); r <= block.GetBottomRow(); ++r )
{
uniqueRows.Add(r);
}
}
}
wxArrayInt result;
result.reserve(uniqueRows.size());
for( size_t i = 0; i < uniqueRows.size(); ++i )
{
result.push_back(uniqueRows[i]);
}
return result;
}
// See comments for GetRowSelection().
wxArrayInt wxGridSelection::GetColSelection() const
{
if ( m_selectionMode == wxGrid::wxGridSelectRows )
return wxArrayInt();
wxIntSortedArray uniqueRows(&CompareInts);
const size_t count = m_selection.size();
for ( size_t n = 0; n < count; ++n )
{
const wxGridBlockCoords& block = m_selection[n];
if ( block.GetTopRow() == 0 &&
block.GetBottomRow() == m_grid->GetNumberRows() - 1 )
{
for ( int c = block.GetLeftCol(); c <= block.GetRightCol(); ++c )
{
uniqueRows.Add(c);
}
}
}
wxArrayInt result;
result.reserve(uniqueRows.size());
for( size_t i = 0; i < uniqueRows.size(); ++i )
{
result.push_back(uniqueRows[i]);
}
return result;
}
void
wxGridSelection::Select(const wxGridBlockCoords& block,
const wxKeyboardState& kbd, bool sendEvent)
{
if (m_grid->GetNumberRows() == 0 || m_grid->GetNumberCols() == 0)
return;
m_selection.push_back(block);
// Update View:
if ( !m_grid->GetBatchCount() )
{
m_grid->RefreshBlock(block.GetTopLeft(), block.GetBottomRight());
}
// Send Event, if not disabled.
if ( sendEvent )
{
wxGridRangeSelectEvent gridEvt( m_grid->GetId(),
wxEVT_GRID_RANGE_SELECT,
m_grid,
block.GetTopLeft(),
block.GetBottomRight(),
true,
kbd);
m_grid->GetEventHandler()->ProcessEvent( gridEvt );
}
}
void wxGridSelection::MergeOrAddBlock(wxVectorGridBlockCoords& blocks,
const wxGridBlockCoords& newBlock)
{
size_t count = blocks.size();
for ( size_t n = 0; n < count; n++ )
{
const wxGridBlockCoords& block = blocks[n];
if ( block.ContainsBlock(newBlock) )
return;
if ( newBlock.ContainsBlock(block) )
{
blocks.erase(blocks.begin() + n);
n--;
count--;
}
}
blocks.push_back(newBlock);
}
#endif