// // Fl_Table -- A table widget for the Fast Light Tool Kit (FLTK). // // Copyright 2002 by Greg Ercolano. // Copyright (c) 2004 O'ksi'D // Copyright 2023-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 #include #include #include #include // memcpy #include // fprintf #include // 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= leftcol ) { scroll = leftcol_scrollpos; startcol = leftcol; } for ( int t=startcol; t; // column widths in pixels _rowheights = new std::vector; // 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 #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 */ void Fl_Table::get_selection(int& row_top, int& col_left, int& row_bot, int& col_right) { 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]); }