fltk/src/Fl_Table.cxx
Albrecht Schlosser 1182cd66ec Make Fl_Table::get_selection() 'const' (#1305)
See also branch-1.4 with FLTK_ABI_VERSION >= 10405:
  commit 9b9426bf6e
2025-09-05 17:38:05 +02:00

1410 lines
44 KiB
C++

//
// Fl_Table -- A table widget for the Fast Light Tool Kit (FLTK).
//
// Copyright 2002 by Greg Ercolano.
// Copyright (c) 2004 O'ksi'D
// Copyright 2009-2025 by Bill Spitzak and others.
//
// This library is free software. Distribution and use rights are outlined in
// the file "COPYING" which should have been included with this file. If this
// file is missing or damaged, see the license at:
//
// https://www.fltk.org/COPYING.php
//
// Please see the following page on how to report bugs and issues:
//
// https://www.fltk.org/bugs.php
//
#include <FL/Fl_Table.H>
#include <FL/Fl.H>
#include <FL/fl_draw.H>
#include <sys/types.h>
#include <string.h> // memcpy
#include <stdio.h> // fprintf
#include <stdlib.h> // realloc/free
/** Sets the vertical scroll position so 'row' is at the top,
and causes the screen to redraw.
*/
void Fl_Table::row_position(int row) {
if ( _row_position == row ) return; // OPTIMIZATION: no change? avoid redraw
if ( row < 0 ) row = 0;
else if ( row >= rows() ) row = rows() - 1;
if ( table_h <= tih ) return; // don't scroll if table smaller than window
double newtop = row_scroll_position(row);
if ( newtop > vscrollbar->maximum() ) {
newtop = vscrollbar->maximum();
}
vscrollbar->Fl_Slider::value(newtop);
table_scrolled();
redraw();
_row_position = row; // HACK: override what table_scrolled() came up with
}
/**
Sets the horizontal scroll position so 'col' is at the left,
and causes the screen to redraw.
*/
void Fl_Table::col_position(int col) {
if ( _col_position == col ) return; // OPTIMIZATION: no change? avoid redraw
if ( col < 0 ) col = 0;
else if ( col >= cols() ) col = cols() - 1;
if ( table_w <= tiw ) return; // don't scroll if table smaller than window
double newleft = col_scroll_position(col);
if ( newleft > hscrollbar->maximum() ) {
newleft = hscrollbar->maximum();
}
hscrollbar->Fl_Slider::value(newleft);
table_scrolled();
redraw();
_col_position = col; // HACK: override what table_scrolled() came up with
}
/**
Returns the scroll position (in pixels) of the specified 'row'.
*/
long Fl_Table::row_scroll_position(int row) {
int startrow = 0;
long scroll = 0;
// OPTIMIZATION:
// Attempt to use precomputed row scroll position
//
if ( toprow_scrollpos != -1 && row >= toprow ) {
scroll = toprow_scrollpos;
startrow = toprow;
}
for ( int t=startrow; t<row; t++ ) {
scroll += row_height(t);
}
return(scroll);
}
/**
Returns the scroll position (in pixels) of the specified column 'col'.
*/
long Fl_Table::col_scroll_position(int col) {
int startcol = 0;
long scroll = 0;
// OPTIMIZATION:
// Attempt to use precomputed row scroll position
//
if ( leftcol_scrollpos != -1 && col >= leftcol ) {
scroll = leftcol_scrollpos;
startcol = leftcol;
}
for ( int t=startcol; t<col; t++ ) {
scroll += col_width(t);
}
return(scroll);
}
/**
The constructor for Fl_Table.
This creates an empty table with no rows or columns,
with headers and row/column resize behavior disabled.
*/
Fl_Table::Fl_Table(int X, int Y, int W, int H, const char *l) : Fl_Group(X,Y,W,H,l) {
_rows = 0;
_cols = 0;
_row_header_w = 40;
_col_header_h = 18;
_row_header = 0;
_col_header = 0;
_row_header_color = color();
_col_header_color = color();
_row_resize = 0;
_col_resize = 0;
_row_resize_min = 1;
_col_resize_min = 1;
_redraw_toprow = -1;
_redraw_botrow = -1;
_redraw_leftcol = -1;
_redraw_rightcol = -1;
table_w = 0;
table_h = 0;
toprow = 0;
botrow = 0;
leftcol = 0;
rightcol = 0;
toprow_scrollpos = -1;
leftcol_scrollpos = -1;
_last_cursor = FL_CURSOR_DEFAULT;
_resizing_col = -1;
_resizing_row = -1;
_dragging_x = -1;
_dragging_y = -1;
_last_row = -1;
_auto_drag = 0;
current_col = -1;
current_row = -1;
select_row = -1;
select_col = -1;
_scrollbar_size = 0;
flags_ = 0; // TABCELLNAV off
_colwidths = new std::vector<int>; // column widths in pixels
_rowheights = new std::vector<int>; // row heights in pixels
box(FL_THIN_DOWN_FRAME);
vscrollbar = new Fl_Scrollbar(x()+w()-Fl::scrollbar_size(), y(),
Fl::scrollbar_size(), h()-Fl::scrollbar_size());
vscrollbar->type(FL_VERTICAL);
vscrollbar->callback(scroll_cb, (void*)this);
hscrollbar = new Fl_Scrollbar(x(), y()+h()-Fl::scrollbar_size(),
w(), Fl::scrollbar_size());
hscrollbar->type(FL_HORIZONTAL);
hscrollbar->callback(scroll_cb, (void*)this);
table = new Fl_Scroll(x(), y(), w(), h());
table->box(FL_NO_BOX);
table->type(0); // don't show Fl_Scroll's scrollbars -- use our own
table->hide(); // hide unless children are present
table->end();
table_resized();
redraw();
Fl_Group::end(); // end the group's begin()
table->begin(); // leave with fltk children getting added to the scroll
}
/**
The destructor for Fl_Table.
Destroys the table and its associated widgets.
*/
Fl_Table::~Fl_Table() {
// The parent Fl_Group takes care of destroying scrollbars
delete _colwidths;
delete _rowheights;
}
/**
Returns the current number of columns.
This is equivalent to the size of the column widths vector.
\returns Number of columns.
*/
int Fl_Table::col_size() {
return int(_colwidths->size());
}
/**
Returns the current number of rows.
This is equivalent to the size of the row heights vector.
\returns Number of rows.
*/
int Fl_Table::row_size() {
return int(_rowheights->size());
}
/**
Sets the height of the specified row in pixels,
and the table is redrawn.
callback() will be invoked with CONTEXT_RC_RESIZE
if the row's height was actually changed, and when() is FL_WHEN_CHANGED.
*/
void Fl_Table::row_height(int row, int height) {
if ( row < 0 ) return;
if ( row < row_size() && (*_rowheights)[row] == height ) {
return; // OPTIMIZATION: no change? avoid redraw
}
// Add row heights, even if none yet
int now_size = row_size();
if (row >= now_size) {
_rowheights->resize(row, height);
}
(*_rowheights)[row] = height;
table_resized();
if ( row <= botrow ) { // OPTIMIZATION: only redraw if onscreen or above screen
redraw();
}
// ROW RESIZE CALLBACK
if ( Fl_Widget::callback() && when() & FL_WHEN_CHANGED ) {
do_callback(CONTEXT_RC_RESIZE, row, 0);
}
}
/**
Sets the width of the specified column in pixels, and the table is redrawn.
callback() will be invoked with CONTEXT_RC_RESIZE
if the column's width was actually changed, and when() is FL_WHEN_CHANGED.
*/
void Fl_Table::col_width(int col, int width)
{
if ( col < 0 ) return;
if ( col < col_size() && (*_colwidths)[col] == width ) {
return; // OPTIMIZATION: no change? avoid redraw
}
// Add column widths, even if none yet
int now_size = col_size();
if ( col >= now_size ) {
_colwidths->resize(col+1, width);
}
(*_colwidths)[col] = width;
table_resized();
if ( col <= rightcol ) { // OPTIMIZATION: only redraw if onscreen or to the left
redraw();
}
// COLUMN RESIZE CALLBACK
if ( Fl_Widget::callback() && when() & FL_WHEN_CHANGED ) {
do_callback(CONTEXT_RC_RESIZE, 0, col);
}
}
/**
Return specified row/col values R and C to within the table's
current row/col limits.
\returns 0 if no changes were made, or 1 if they were.
*/
int Fl_Table::row_col_clamp(TableContext context, int &R, int &C) {
int clamped = 0;
if ( R < 0 ) { R = 0; clamped = 1; }
if ( C < 0 ) { C = 0; clamped = 1; }
switch ( context ) {
case CONTEXT_COL_HEADER:
// Allow col headers to draw even if no rows
if ( R >= _rows && R != 0 ) { R = _rows - 1; clamped = 1; }
break;
case CONTEXT_ROW_HEADER:
// Allow row headers to draw even if no columns
if ( C >= _cols && C != 0 ) { C = _cols - 1; clamped = 1; }
break;
case CONTEXT_CELL:
default:
// CLAMP R/C TO _rows/_cols
if ( R >= _rows ) { R = _rows - 1; clamped = 1; }
if ( C >= _cols ) { C = _cols - 1; clamped = 1; }
break;
}
return(clamped);
}
/**
Returns the (X,Y,W,H) bounding region for the specified 'context'.
*/
void Fl_Table::get_bounds(TableContext context, int &X, int &Y, int &W, int &H) {
switch ( context ) {
case CONTEXT_COL_HEADER:
// Column header clipping.
X = tox;
Y = wiy;
W = tow;
H = col_header_height();
return;
case CONTEXT_ROW_HEADER:
// Row header clipping.
X = wix;
Y = toy;
W = row_header_width();
H = toh;
return;
case CONTEXT_TABLE:
// Table inner dimensions
X = tix; Y = tiy; W = tiw; H = tih;
return;
// TODO: Add other contexts..
default:
fprintf(stderr, "Fl_Table::get_bounds(): context %d unimplemented\n", (int)context);
return;
}
//NOTREACHED
}
/**
Find row/col for the recent mouse event.
Returns the context, and the row/column values in R/C.
Also returns 'resizeflag' if mouse is hovered over a resize boundary.
*/
Fl_Table::TableContext Fl_Table::cursor2rowcol(int &R, int &C, ResizeFlag &resizeflag) {
// return values
R = C = 0;
resizeflag = RESIZE_NONE;
// Row header?
int X, Y, W, H;
if ( row_header() ) {
// Inside a row heading?
get_bounds(CONTEXT_ROW_HEADER, X, Y, W, H);
if ( Fl::event_inside(X, Y, W, H) ) {
// Scan visible rows until found
for ( R = toprow; R <= botrow; R++ ) {
find_cell(CONTEXT_ROW_HEADER, R, 0, X, Y, W, H);
if ( Fl::event_y() >= Y && Fl::event_y() < (Y+H) ) {
// Found row?
// If cursor over resize boundary, and resize enabled,
// enable the appropriate resize flag.
//
if ( row_resize() ) {
if ( Fl::event_y() <= (Y+3-0) ) { resizeflag = RESIZE_ROW_ABOVE; }
if ( Fl::event_y() >= (Y+H-3) ) { resizeflag = RESIZE_ROW_BELOW; }
}
return(CONTEXT_ROW_HEADER);
}
}
// Must be in row header dead zone
return(CONTEXT_NONE);
}
}
// Column header?
if ( col_header() ) {
// Inside a column heading?
get_bounds(CONTEXT_COL_HEADER, X, Y, W, H);
if ( Fl::event_inside(X, Y, W, H) ) {
// Scan visible columns until found
for ( C = leftcol; C <= rightcol; C++ ) {
find_cell(CONTEXT_COL_HEADER, 0, C, X, Y, W, H);
if ( Fl::event_x() >= X && Fl::event_x() < (X+W) ) {
// Found column?
// If cursor over resize boundary, and resize enabled,
// enable the appropriate resize flag.
//
if ( col_resize() ) {
if ( Fl::event_x() <= (X+3-0) ) { resizeflag = RESIZE_COL_LEFT; }
if ( Fl::event_x() >= (X+W-3) ) { resizeflag = RESIZE_COL_RIGHT; }
}
return(CONTEXT_COL_HEADER);
}
}
// Must be in column header dead zone
return(CONTEXT_NONE);
}
}
// Mouse somewhere in table?
// Scan visible r/c's until we find it.
//
if ( Fl::event_inside(tox, toy, tow, toh) ) {
for ( R = toprow; R <= botrow; R++ ) {
find_cell(CONTEXT_CELL, R, C, X, Y, W, H);
if ( Fl::event_y() < Y ) break; // OPT: thanks lars
if ( Fl::event_y() >= (Y+H) ) continue; // OPT: " "
for ( C = leftcol; C <= rightcol; C++ ) {
find_cell(CONTEXT_CELL, R, C, X, Y, W, H);
if ( Fl::event_inside(X, Y, W, H) ) {
return(CONTEXT_CELL); // found it
}
}
}
// Must be in a dead zone of the table
R = C = 0;
return(CONTEXT_TABLE);
}
// Somewhere else
return(CONTEXT_NONE);
}
/**
Find a cell's X/Y/W/H region for the specified cell in row 'R', column 'C'.
\returns
- 0 -- on success, XYWH returns the region of the specified cell.
- -1 -- if R or C are out of range, and X/Y/W/H will be set to zero.
*/
int Fl_Table::find_cell(TableContext context, int R, int C, int &X, int &Y, int &W, int &H) {
if ( row_col_clamp(context, R, C) ) { // row or col out of range? error
X=Y=W=H=0;
return(-1);
}
X = (int)col_scroll_position(C) - hscrollbar->value() + tix;
Y = (int)row_scroll_position(R) - vscrollbar->value() + tiy;
W = col_width(C);
H = row_height(R);
switch ( context ) {
case CONTEXT_COL_HEADER:
Y = wiy;
H = col_header_height();
return(0);
case CONTEXT_ROW_HEADER:
X = wix;
W = row_header_width();
return(0);
case CONTEXT_CELL:
return(0);
case CONTEXT_TABLE:
return(0);
// TODO -- HANDLE OTHER CONTEXTS
default:
fprintf(stderr, "Fl_Table::find_cell: unknown context %d\n", (int)context);
return(-1);
}
//NOTREACHED
}
// Enable automatic scroll-selection
void Fl_Table::_start_auto_drag() {
if (_auto_drag) return;
_auto_drag = 1;
Fl::add_timeout(0.3, _auto_drag_cb2, this);
}
// Disable automatic scroll-selection
void Fl_Table::_stop_auto_drag() {
if (!_auto_drag) return;
Fl::remove_timeout(_auto_drag_cb2, this);
_auto_drag = 0;
}
void Fl_Table::_auto_drag_cb2(void *d) {
((Fl_Table*)d)->_auto_drag_cb();
}
// Handle automatic scroll-selection if mouse selection dragged off table edge
void Fl_Table::_auto_drag_cb() {
int lx = Fl::e_x;
int ly = Fl::e_y;
if (_selecting == CONTEXT_COL_HEADER)
{ ly = y() + col_header_height(); }
else if (_selecting == CONTEXT_ROW_HEADER)
{ lx = x() + row_header_width(); }
if (lx > x() + w() - 20) {
Fl::e_x = x() + w() - 20;
if (hscrollbar->visible())
((Fl_Slider*)hscrollbar)->value(hscrollbar->clamp(hscrollbar->value() + 30));
hscrollbar->do_callback();
_dragging_x = Fl::e_x - 30;
}
else if (lx < (x() + row_header_width())) {
Fl::e_x = x() + row_header_width() + 1;
if (hscrollbar->visible()) {
((Fl_Slider*)hscrollbar)->value(hscrollbar->clamp(hscrollbar->value() - 30));
}
hscrollbar->do_callback();
_dragging_x = Fl::e_x + 30;
}
if (ly > y() + h() - 20) {
Fl::e_y = y() + h() - 20;
if (vscrollbar->visible()) {
((Fl_Slider*)vscrollbar)->value(vscrollbar->clamp(vscrollbar->value() + 30));
}
vscrollbar->do_callback();
_dragging_y = Fl::e_y - 30;
}
else if (ly < (y() + col_header_height())) {
Fl::e_y = y() + col_header_height() + 1;
if (vscrollbar->visible()) {
((Fl_Slider*)vscrollbar)->value(vscrollbar->clamp(vscrollbar->value() - 30));
}
vscrollbar->do_callback();
_dragging_y = Fl::e_y + 30;
}
_auto_drag = 2;
handle(FL_DRAG);
_auto_drag = 1;
Fl::e_x = lx;
Fl::e_y = ly;
Fl::check();
Fl::flush();
if (Fl::event_buttons() && _auto_drag) {
Fl::add_timeout(0.05, _auto_drag_cb2, this);
}
}
/**
Recalculate the dimensions of the table, and affect any children.
Internally, Fl_Group::resize() and init_sizes() are called.
*/
void Fl_Table::recalc_dimensions() {
// Recalc to* (Table Outer), ti* (Table Inner), wi* ( Widget Inner)
wix = ( x() + Fl::box_dx(box())); tox = wix; tix = tox + Fl::box_dx(table->box());
wiy = ( y() + Fl::box_dy(box())); toy = wiy; tiy = toy + Fl::box_dy(table->box());
wiw = ( w() - Fl::box_dw(box())); tow = wiw; tiw = tow - Fl::box_dw(table->box());
wih = ( h() - Fl::box_dh(box())); toh = wih; tih = toh - Fl::box_dh(table->box());
// Trim window if headers enabled
if ( col_header() ) {
tiy += col_header_height(); toy += col_header_height();
tih -= col_header_height(); toh -= col_header_height();
}
if ( row_header() ) {
tix += row_header_width(); tox += row_header_width();
tiw -= row_header_width(); tow -= row_header_width();
}
// Make scroll bars disappear if window large enough
{
// First pass: can hide via window size?
int hidev = (table_h <= tih);
int hideh = (table_w <= tiw);
int scrollsize = _scrollbar_size ? _scrollbar_size : Fl::scrollbar_size();
// Second pass: Check for interference
if ( !hideh && hidev ) { hidev = (( table_h - tih + scrollsize ) <= 0 ); }
if ( !hidev && hideh ) { hideh = (( table_w - tiw + scrollsize ) <= 0 ); }
// Determine scrollbar visibility, trim ti[xywh]/to[xywh]
if ( hidev ) { vscrollbar->hide(); }
else { vscrollbar->show(); tiw -= scrollsize; tow -= scrollsize; }
if ( hideh ) { hscrollbar->hide(); }
else { hscrollbar->show(); tih -= scrollsize; toh -= scrollsize; }
}
// Resize the child table
table->resize(tox, toy, tow, toh);
table->init_sizes();
}
/**
Recalculate internals after a scroll.
Call this if table has been scrolled or resized.
Does not handle redraw().
TODO: Assumes ti[xywh] has already been recalculated.
*/
void Fl_Table::table_scrolled() {
// Find top row
int y, row, voff = vscrollbar->value();
for ( row=y=0; row < _rows; row++ ) {
y += row_height(row);
if ( y > voff ) { y -= row_height(row); break; }
}
_row_position = toprow = ( row >= _rows ) ? (row - 1) : row;
toprow_scrollpos = y; // OPTIMIZATION: save for later use
// Find bottom row
voff = vscrollbar->value() + tih;
for ( ; row < _rows; row++ ) {
y += row_height(row);
if ( y >= voff ) { break; }
}
botrow = ( row >= _rows ) ? (row - 1) : row;
// Left column
int x, col, hoff = hscrollbar->value();
for ( col=x=0; col < _cols; col++ ) {
x += col_width(col);
if ( x > hoff ) { x -= col_width(col); break; }
}
_col_position = leftcol = ( col >= _cols ) ? (col - 1) : col;
leftcol_scrollpos = x; // OPTIMIZATION: save for later use
// Right column
// Work with data left over from leftcol calculation
//
hoff = hscrollbar->value() + tiw;
for ( ; col < _cols; col++ ) {
x += col_width(col);
if ( x >= hoff ) { break; }
}
rightcol = ( col >= _cols ) ? (col - 1) : col;
// First tell children to scroll
draw_cell(CONTEXT_RC_RESIZE, 0,0,0,0,0,0);
}
/**
Call this if table was resized, to recalculate internal data.
Calls recall_dimensions(), and recalculates scrollbar sizes.
*/
void Fl_Table::table_resized() {
table_h = (int)row_scroll_position(rows());
table_w = (int)col_scroll_position(cols());
recalc_dimensions();
// Recalc scrollbar sizes
// Clamp scrollbar value() after a resize.
// Resize scrollbars to enforce a constant trough width after a window resize.
//
{
// Vertical scrollbar
float vscrolltab = ( table_h == 0 || tih > table_h ) ? 1 : (float)tih / table_h;
float hscrolltab = ( table_w == 0 || tiw > table_w ) ? 1 : (float)tiw / table_w;
int scrollsize = _scrollbar_size ? _scrollbar_size : Fl::scrollbar_size();
vscrollbar->bounds(0, table_h-tih);
vscrollbar->precision(10);
vscrollbar->slider_size(vscrolltab);
vscrollbar->resize(wix+wiw-scrollsize, wiy,
scrollsize,
wih - ((hscrollbar->visible())?scrollsize:0));
vscrollbar->Fl_Valuator::value(vscrollbar->clamp(vscrollbar->value()));
// Horizontal scrollbar
hscrollbar->bounds(0, table_w-tiw);
hscrollbar->precision(10);
hscrollbar->slider_size(hscrolltab);
hscrollbar->resize(wix, wiy+wih-scrollsize,
wiw - ((vscrollbar->visible())?scrollsize:0),
scrollsize);
hscrollbar->Fl_Valuator::value(hscrollbar->clamp(hscrollbar->value()));
}
// Tell FLTK child widgets were resized
Fl_Group::init_sizes();
// Recalc top/bot/left/right
table_scrolled();
// DO *NOT* REDRAW -- LEAVE THIS UP TO THE CALLER
// redraw();
}
/**
Callback for when someone moves a scrollbar.
*/
void Fl_Table::scroll_cb(Fl_Widget*w, void *data) {
Fl_Table *o = (Fl_Table*)data;
o->recalc_dimensions(); // recalc tix, tiy, etc.
o->table_scrolled();
o->redraw();
}
/**
Sets the number of rows in the table, and the table is redrawn.
*/
void Fl_Table::rows(int val) {
int oldrows = _rows;
_rows = val;
int default_h = row_size() > 0 ? _rowheights->back() : 25;
int now_size = row_size();
if (now_size != val)
_rowheights->resize(val, default_h); // enlarge or shrink as needed
table_resized();
// OPTIMIZATION: redraw only if change is visible.
if ( val >= oldrows && oldrows > botrow ) {
// NO REDRAW
} else {
redraw();
}
}
/**
Set the number of columns in the table and redraw.
*/
void Fl_Table::cols(int val) {
_cols = val;
int default_w = col_size() > 0 ? (*_colwidths)[col_size()-1] : 80;
int now_size = col_size();
if (now_size != val)
_colwidths->resize(val, default_w); // enlarge or shrink as needed
table_resized();
redraw();
}
/**
Change mouse cursor to different type
*/
void Fl_Table::change_cursor(Fl_Cursor newcursor) {
if ( newcursor != _last_cursor ) {
fl_cursor(newcursor, FL_BLACK, FL_WHITE);
_last_cursor = newcursor;
}
}
/**
Sets the damage zone to the specified row/col values.
Calls redraw_range().
*/
void Fl_Table::damage_zone(int r1, int c1, int r2, int c2, int r3, int c3) {
int R1 = r1, C1 = c1;
int R2 = r2, C2 = c2;
if (r1 > R2) R2 = r1;
if (r2 < R1) R1 = r2;
if (r3 > R2) R2 = r3;
if (r3 < R1) R1 = r3;
if (c1 > C2) C2 = c1;
if (c2 < C1) C1 = c2;
if (c3 > C2) C2 = c3;
if (c3 < C1) C1 = c3;
if (R1 < 0) {
if (R2 < 0) return;
R1 = 0;
}
if (C1 < 0) {
if (C2 < 0) return;
C1 = 0;
}
if (R1 < toprow) R1 = toprow;
if (R2 > botrow) R2 = botrow;
if (C1 < leftcol) C1 = leftcol;
if (C2 > rightcol) C2 = rightcol;
redraw_range(R1, R2, C1, C2);
}
/**
Moves the selection cursor a relative number of rows/columns specifed by R/C.
R/C can be positive or negative, depending on the direction to move.
A value of 0 for R or C prevents cursor movement on that axis.
If shiftselect is set, the selection range is extended to the new
cursor position. If clear, the cursor is simply moved, and any previous
selection is cancelled.
Used mainly by keyboard events (e.g. Fl_Right, FL_Home, FL_End..)
to let the user keyboard navigate the selection cursor around.
The scroll positions may be modified if the selection cursor traverses
into cells off the screen's edge.
Internal variables select_row/select_col and current_row/current_col
are modified, among others.
\code
Examples:
R=1, C=0 -- moves the selection cursor one row downward.
R=5, C=0 -- moves the selection cursor 5 rows downward.
R=-5, C=0 -- moves the cursor 5 rows upward.
R=2, C=2 -- moves the cursor 2 rows down and 2 columns to the right.
\endcode
*/
int Fl_Table::move_cursor(int R, int C, int shiftselect) {
if (select_row == -1) R++;
if (select_col == -1) C++;
R += select_row;
C += select_col;
if (R < 0) R = 0;
if (R >= rows()) R = rows() - 1;
if (C < 0) C = 0;
if (C >= cols()) C = cols() - 1;
if (R == select_row && C == select_col) return 0;
damage_zone(current_row, current_col, select_row, select_col, R, C);
select_row = R;
select_col = C;
if (!shiftselect || !Fl::event_state(FL_SHIFT)) {
current_row = R;
current_col = C;
}
if (R < toprow + 1 || R > botrow - 1) row_position(R);
if (C < leftcol + 1 || C > rightcol - 1) col_position(C);
return 1;
}
/**
Same as move_cursor(R,C,1);
*/
int Fl_Table::move_cursor(int R, int C) {
return move_cursor(R,C,1);
}
//#define DEBUG 1
#ifdef DEBUG
#include <FL/names.h>
#define PRINTEVENT \
fprintf(stderr,"Table %s: ** Event: %s --\n", (label()?label():"none"), fl_eventnames[event]);
#else
#define PRINTEVENT
#endif
/**
Handle FLTK events.
*/
int Fl_Table::handle(int event) {
PRINTEVENT;
int ret = Fl_Group::handle(event); // let FLTK group handle events first
// Which row/column are we over?
int R, C; // row/column being worked on
ResizeFlag resizeflag; // which resizing area are we over? (0=none)
TableContext context = cursor2rowcol(R, C, resizeflag);
if (ret) {
if (Fl::event_inside(hscrollbar) || Fl::event_inside(vscrollbar)) return 1;
if ( context != CONTEXT_ROW_HEADER && // mouse not in row header (STR#2742)
context != CONTEXT_COL_HEADER && // mouse not in col header (STR#2742)
Fl::focus() != this && // we don't have focus?
contains(Fl::focus())) { // focus is a child?
return 1;
}
}
// Make snapshots of realtime event states *before* we service user's cb,
// which may do things like post popup menus that return with unexpected button states.
int _event_button = Fl::event_button();
int _event_clicks = Fl::event_clicks();
int _event_x = Fl::event_x();
int _event_y = Fl::event_y();
int _event_key = Fl::event_key();
int _event_state = Fl::event_state();
Fl_Widget *_focus = Fl::focus();
switch ( event ) {
case FL_PUSH:
// Single left-click on table? do user's callback with CONTEXT_TABLE
if (_event_button == 1 && !_event_clicks) {
if (_focus == this) {
take_focus();
do_callback(CONTEXT_TABLE, -1, -1);
ret = 1;
}
damage_zone(current_row, current_col, select_row, select_col, R, C);
if (context == CONTEXT_CELL) {
current_row = select_row = R;
current_col = select_col = C;
_selecting = CONTEXT_CELL;
} else {
// Clear selection if not resizing row/col
if ( !resizeflag ) {
current_row = select_row = -1;
current_col = select_col = -1;
}
}
}
// A click on table with user's callback defined?
// Need this for eg. right click to pop up a menu
//
if ( Fl_Widget::callback() && // callback defined?
resizeflag == RESIZE_NONE ) { // not resizing?
do_callback(context, R, C); // do callback with context (cell, header, etc)
}
// Handle selection if handling a left-click
// Use snapshot of _event_button we made before servicing user's cb's
// to avoid checking realtime state of buttons which may have changed
// during the user's callbacks.
//
switch ( context ) {
case CONTEXT_CELL:
// FL_PUSH on a cell?
ret = 1; // express interest in FL_RELEASE
break;
case CONTEXT_NONE:
// FL_PUSH on table corner?
if ( _event_button == 1 && _event_x < x() + row_header_width()) {
current_col = 0;
select_col = cols() - 1;
current_row = 0;
select_row = rows() - 1;
damage_zone(current_row, current_col, select_row, select_col);
ret = 1;
}
break;
case CONTEXT_COL_HEADER:
// FL_PUSH on a column header?
if ( _event_button == 1) {
// Resizing? Handle it
if ( resizeflag ) {
// Start resize if left click on column border.
// "ret=1" ensures we get drag events from now on.
// (C-1) is used if mouse is over the left hand side
// of cell, so we resize the next column on the left.
//
_resizing_col = ( resizeflag & RESIZE_COL_LEFT ) ? C-1 : C;
_resizing_row = -1;
_dragging_x = _event_x;
ret = 1;
} else {
// Not resizing? Select the column
if ( Fl::focus() != this && contains(Fl::focus()) ) return 0; // STR #3018 - item 1
current_col = select_col = C;
current_row = 0;
select_row = rows() - 1;
_selecting = CONTEXT_COL_HEADER;
damage_zone(current_row, current_col, select_row, select_col);
ret = 1;
}
}
break;
case CONTEXT_ROW_HEADER:
// FL_PUSH on a row header?
if ( _event_button == 1 ) {
// Resizing? Handle it
if ( resizeflag ) {
// Start resize if left mouse clicked on row border.
// "ret = 1" ensures we get drag events from now on.
// (R-1) is used if mouse is over the top of the cell,
// so that we resize the row above.
//
_resizing_row = ( resizeflag & RESIZE_ROW_ABOVE ) ? R-1 : R;
_resizing_col = -1;
_dragging_y = _event_y;
ret = 1;
} else {
// Not resizing? Select the row
if ( Fl::focus() != this && contains(Fl::focus()) ) return 0; // STR #3018 - item 1
current_row = select_row = R;
current_col = 0;
select_col = cols() - 1;
_selecting = CONTEXT_ROW_HEADER;
damage_zone(current_row, current_col, select_row, select_col);
ret = 1;
}
}
break;
default:
ret = 0; // express disinterest
break;
}
_last_row = R;
break;
case FL_DRAG:
if (_auto_drag == 1) {
ret = 1;
break;
}
if ( _resizing_col > -1 ) {
// Dragging column?
//
// Let user drag even /outside/ the row/col widget.
// Don't allow column width smaller than 1.
// Continue to show FL_CURSOR_WE at all times during drag.
//
int offset = _dragging_x - _event_x;
int new_w = col_width(_resizing_col) - offset;
if ( new_w < _col_resize_min ) new_w = _col_resize_min;
col_width(_resizing_col, new_w);
_dragging_x = _event_x;
table_resized();
redraw();
change_cursor(FL_CURSOR_WE);
ret = 1;
if ( Fl_Widget::callback() && when() & FL_WHEN_CHANGED ) {
do_callback(CONTEXT_RC_RESIZE, R, C);
}
}
else if ( _resizing_row > -1 ) {
// Dragging row?
//
// Let user drag even /outside/ the row/col widget.
// Don't allow row width smaller than 1.
// Continue to show FL_CURSOR_NS at all times during drag.
//
int offset = _dragging_y - _event_y;
int new_h = row_height(_resizing_row) - offset;
if ( new_h < _row_resize_min ) new_h = _row_resize_min;
row_height(_resizing_row, new_h);
_dragging_y = _event_y;
table_resized();
redraw();
change_cursor(FL_CURSOR_NS);
ret = 1;
if ( Fl_Widget::callback() && when() & FL_WHEN_CHANGED ) {
do_callback(CONTEXT_RC_RESIZE, R, C);
}
} else {
if (_event_button == 1 &&
_selecting == CONTEXT_CELL &&
context == CONTEXT_CELL) {
// Dragging a cell selection?
if ( _event_clicks ) break; // STR #3018 - item 2
if (select_row != R || select_col != C) {
damage_zone(current_row, current_col, select_row, select_col, R, C);
}
select_row = R;
select_col = C;
ret = 1;
}
else if (_event_button == 1 &&
_selecting == CONTEXT_ROW_HEADER &&
context & (CONTEXT_ROW_HEADER|CONTEXT_COL_HEADER|CONTEXT_CELL)) {
if (select_row != R) {
damage_zone(current_row, current_col, select_row, select_col, R, C);
}
select_row = R;
ret = 1;
}
else if (_event_button == 1 &&
_selecting == CONTEXT_COL_HEADER
&& context & (CONTEXT_ROW_HEADER|CONTEXT_COL_HEADER|CONTEXT_CELL)) {
if (select_col != C) {
damage_zone(current_row, current_col, select_row, select_col, R, C);
}
select_col = C;
ret = 1;
}
}
// Enable autodrag if not resizing, and mouse has moved off table edge
if ( _resizing_row < 0 && _resizing_col < 0 && _auto_drag == 0 &&
( _event_x > x() + w() - 20 ||
_event_x < x() + row_header_width() ||
_event_y > y() + h() - 20 ||
_event_y < y() + col_header_height()
) ) {
_start_auto_drag();
}
break;
case FL_RELEASE:
_stop_auto_drag();
switch ( context ) {
case CONTEXT_ROW_HEADER: // release on row header
case CONTEXT_COL_HEADER: // release on col header
case CONTEXT_CELL: // release on a cell
case CONTEXT_TABLE: // release on dead zone
if ( _resizing_col == -1 && // not resizing a column
_resizing_row == -1 && // not resizing a row
Fl_Widget::callback() && // callback defined
when() & FL_WHEN_RELEASE && // on button release
_last_row == R ) { // release on same row PUSHed?
// Need this for eg. left clicking on a cell to select it
do_callback(context, R, C);
}
break;
default:
break;
}
if ( _event_button == 1 ) {
change_cursor(FL_CURSOR_DEFAULT);
_resizing_col = -1;
_resizing_row = -1;
ret = 1;
}
break;
case FL_MOVE:
if ( context == CONTEXT_COL_HEADER && // in column header?
resizeflag ) { // resize + near boundary?
change_cursor(FL_CURSOR_WE); // show resize cursor
}
else if ( context == CONTEXT_ROW_HEADER && // in row header?
resizeflag ) { // resize + near boundary?
change_cursor(FL_CURSOR_NS); // show resize cursor
} else {
change_cursor(FL_CURSOR_DEFAULT); // normal cursor
}
ret = 1;
break;
case FL_ENTER: // See FLTK event docs on the FL_ENTER widget
if (!ret) take_focus();
ret = 1;
//FALLTHROUGH
case FL_LEAVE: // We want to track the mouse if resizing is allowed.
if ( resizeflag ) {
ret = 1;
}
if ( event == FL_LEAVE ) {
_stop_auto_drag();
change_cursor(FL_CURSOR_DEFAULT);
}
break;
case FL_FOCUS:
Fl::focus(this);
//FALLTHROUGH
case FL_UNFOCUS:
_stop_auto_drag();
ret = 1;
break;
case FL_KEYBOARD: {
ret = 0;
int is_row = select_row;
int is_col = select_col;
switch(_event_key) {
case FL_Home:
ret = move_cursor(0, -1000000);
break;
case FL_End:
ret = move_cursor(0, 1000000);
break;
case FL_Page_Up:
ret = move_cursor(-(botrow - toprow - 1), 0);
break;
case FL_Page_Down:
ret = move_cursor(botrow - toprow - 1 , 0);
break;
case FL_Left:
ret = move_cursor(0, -1);
break;
case FL_Right:
ret = move_cursor(0, 1);
break;
case FL_Up:
ret = move_cursor(-1, 0);
break;
case FL_Down:
ret = move_cursor(1, 0);
break;
case FL_Tab:
if ( !tab_cell_nav() ) break; // not navigating cells? let fltk handle it (STR#2862)
if ( _event_state & FL_SHIFT ) {
ret = move_cursor(0, -1, 0); // shift-tab -> left
} else {
ret = move_cursor(0, 1, 0); // tab -> right
}
break;
}
if (ret && Fl::focus() != this) {
do_callback(CONTEXT_TABLE, -1, -1);
take_focus();
}
//if (!ret && Fl_Widget::callback() && when() & FL_WHEN_NOT_CHANGED )
if ( Fl_Widget::callback() &&
(
( !ret && when() & FL_WHEN_NOT_CHANGED ) ||
( is_row!= select_row || is_col!= select_col )
)
) {
do_callback(CONTEXT_CELL, select_row, select_col);
//damage_zone(current_row, current_col, select_row, select_col);
ret = 1;
}
break;
}
default:
change_cursor(FL_CURSOR_DEFAULT);
break;
}
return(ret);
}
/**
Handle resize events if user resizes parent window.
This changes the size of Fl_Table, causing it to redraw.
*/
void Fl_Table::resize(int X, int Y, int W, int H) {
// Tell group to resize, and recalc our own widget as well
Fl_Group::resize(X, Y, W, H);
table_resized();
redraw();
}
// Draw a cell
void Fl_Table::_redraw_cell(TableContext context, int r, int c) {
if ( r < 0 || c < 0 ) return;
int X,Y,W,H;
find_cell(context, r, c, X, Y, W, H); // find positions of cell
draw_cell(context, r, c, X, Y, W, H); // call users' function to draw it
}
/**
See if the cell at row \p r and column \p c is selected.
\returns 1 if the cell is selected, 0 if not.
*/
int Fl_Table::is_selected(int r, int c) {
int s_left, s_right, s_top, s_bottom;
if (select_col > current_col) {
s_left = current_col;
s_right = select_col;
} else {
s_right = current_col;
s_left = select_col;
}
if (select_row > current_row) {
s_top = current_row;
s_bottom = select_row;
} else {
s_bottom = current_row;
s_top = select_row;
}
if (r >= s_top && r <= s_bottom && c >= s_left && c <= s_right) {
return 1;
}
return 0;
}
/**
Gets the region of cells selected (highlighted).
\param[in] row_top Returns the top row of selection area
\param[in] col_left Returns the left column of selection area
\param[in] row_bot Returns the bottom row of selection area
\param[in] col_right Returns the right column of selection area
\internal
This method is 'const' since FLTK 1.5 (and 1.4.5 ABI), see issue #1305
*/
void Fl_Table::get_selection(int& row_top, int& col_left, int& row_bot, int& col_right) const {
if (select_col > current_col) {
col_left = current_col;
col_right = select_col;
} else {
col_right = current_col;
col_left = select_col;
}
if (select_row > current_row) {
row_top = current_row;
row_bot = select_row;
} else {
row_bot = current_row;
row_top = select_row;
}
}
/**
Sets the region of cells to be selected (highlighted).
So for instance, set_selection(0,0,0,0) selects the top/left cell in the table.
And set_selection(0,0,1,1) selects the four cells in rows 0 and 1, column 0 and 1.
To deselect all cells, use set_selection(-1,-1,-1,-1);
\param[in] row_top Top row of selection area
\param[in] col_left Left column of selection area
\param[in] row_bot Bottom row of selection area
\param[in] col_right Right column of selection area
*/
void Fl_Table::set_selection(int row_top, int col_left, int row_bot, int col_right) {
damage_zone(current_row, current_col, select_row, select_col);
current_col = col_left;
current_row = row_top;
select_col = col_right;
select_row = row_bot;
damage_zone(current_row, current_col, select_row, select_col);
}
/**
Draws the entire Fl_Table.
Lets fltk widgets draw themselves first, followed by the cells
via calls to draw_cell().
*/
void Fl_Table::draw() {
int scrollsize = _scrollbar_size ? _scrollbar_size : Fl::scrollbar_size();
// Check if scrollbar size changed
if ( ( vscrollbar && (scrollsize != vscrollbar->w()) ) ||
( hscrollbar && (scrollsize != hscrollbar->h()) ) ) {
// handle size change, min/max, table dim's, etc
table_resized();
}
draw_cell(CONTEXT_STARTPAGE, 0, 0, // let user's drawing routine
tix, tiy, tiw, tih); // prep new page
// Let fltk widgets draw themselves first. Do this after
// draw_cell(CONTEXT_STARTPAGE) in case user moves widgets around.
// Use window 'inner' clip to prevent drawing into table border.
// (unfortunately this clips FLTK's border, so we must draw it explicitly below)
//
fl_push_clip(wix, wiy, wiw, wih);
{
Fl_Group::draw();
}
fl_pop_clip();
// Explicitly draw border around widget, if any
draw_box(box(), x(), y(), w(), h(), color());
// If Fl_Scroll 'table' is hidden, draw its box
// Do this after Fl_Group::draw() so we draw over scrollbars
// that leak around the border.
//
if ( ! table->visible() ) {
if ( damage() & FL_DAMAGE_ALL || damage() & FL_DAMAGE_CHILD ) {
draw_box(table->box(), tox, toy, tow, toh, table->color());
}
}
// Clip all further drawing to the inner widget dimensions
fl_push_clip(wix, wiy, wiw, wih);
{
// Only redraw a few cells?
if ( ! ( damage() & FL_DAMAGE_ALL ) && _redraw_leftcol != -1 ) {
fl_push_clip(tix, tiy, tiw, tih);
for ( int c = _redraw_leftcol; c <= _redraw_rightcol; c++ ) {
for ( int r = _redraw_toprow; r <= _redraw_botrow; r++ ) {
_redraw_cell(CONTEXT_CELL, r, c);
}
}
fl_pop_clip();
}
if ( damage() & FL_DAMAGE_ALL ) {
int X,Y,W,H;
// Draw row headers, if any
if ( row_header() ) {
get_bounds(CONTEXT_ROW_HEADER, X, Y, W, H);
fl_push_clip(X,Y,W,H);
for ( int r = toprow; r <= botrow; r++ ) {
_redraw_cell(CONTEXT_ROW_HEADER, r, 0);
}
fl_pop_clip();
}
// Draw column headers, if any
if ( col_header() ) {
get_bounds(CONTEXT_COL_HEADER, X, Y, W, H);
fl_push_clip(X,Y,W,H);
for ( int c = leftcol; c <= rightcol; c++ ) {
_redraw_cell(CONTEXT_COL_HEADER, 0, c);
}
fl_pop_clip();
}
// Draw all cells.
// This includes cells partially obscured off edges of table.
// No longer do this last; you might think it would be nice
// to draw over dead zones, but on redraws it flickers. Avoid
// drawing over deadzones; prevent deadzones by sizing columns.
//
fl_push_clip(tix, tiy, tiw, tih); {
for ( int r = toprow; r <= botrow; r++ ) {
for ( int c = leftcol; c <= rightcol; c++ ) {
_redraw_cell(CONTEXT_CELL, r, c);
}
}
}
fl_pop_clip();
// Draw little rectangle in corner of headers
if ( row_header() && col_header() ) {
fl_rectf(wix, wiy, row_header_width(), col_header_height(), color());
}
// Table has a boxtype? Close those few dead pixels
if ( table->box() ) {
if ( col_header() ) {
fl_rectf(tox, wiy, Fl::box_dx(table->box()), col_header_height(), color());
}
if ( row_header() ) {
fl_rectf(wix, toy, row_header_width(), Fl::box_dx(table->box()), color());
}
}
// Table width smaller than window? Fill remainder with rectangle
if ( table_w < tiw ) {
fl_rectf(tix + table_w, tiy, tiw - table_w, tih, color());
// Col header? fill that too
if ( col_header() ) {
fl_rectf(tix + table_w,
wiy,
// get that corner just right..
(tiw - table_w + Fl::box_dw(table->box()) -
Fl::box_dx(table->box())),
col_header_height(),
color());
}
}
// Table height smaller than window? Fill remainder with rectangle
if ( table_h < tih ) {
fl_rectf(tix, tiy + table_h, tiw, tih - table_h, color());
if ( row_header() ) {
// NOTE:
// Careful with that lower corner; don't use tih; when eg.
// table->box(FL_THIN_UP_FRAME) and hscrollbar hidden,
// leaves a row of dead pixels.
//
fl_rectf(wix, tiy + table_h, row_header_width(),
(wiy+wih) - (tiy+table_h) -
( hscrollbar->visible() ? scrollsize : 0),
color());
}
}
}
// Both scrollbars? Draw little box in lower right
if ( vscrollbar->visible() && hscrollbar->visible() ) {
fl_rectf(vscrollbar->x(), hscrollbar->y(),
vscrollbar->w(), hscrollbar->h(), color());
}
draw_cell(CONTEXT_ENDPAGE, 0, 0, // let user's drawing
tix, tiy, tiw, tih); // routines cleanup
_redraw_leftcol = _redraw_rightcol = _redraw_toprow = _redraw_botrow = -1;
}
fl_pop_clip();
}
/**
Returns the current height of the specified row as a value in pixels.
*/
int Fl_Table::row_height(int row) {
return((row < 0 || row >= row_size()) ? 0 : (*_rowheights)[row]);
}
/**
Returns the current width of the specified column in pixels.
*/
int Fl_Table::col_width(int col) {
return((col < 0 || col >= col_size()) ? 0 : (*_colwidths)[col]);
}