// // "$Id$" // // Browser widget for the Fast Light Tool Kit (FLTK). // // Copyright 1998-2009 by Bill Spitzak and others. // // This library is free software; you can redistribute it and/or // modify it under the terms of the GNU Library General Public // License as published by the Free Software Foundation; either // version 2 of the License, or (at your option) any later version. // // This library is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // Library General Public License for more details. // // You should have received a copy of the GNU Library General Public // License along with this library; if not, write to the Free Software // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 // USA. // // Please report all bugs and problems on the following page: // // http://www.fltk.org/str.php // #include #include #include #include "flstring.h" #include #include // I modified this from the original Forms data to use a linked list // so that the number of items in the browser and size of those items // is unlimited. The only problem is that the old browser used an // index number to identify a line, and it is slow to convert from/to // a pointer. I use a cache of the last match to try to speed this up. // Also added the ability to "hide" a line. This sets its height to // zero, so the Fl_Browser_ cannot pick it. #define SELECTED 1 #define NOTDISPLAYED 2 struct FL_BLINE { // data is in a linked list of these FL_BLINE* prev; FL_BLINE* next; void* data; short length; // sizeof(txt)-1, may be longer than string char flags; // selected, displayed char txt[1]; // start of allocated array }; void* Fl_Browser::item_first() const {return first;} void* Fl_Browser::item_next(void* l) const {return ((FL_BLINE*)l)->next;} void* Fl_Browser::item_prev(void* l) const {return ((FL_BLINE*)l)->prev;} void* Fl_Browser::item_last() const {return last;} int Fl_Browser::item_selected(void* l) const { return ((FL_BLINE*)l)->flags&SELECTED;} void Fl_Browser::item_select(void* l, int v) { if (v) ((FL_BLINE*)l)->flags |= SELECTED; else ((FL_BLINE*)l)->flags &= ~SELECTED; } const char *Fl_Browser::item_text(void *item) const { return ((FL_BLINE*)item)->txt; } /** Return entry for line number \a line. */ FL_BLINE* Fl_Browser::find_line(int line) const { int n; FL_BLINE* l; if (line == cacheline) return cache; if (cacheline && line > (cacheline/2) && line < ((cacheline+lines)/2)) { n = cacheline; l = cache; } else if (line <= (lines/2)) { n = 1; l = first; } else { n = lines; l = last; } for (; n < line && l; n++) l = l->next; for (; n > line && l; n--) l = l->prev; ((Fl_Browser*)this)->cacheline = line; ((Fl_Browser*)this)->cache = l; return l; } /** Returns line number corresponding to data \a v, zero if not found. */ int Fl_Browser::lineno(void* v) const { FL_BLINE* l = (FL_BLINE*)v; if (!l) return 0; if (l == cache) return cacheline; if (l == first) return 1; if (l == last) return lines; if (!cache) { ((Fl_Browser*)this)->cache = first; ((Fl_Browser*)this)->cacheline = 1; } // assumme it is near cache, search both directions: FL_BLINE* b = cache->prev; int bnum = cacheline-1; FL_BLINE* f = cache->next; int fnum = cacheline+1; int n = 0; for (;;) { if (b == l) {n = bnum; break;} if (f == l) {n = fnum; break;} if (b) {b = b->prev; bnum--;} if (f) {f = f->next; fnum++;} } ((Fl_Browser*)this)->cache = l; ((Fl_Browser*)this)->cacheline = n; return n; } /** Remove entry for given line number. \param[in] line line number. Must be in range! \returns pointer to browser entry. */ FL_BLINE* Fl_Browser::_remove(int line) { FL_BLINE* ttt = find_line(line); deleting(ttt); cacheline = line-1; cache = ttt->prev; lines--; full_height_ -= item_height(ttt); if (ttt->prev) ttt->prev->next = ttt->next; else first = ttt->next; if (ttt->next) ttt->next->prev = ttt->prev; else last = ttt->prev; return(ttt); } /** Remove line \a line and make the browser one line shorter. */ void Fl_Browser::remove(int line) { if (line < 1 || line > lines) return; free(_remove(line)); } /** Insert a new line \a t \e before line \a n. If \a n > size() then the line is added to the end. */ void Fl_Browser::insert(int line, FL_BLINE* t) { if (!first) { t->prev = t->next = 0; first = last = t; } else if (line <= 1) { inserting(first, t); t->prev = 0; t->next = first; t->next->prev = t; first = t; } else if (line > lines) { t->prev = last; t->prev->next = t; t->next = 0; last = t; } else { FL_BLINE* n = find_line(line); inserting(n, t); t->next = n; t->prev = n->prev; t->prev->next = t; n->prev = t; } cacheline = line; cache = t; lines++; full_height_ += item_height(t); redraw_line(t); } /** Insert a new entry \e before given line. \param[in] line if \a line > size(), the entry will be added at the end. \param[in] newtext text entry for the new line. \param[in] d pointer to data associated with the new line. */ void Fl_Browser::insert(int line, const char* newtext, void* d) { int l = strlen(newtext); FL_BLINE* t = (FL_BLINE*)malloc(sizeof(FL_BLINE)+l); t->length = (short)l; t->flags = 0; strcpy(t->txt, newtext); t->data = d; insert(line, t); } /** Line \a from is removed and reinserted at \a to; \a to is calculated after the line is removed. */ void Fl_Browser::move(int to, int from) { if (from < 1 || from > lines) return; insert(to, _remove(from)); } /** Sets the text for line \a line to text \a newtext. Does nothing if \a line is out of range. */ void Fl_Browser::text(int line, const char* newtext) { if (line < 1 || line > lines) return; FL_BLINE* t = find_line(line); int l = strlen(newtext); if (l > t->length) { FL_BLINE* n = (FL_BLINE*)malloc(sizeof(FL_BLINE)+l); replacing(t, n); cache = n; n->data = t->data; n->length = (short)l; n->flags = t->flags; n->prev = t->prev; if (n->prev) n->prev->next = n; else first = n; n->next = t->next; if (n->next) n->next->prev = n; else last = n; free(t); t = n; } strcpy(t->txt, newtext); redraw_line(t); } /** Sets the data for line \a line to \a d. Does nothing if \a line is out of range. */ void Fl_Browser::data(int line, void* d) { if (line < 1 || line > lines) return; find_line(line)->data = d; } /** Returns height of line \p lv. */ int Fl_Browser::item_height(void* lv) const { FL_BLINE* l = (FL_BLINE*)lv; if (l->flags & NOTDISPLAYED) return 0; int hmax = 2; // use 2 to insure we don't return a zero! if (!l->txt[0]) { // For blank lines set the height to exactly 1 line! fl_font(textfont(), textsize()); int hh = fl_height(); if (hh > hmax) hmax = hh; } else { const int* i = column_widths(); // do each column separately as they may all set different fonts: for (char* str = l->txt; str && *str; str++) { Fl_Font font = textfont(); // default font int tsize = textsize(); // default size while (*str==format_char()) { str++; switch (*str++) { case 'l': case 'L': tsize = 24; break; case 'm': case 'M': tsize = 18; break; case 's': tsize = 11; break; case 'b': font = (Fl_Font)(font|FL_BOLD); break; case 'i': font = (Fl_Font)(font|FL_ITALIC); break; case 'f': case 't': font = FL_COURIER; break; case 'B': case 'C': strtol(str, &str, 10); break;// skip a color number case 'F': font = (Fl_Font)strtol(str,&str,10); break; case 'S': tsize = strtol(str,&str,10); break; case 0: case '@': str--; case '.': goto END_FORMAT; } } END_FORMAT: char* ptr = str; if (ptr && *i++) str = strchr(str, column_char()); else str = NULL; if((!str && *ptr) || (str && ptr < str)) { fl_font(font, tsize); int hh = fl_height(); if (hh > hmax) hmax = hh; } if (!str || !*str) break; } } return hmax; // previous version returned hmax+2! } int Fl_Browser::item_width(void* v) const { char* str = ((FL_BLINE*)v)->txt; const int* i = column_widths(); int ww = 0; while (*i) { // add up all tab-separated fields char* e; e = strchr(str, column_char()); if (!e) break; // last one occupied by text str = e+1; ww += *i++; } // OK, we gotta parse the string and find the string width... int tsize = textsize(); Fl_Font font = textfont(); int done = 0; while (*str == format_char_ && str[1] && str[1] != format_char_) { str ++; switch (*str++) { case 'l': case 'L': tsize = 24; break; case 'm': case 'M': tsize = 18; break; case 's': tsize = 11; break; case 'b': font = (Fl_Font)(font|FL_BOLD); break; case 'i': font = (Fl_Font)(font|FL_ITALIC); break; case 'f': case 't': font = FL_COURIER; break; case 'B': case 'C': strtol(str, &str, 10); break;// skip a color number case 'F': font = (Fl_Font)strtol(str, &str, 10); break; case 'S': tsize = strtol(str, &str, 10); break; case '.': done = 1; break; case '@': str--; done = 1; } if (done) break; } if (*str == format_char_ && str[1]) str ++; fl_font(font, tsize); return ww + int(fl_width(str)) + 6; } int Fl_Browser::full_height() const { return full_height_; } int Fl_Browser::incr_height() const { return textsize()+2; } void Fl_Browser::item_draw(void* v, int X, int Y, int W, int H) const { char* str = ((FL_BLINE*)v)->txt; const int* i = column_widths(); while (W > 6) { // do each tab-separated field int w1 = W; // width for this field char* e = 0; // pointer to end of field or null if none if (*i) { // find end of field and temporarily replace with 0 e = strchr(str, column_char()); if (e) {*e = 0; w1 = *i++;} } int tsize = textsize(); Fl_Font font = textfont(); Fl_Color lcol = textcolor(); Fl_Align talign = FL_ALIGN_LEFT; // check for all the @-lines recognized by XForms: //#warning FIXME This maybe needs to be more UTF8 aware now...? while (*str == format_char() && *++str && *str != format_char()) { switch (*str++) { case 'l': case 'L': tsize = 24; break; case 'm': case 'M': tsize = 18; break; case 's': tsize = 11; break; case 'b': font = (Fl_Font)(font|FL_BOLD); break; case 'i': font = (Fl_Font)(font|FL_ITALIC); break; case 'f': case 't': font = FL_COURIER; break; case 'c': talign = FL_ALIGN_CENTER; break; case 'r': talign = FL_ALIGN_RIGHT; break; case 'B': if (!(((FL_BLINE*)v)->flags & SELECTED)) { fl_color((Fl_Color)strtol(str, &str, 10)); fl_rectf(X, Y, w1, H); } else strtol(str, &str, 10); break; case 'C': lcol = (Fl_Color)strtol(str, &str, 10); break; case 'F': font = (Fl_Font)strtol(str, &str, 10); break; case 'N': lcol = FL_INACTIVE_COLOR; break; case 'S': tsize = strtol(str, &str, 10); break; case '-': fl_color(FL_DARK3); fl_line(X+3, Y+H/2, X+w1-3, Y+H/2); fl_color(FL_LIGHT3); fl_line(X+3, Y+H/2+1, X+w1-3, Y+H/2+1); break; case 'u': case '_': fl_color(lcol); fl_line(X+3, Y+H-1, X+w1-3, Y+H-1); break; case '.': goto BREAK; case '@': str--; goto BREAK; } } BREAK: fl_font(font, tsize); if (((FL_BLINE*)v)->flags & SELECTED) lcol = fl_contrast(lcol, selection_color()); if (!active_r()) lcol = fl_inactive(lcol); fl_color(lcol); fl_draw(str, X+3, Y, w1-6, H, e ? Fl_Align(talign|FL_ALIGN_CLIP) : talign, 0, 0); if (!e) break; // no more fields... *e = column_char(); // put the separator back X += w1; W -= w1; str = e+1; } } static const int no_columns[1] = {0}; /** The constructor makes an empty browser. \param[in] X,Y,W,H position and size. \param[in] L label string, may be NULL. */ Fl_Browser::Fl_Browser(int X, int Y, int W, int H, const char *L) : Fl_Browser_(X, Y, W, H, L) { column_widths_ = no_columns; lines = 0; full_height_ = 0; cacheline = 0; format_char_ = '@'; column_char_ = '\t'; first = last = cache = 0; } /** Updates the browser so that \a line is shown at position \a pos. \param[in] line line number. \param[in] pos position. */ void Fl_Browser::lineposition(int line, Fl_Line_Position pos) { if (line<1) line = 1; if (line>lines) line = lines; int p = 0; FL_BLINE* l; for (l=first; l && line>1; l = l->next) { line--; p += item_height(l); } if (l && (pos == BOTTOM)) p += item_height (l); int final = p, X, Y, W, H; bbox(X, Y, W, H); switch(pos) { case TOP: break; case BOTTOM: final -= H; break; case MIDDLE: final -= H/2; break; } if (final > (full_height() - H)) final = full_height() -H; position(final); } /** Returns the current top line in the browser. If there is no vertical scrollbar then this will always return 1. */ int Fl_Browser::topline() const { return lineno(top()); } /** Removes all the lines in the browser. */ void Fl_Browser::clear() { for (FL_BLINE* l = first; l;) { FL_BLINE* n = l->next; free(l); l = n; } full_height_ = 0; first = 0; last = 0; lines = 0; new_list(); } /** Adds a new line to the end of the browser. The text is copied using the strdup() function. It may also be NULL to make a blank line. The void * argument \a d is returned as the data() of the new item. */ void Fl_Browser::add(const char* newtext, void* d) { insert(lines+1, newtext, d); //Fl_Browser_::display(last); } /** Returns data entry for line \p line, or NULL if out of range. */ const char* Fl_Browser::text(int line) const { if (line < 1 || line > lines) return 0; return find_line(line)->txt; } /** Returns the data for line \p line, or NULL if \p line is out of range. */ void* Fl_Browser::data(int line) const { if (line < 1 || line > lines) return 0; return find_line(line)->data; } /** Sets the selection state of entry \a line. \param[in] line line number. \param[in] v new selection state. \returns 1 if the state changed, 0 if not. */ int Fl_Browser::select(int line, int v) { if (line < 1 || line > lines) return 0; return Fl_Browser_::select(find_line(line), v); } /** Returns 1 if line \a line is selected, 0 if it is not selected.*/ int Fl_Browser::selected(int line) const { if (line < 1 || line > lines) return 0; return find_line(line)->flags & SELECTED; } /** Makes line \a line visible for selection. */ void Fl_Browser::show(int line) { FL_BLINE* t = find_line(line); if (t->flags & NOTDISPLAYED) { t->flags &= ~NOTDISPLAYED; full_height_ += item_height(t); if (Fl_Browser_::displayed(t)) redraw(); } } /** Makes line \a line invisible, preventing selection by the user. The line can still be selected under program control. */ void Fl_Browser::hide(int line) { FL_BLINE* t = find_line(line); if (!(t->flags & NOTDISPLAYED)) { full_height_ -= item_height(t); t->flags |= NOTDISPLAYED; if (Fl_Browser_::displayed(t)) redraw(); } } /** For back compatibility. This calls show(line) if \a v is true, and hide(line) otherwise. \see show(int line), hide(int line) */ void Fl_Browser::display(int line, int v) { if (line < 1 || line > lines) return; if (v) show(line); else hide(line); } /** Returns a non-zero value if line \a line is visible. */ int Fl_Browser::visible(int line) const { if (line < 1 || line > lines) return 0; return !(find_line(line)->flags&NOTDISPLAYED); } /** Gets the browser's value. \returns line number of current selection, or 0 if no selection. */ int Fl_Browser::value() const { return lineno(selection()); } /** Swap two lines, \p a and \p b */ void Fl_Browser::swap(FL_BLINE *a, FL_BLINE *b) { if ( a == b || !a || !b) return; // nothing to do swapping(a, b); FL_BLINE *aprev = a->prev; FL_BLINE *anext = a->next; FL_BLINE *bprev = b->prev; FL_BLINE *bnext = b->next; if ( b->prev == a ) { // A ADJACENT TO B if ( aprev ) aprev->next = b; else first = b; b->next = a; a->next = bnext; b->prev = aprev; a->prev = b; if ( bnext ) bnext->prev = a; else last = a; } else if ( a->prev == b ) { // B ADJACENT TO A if ( bprev ) bprev->next = a; else first = a; a->next = b; b->next = anext; a->prev = bprev; b->prev = a; if ( anext ) anext->prev = b; else last = b; } else { // A AND B NOT ADJACENT // handle prev's b->prev = aprev; if ( anext ) anext->prev = b; else last = b; a->prev = bprev; if ( bnext ) bnext->prev = a; else last = a; // handle next's if ( aprev ) aprev->next = b; else first = b; b->next = anext; if ( bprev ) bprev->next = a; else first = a; a->next = bnext; } // Disable cache -- we played around with positions cacheline = 0; cache = 0; } /** Swaps two lines in the browser.*/ void Fl_Browser::swap(int ai, int bi) { if (ai < 1 || ai > lines || bi < 1 || bi > lines) return; FL_BLINE* a = find_line(ai); FL_BLINE* b = find_line(bi); swap(a,b); } // // End of "$Id$". //