// // table-sort -- An example application using a sortable Fl_Table // // Originally the 'sortapp.cxx' example program that came with // erco's Fl_Table widget. Added to FLTK in 2010. // // Example of a non-trivial application that uses Fl_Table // with sortable columns. This example is not trying to be simple, // but to demonstrate the complexities of an actual app. // // Copyright 2010 Greg Ercolano. // Copyright 2011-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 #include #include #include #define MARGIN 20 #ifdef _WIN32 // WINDOWS // DIR flags that simplify parsing: // /-C -- disable 1000's separator in file sizes // /A-D -- don't show directories # define DIRCMD "dir /-C /A-D" static const char *G_header[] = { "Date", "Time", "Size", "Filename" }; static int G_header_count = 4; # ifdef _MSC_VER # define popen _popen # define pclose _pclose # endif #else /* _WIN32 */ // UNIX # define DIRCMD "ls -l" static const char *G_header[] = { "Perms", "#L", "Own", "Group", "Size", "Date", "", "", "Filename" }; static int G_header_count = 9; #endif /* _WIN32 */ // Font face/sizes for header and rows #define HEADER_FONTFACE FL_HELVETICA_BOLD #define HEADER_FONTSIZE 16 #define ROW_FONTFACE FL_HELVETICA #define ROW_FONTSIZE 16 // A single row of columns struct Row { char **cols; int cols_count; int cols_alloc; }; static void row_init(Row *r) { r->cols = 0; r->cols_count = 0; r->cols_alloc = 0; } static void row_free(Row *r) { int i; for (i = 0; i < r->cols_count; i++) { free(r->cols[i]); } free(r->cols); r->cols = 0; r->cols_count = 0; r->cols_alloc = 0; } static void row_push(Row *r, const char *s) { if (r->cols_count >= r->cols_alloc) { int new_alloc = r->cols_alloc ? r->cols_alloc * 2 : 16; r->cols = (char **)realloc(r->cols, new_alloc * sizeof(char *)); r->cols_alloc = new_alloc; } r->cols[r->cols_count++] = strdup(s); } static void row_append_last(Row *r, const char *s) { if (r->cols_count > 0) { char *old = r->cols[r->cols_count - 1]; size_t oldlen = strlen(old); size_t slen = strlen(s); char *newstr = (char *)malloc(oldlen + 1 + slen + 1); memcpy(newstr, old, oldlen); newstr[oldlen] = ' '; memcpy(newstr + oldlen + 1, s, slen + 1); free(old); r->cols[r->cols_count - 1] = newstr; } } // Global sort state for qsort static int g_sort_col = 0; static int g_sort_reverse = 0; static int row_compare(const void *a, const void *b) { const Row *ra = (const Row *)a; const Row *rb = (const Row *)b; const char *ap = (g_sort_col < ra->cols_count) ? ra->cols[g_sort_col] : ""; const char *bp = (g_sort_col < rb->cols_count) ? rb->cols[g_sort_col] : ""; int result; if (isdigit((unsigned char)*ap) && isdigit((unsigned char)*bp)) { // Numeric sort int av = 0, bv = 0; sscanf(ap, "%d", &av); sscanf(bp, "%d", &bv); result = bv - av; } else { // Alphabetic sort result = strcmp(ap, bp); } return g_sort_reverse ? -result : result; } // Derive a custom class from Fl_Table_Row class MyTable : public Fl_Table_Row { private: Row *rowdata_; int rowdata_count_; int rowdata_alloc_; int sort_reverse_; int sort_lastcol_; static void event_callback(Fl_Widget*, void*); void event_callback2(); // callback for table events protected: void draw_cell(TableContext context, int R=0, int C=0, // table cell drawing int X=0, int Y=0, int W=0, int H=0); void sort_column(int col, int reverse=0); // sort table by a column void draw_sort_arrow(int X,int Y,int W,int H); public: // Ctor MyTable(int x, int y, int w, int h, const char *l=0) : Fl_Table_Row(x,y,w,h,l) { rowdata_ = 0; rowdata_count_ = 0; rowdata_alloc_ = 0; sort_reverse_ = 0; sort_lastcol_ = -1; end(); callback(event_callback, (void*)this); } ~MyTable() { int i; for (i = 0; i < rowdata_count_; i++) { row_free(&rowdata_[i]); } free(rowdata_); } void load_command(const char *cmd); // Load the output of a command into table void autowidth(int pad); // Automatically set column widths to data void resize_window(); // Resize parent window to size of table }; // Sort a column up or down void MyTable::sort_column(int col, int reverse) { g_sort_col = col; g_sort_reverse = reverse; qsort(rowdata_, rowdata_count_, sizeof(Row), row_compare); redraw(); } // Draw sort arrow void MyTable::draw_sort_arrow(int X,int Y,int W,int H) { int xlft = X+(W-6)-8; int xctr = X+(W-6)-4; int xrit = X+(W-6)-0; int ytop = Y+(H/2)-4; int ybot = Y+(H/2)+4; if ( sort_reverse_ ) { // Engraved down arrow fl_color(FL_WHITE); fl_line(xrit, ytop, xctr, ybot); fl_color(41); // dark gray fl_line(xlft, ytop, xrit, ytop); fl_line(xlft, ytop, xctr, ybot); } else { // Engraved up arrow fl_color(FL_WHITE); fl_line(xrit, ybot, xctr, ytop); fl_line(xrit, ybot, xlft, ybot); fl_color(41); // dark gray fl_line(xlft, ybot, xctr, ytop); } } // Handle drawing all cells in table void MyTable::draw_cell(TableContext context, int R, int C, int X, int Y, int W, int H) { const char *s = ""; if ( R < rowdata_count_ && C < rowdata_[R].cols_count ) s = rowdata_[R].cols[C]; switch ( context ) { case CONTEXT_COL_HEADER: fl_push_clip(X,Y,W,H); { fl_draw_box(FL_THIN_UP_BOX, X,Y,W,H, FL_BACKGROUND_COLOR); if ( C < G_header_count ) { fl_font(HEADER_FONTFACE, HEADER_FONTSIZE); fl_color(FL_BLACK); fl_draw(G_header[C], X+2,Y,W,H, FL_ALIGN_LEFT, 0, 0); // +2=pad left // Draw sort arrow if ( C == sort_lastcol_ ) { draw_sort_arrow(X,Y,W,H); } } } fl_pop_clip(); return; case CONTEXT_CELL: { fl_push_clip(X,Y,W,H); { // Bg color Fl_Color bgcolor = row_selected(R) ? selection_color() : FL_WHITE; fl_color(bgcolor); fl_rectf(X,Y,W,H); fl_font(ROW_FONTFACE, ROW_FONTSIZE); fl_color(FL_BLACK); fl_draw(s, X+2,Y,W,H, FL_ALIGN_LEFT); // +2=pad left // Border fl_color(FL_LIGHT2); fl_rect(X,Y,W,H); } fl_pop_clip(); return; } default: return; } } // Automatically set column widths to widest data in each column void MyTable::autowidth(int pad) { int w, h, c, r; // Initialize all column widths to header width fl_font(HEADER_FONTFACE, HEADER_FONTSIZE); for ( c = 0; c < G_header_count; c++ ) { w=0; fl_measure(G_header[c], w, h, 0); // pixel width of header text col_width(c, w+pad); } fl_font(ROW_FONTFACE, ROW_FONTSIZE); for ( r=0; r col_width(c)) col_width(c, w + pad); } } table_resized(); redraw(); } // Resize parent window to size of table void MyTable::resize_window() { int t; // Determine exact outer width of table with all columns visible int width = 2; // width of table borders for ( t=0; tw(); // include width of scrollbar width += MARGIN*2; if ( width < 200 || width > Fl::w() ) return; window()->resize(window()->x(), window()->y(), width, window()->h()); // resize window to fit } // Load table with output of 'cmd' void MyTable::load_command(const char *cmd) { char s[512]; int line; FILE *fp = popen(cmd, "r"); cols(0); for ( line=0; fgets(s, sizeof(s)-1, fp); line++ ) { #ifdef _WIN32 // WINDOWS if (s[0] == '\n' || s[0] == ' ') continue; // ignore header/footer lines #else // UNIX if ( line==0 && strncmp(s,"total ",6)==0) continue; #endif // Add a new row if (rowdata_count_ >= rowdata_alloc_) { int new_alloc = rowdata_alloc_ ? rowdata_alloc_ * 2 : 64; rowdata_ = (Row *)realloc(rowdata_, new_alloc * sizeof(Row)); rowdata_alloc_ = new_alloc; } row_init(&rowdata_[rowdata_count_]); rowdata_count_++; Row *rc = &rowdata_[rowdata_count_ - 1]; // Break line into separate word 'columns' char *ss; int t; const char *delim = " \t\n"; for(t=0; (t==0)?(ss=strtok(s,delim)):(ss=strtok(0,delim)); t++) { #ifdef _WIN32 // DIR: Some systems show meridiem field, some don't (24hr time) if (t==2 && (strcmp(ss,"AM")==0 || strcmp(ss,"PM")==0)) { row_append_last(rc, ss); continue; } #endif row_push(rc, ss); } // Keep track of max # columns if ( rc->cols_count > cols() ) { cols(rc->cols_count); } } pclose(fp); // How many rows we loaded rows(rowdata_count_); // Auto-calculate widths, with 20 pixel padding autowidth(20); } // Callback whenever someone clicks on different parts of the table void MyTable::event_callback(Fl_Widget*, void *data) { MyTable *o = (MyTable*)data; o->event_callback2(); } void MyTable::event_callback2() { //int ROW = callback_row(); // unused int COL = callback_col(); TableContext context = callback_context(); switch ( context ) { case CONTEXT_COL_HEADER: { // someone clicked on column header if ( Fl::event() == FL_RELEASE && Fl::event_button() == 1 ) { if ( sort_lastcol_ == COL ) { // Click same column? Toggle sort sort_reverse_ ^= 1; } else { // Click diff column? Up sort sort_reverse_ = 0; } sort_column(COL, sort_reverse_); sort_lastcol_ = COL; } break; } default: return; } } int main() { Fl_Double_Window win(900,500,"Table Sort"); MyTable table(MARGIN, MARGIN, win.w()-MARGIN*2, win.h()-MARGIN*2); table.selection_color(FL_YELLOW); table.col_header(1); table.col_resize(1); table.when(FL_WHEN_RELEASE); // handle table events on release table.load_command(DIRCMD); // load table with a directory listing table.row_height_all(18); // height of all rows table.tooltip("Click on column headings to toggle column sorting"); table.color(FL_WHITE); win.end(); win.resizable(table); table.resize_window(); win.show(); return(Fl::run()); }