summaryrefslogtreecommitdiff
path: root/examples/table-sort.cxx
blob: 94c885ffa5d59244bc1b76b74cad148d31ae391d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
//
//      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 <FL/Fl.H>
#include <FL/Fl_Double_Window.H>
#include <FL/fl_draw.H>
#include <FL/Fl_Table_Row.H>

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>

#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<rowdata_count_; r++ ) {
        for ( c=0; c<rowdata_[r].cols_count; c++ ) {
            w=0; fl_measure(rowdata_[r].cols[c], w, h, 0);       // pixel width of row text
            if ( (w + pad) > 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; t<cols(); t++ ) width += col_width(t);   // total width of all columns
    width += vscrollbar->w();                               // 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());
}