#include "../include/xcv_license.h" //PCH namespace Widgets95 {//-. //<-' static const int N = 15; //HACK: Static column limit. extern void xcv_list_scrollbar_callback(ui::bar *sb) { ui::listbox *my = (ui::listbox*)sb->associated_object; //if(!my->scrollbar) return; if(!my||sb!=my->scrollbar){ assert(0); return; } int i = sb->int_val(); if(i!=my->_start_line) { my->_start_line = i; my->redraw(); } } /****************************** WIDGETS_95_List::mouse_down_handler() **********/ bool ui::listbox::_mouse_down_handler(int local_x, int local_y) { int prev = _curr_line; //TODO: Spacebar should set click_et so it is able to //double-click normally. int line; if(local_x==-1) //2019: spacebar_mouse_click? { line = _curr_line; //REMOVE ME if(_clicked=!_clicked) //start over counting? { _curr_line = ~_curr_line; //dbl } } else { if(!box_test(local_x,local_y)) return true; _clicked = true; //needed for double spacebar click line = find_line(local_x-_x_abs,local_y-_y_abs); if(line<=-1||line>=_num_lines) //if(-1==line) { return false; } } bool dbl = _curr_line==line; _curr_line = line; if(dbl&&e.double_click_millis) { //REMOVE ME if(local_x!=-1) //spacebar_mouse_click? if(2!=e::get_click()) dbl = false; //! } int curr_impl = 0; int prev_impl = prev==line; bool unsel = ~e.curr_modifiers&GLUT_ACTIVE_CTRL; bool shift = e.curr_modifiers&GLUT_ACTIVE_SHIFT; bool cmp = false; listbar *lb = find_listbar(); if(item*it=find_line_ptr(_curr_line)) { int impl = it->impl(item::impl_features); //This is in case the callback needs to //analyze before and after state. prev_impl|=impl&it->impl(); bool cb = false; if(item::impl_checkbox&impl) { //NOTE: Shift can't be used with the //keyboard without remembering where //the last click was. Ctrl+Shift can //maybe automatically select so that //it's very close to Shift selecting. if(local_x!=-1||shift&&unsel) cb = local_x-_x_abs<20; } if(item::impl_multisel&impl) { if(cb) { bool check = !it->checkbox(); if(it->multisel()) { operator^([check](li::multisel ea) { ea->check(check); }); } else it->check(check); } else if(shift) { _curr_line = prev; //2021 int a = prev; int b = line; if(a>b) std::swap(a,b); int i = 0; operator^([&](li::allitems ea) { if(i>=a&&i<=b) { ea->select(); } else if(unsel) ea->unselect(); i++; }); } else { if(unsel) { //Not efficient but don't know how else //to implement this. operator^([](li::multisel ea) { ea->unselect(); }); } it->select(item::impl_xor); } } else if(cb) it->check(item::impl_xor); //2022: The checkbox is an independent element //but anyway, if the user callback auto checks //boxes then if they receive the callback then //they'll "double check" (undoing the checks.) //and they can't reliably detect this scenario. if(cb) //return false; { _curr_line = prev; return false; } curr_impl = 1|(impl&it->impl()); bool id = false; switch(int s=es()&es_live_item) { case 0: if(lb) case es_live_id: { id = true; item: cmp = _int_val!=it->_id; } else default: { cmp = _text!=it->_text; if(!cmp&&s==es_live_item) goto item; } if(cmp) { e.set_edited(this); //OPTIMIZING //This way update_live isn't called. //REMINDER: Need a non-0-terminator copy. if(id) stage_live(&it->_id); if(!id) stage_live(&it->_text); if(s==es_live_item) _int_val = it->_id; }} } else //??? { prev_impl = 0; //??? cmp = !_text.empty()||_int_val||_float_val; if(cmp) e.set_edited(this); if(cmp) stage_live(""); } if(cmp) output_live(); //NOTE: output_live() has output_callback, and //checkboxes and even multi-selection can also //implement callback logic, but I feel like it //is too much to include them in this facility. e.prev_impl_get = prev_impl; e.curr_impl_get = curr_impl; //Inside callbacks the listbar momentarily has //the column. It can't use set_int_val in this //case. Maybe text/float_val should agree, but //I feel like probably not. Note this could be //stored in "e" too, but header code refers to //the header to get the column, so it tends to //look better written that way. int col = 0; if(lb) { int cx = local_x-_x_abs-_x_lr; int sep = lb->_x_off; for(control*ch=lb->first_child();ch;ch=ch->next()) { if(!ch->_hidden) { cx-=ch->_w; if(cx<=0) break; cx-=sep; } col++; } std::swap(col,lb->_int_val); } //HACK: Letting set_click be used to suppress regular //callbacks. //if(associated_object //??? //Necessary? if(_object_cb //2022: Same logic as below... //??? ||_cb_click_type!=double_click||dbl) { execute_callback(); } //if(associated_object) //Necessary? if(_object_cb) { //2022: 0 is required simply because otherwise there //would be no way to access _object_cb without a new //dedicated click setting. 0 enables arrow key based //callbacks. if(_cb_click_type==0||_cb_click_type==(dbl?2:1)) { _object_cb(this); } } if(lb) std::swap(col,lb->_int_val); //NOTE: Technically maybe this should be saved //and restored in case of modal dialogs. But I //think that code would typically not refer to //them on the backside of the dialogs. e.curr_impl_get = e.prev_impl_get = 0; redraw(); return false; } /****************************** WIDGETS_95_List::draw() **********/ void ui::listbox::_draw() { box_interface::_draw(); item *it = first_item(); if(!it) return; int impl = it->impl(item::impl_features); bool ms = (item::impl_multisel&impl)!=0; bool cb = (item::impl_checkbox&impl)!=0; int lh = _font.height; //15 int lh2 = lh+2; //if(lh2%2) lh2++; //Odd squares focus rect. assert(0==lh2%2); int box_w = box_span(); int text_x = _x_lr+ui_boxinnermarginx; int y = _y_off_top+ui_boxinnermarginx-2; int text_xx = text_x+box_w-1; //2022: Adjust blue fill to square focus rect. if((text_xx-text_x)%2==0) text_xx--; listbar *lb = find_listbar(); if(lb&&!lb->_hidden) y+=lb->_h; //enum{ N=15 }; int n,columns[N+1]; if(lb) { //5 is to indent inner columns less than //the first, since it needs room for the //margin. columns[0] = text_x-5; int sep = lb->_x_off; control *ch = lb->first_child(); for(n=0;ch&&n++next()) { int w = ch->_hidden?0:ch->_w+sep; columns[n] = columns[n-1]+w; if(columns[n]>=box_w) break; } } else n = 1; columns[n] = box_w; columns[0] = text_x; //First column has a margin. //NOTE: 14 is designed to line up the 1st column //text with the label of a checkbox dropped into //the column's button. if(cb) columns[0]+=14; //HACK: Trying to fudge multi-selection's pixels. int active_top = 0, active_bot = this==e::get_active()||scrollbar==e::get_active(); const int curr_line = _curr_line; const int first_line = _start_line; const int last_line = _start_line+_visible_lines-1; int black = _enabled?0:64; for(int line=0;it;line++,it=it->next()) if(line>=first_line&&line<=last_line) { int c = black; if(line==curr_line) { if(active_bot) { //REMINDER: y+1 assumed below. active_top+=y+1; active_bot = y+lh2+2; } if(!ms) goto white; } if(ms&&it->multisel()) white: { c = 255; //white /** Draw selection area dark **/ int top = y+3, bot = y+lh2; //YUCK: This makes room for the dotted //line between two selected list items. if(active_bot) switch(line-curr_line) { case -1: active_top++; if(curr_line<=last_line) if(!it->next()->multisel()) { //Just focus-rect can be skinny. active_top++; } else bot--; break; case 0: //Current line? //HACK: Indicates active_rect top adjusted. if(active_top>y+1) top++; if(linenext()&&it->next()->multisel()) bot--; break; case 1: active_bot--; if(curr_line>=first_line) if(!it->prev()->multisel()) { //Just focus-rect can be skinny. active_bot--; } else top++; break; } window::draw_filled_rect(text_x,top,text_xx,bot,BOX_ACTIVE); } if(cb) { int i; if(it->checkbox()) { i = _enabled?bitmap::check_on:bitmap::check_on_dis; } else i = _enabled?bitmap::check_off:bitmap::check_off_dis; xcv_bitmaps[i]->draw(text_x+2,y+5); } window::draw_color(c); const char *row[N+1]; const char *e = it->_text.c_row(n+1,row); for(int i=0;i=e) break; int col_end = columns[i+1]; window::begin_to_draw_str(x+1,y+4); for(int i=0;text[i];i++) { x+=chr_span(text[i]); //if(x>=box_w) break; if(x>=col_end) break; //TODO: 0-terminate the text buffer. //glutBitmapCharacter(font(),text[i]); window::draw_chr(text[i]); } } y+=lh; } //Finally: Draw multi-select adjusted dotted focus-rect. if(active_top>1) window::draw_active_rect(text_x-2,active_top,text_xx+2,active_bot,false); } /********************************* WIDGETS_95_List::find_line() **********/ int ui::listbox::find_line(int x, int y) { y-=_y_off_top+ui_boxinnermarginx; if(listbar*lb=find_listbar()) if(!lb->_hidden) y-=lb->_h; return _start_line+y/_font.height; //15 } /********************************* WIDGETS_95_List::special_handler() **********/ bool ui::listbox::_key_handler(int key, int modifiers) { //if(key!=CTRL('a')) return true; if('a'==key&&GLUT_ACTIVE_CTRL&modifiers) { bool shift = modifiers&GLUT_ACTIVE_SHIFT; operator^([shift](li::allitems ea) { if(shift) ea->unselect(); else ea->select(); }); redraw(); return false; } return true; } bool ui::listbox::_special_handler(int key, int modifiers) { int cmp = _curr_line, cmp2 = _start_line; //2022 if(key==GLUT_KEY_DOWN) { if(_curr_line<_num_lines-1) { _curr_line++; if(_curr_line>=_start_line+_visible_lines) _start_line++; } _clicked = false; } else if(key==GLUT_KEY_UP) { if(_curr_line>0) { _curr_line--; if(_curr_line<_start_line) _start_line--; } _clicked = false; } else return true; //signal keycode unprocessed if(cmp==_curr_line) return true; //2022 //Letting Ctrl+Shift select items similar to //Shift+click. Note, execute_callback is not //done. Shift+Space checks checkboxes. if(modifiers&GLUT_ACTIVE_SHIFT&&modifiers&GLUT_ACTIVE_CTRL) { item *i = first_item(); if(i->impl(i->impl_features)&i->impl_multisel) if(i=find_line_ptr()) { //NOTE: The second select just covers the //item that was initially outlined, so it //simulates Shift clicking more correctly. i->select(); if(i=key==GLUT_KEY_DOWN?i->prev():i->next()) i->select(); } } if(cmp2!=_start_line) { //I tried to fix this, but there is a _start_line? //if(scrollbar) scrollbar->set_int_val(_curr_line); //if(scrollbar) scrollbar->show_item(_curr_line,visible_lines,false); if(scrollbar) scrollbar->set_int_val(_start_line); } if(_cb_click_type==0) execute_callback(); //2022 redraw(); return false; //return true; //2019 } /************************************ WIDGETS_95_List::update_area() **********/ void ui::listbox::_update_area() { bar *sb = scrollbar_init(xcv_list_scrollbar_callback); int sb2 = sb&&!sb->_hidden?ui_bar_arrow_size:0; int lbw = 0; listbar *lb = find_listbar(); control *lc = lb?lb->last_child():nullptr; while(lc&&lc->_hidden) lc = lc->prev(); if(control*ch=lc) { int x = lb->_x_off; ch->_w = ui_button_span; for(;ch;ch=ch->prev()) if(!ch->_hidden) { ch->update_area(); //YUCK lbw+=ch->_w+x; } lbw-=x; } box_interface::_update_area(); int mw = box_span(0); int cmp = std::max(lbw,ui_min_text_span); if(mw_hidden) ih-=lb->_h; _visible_lines = ih/lh; //2019: Rounding up to lines count. _h = _visible_lines*lh; offset_dims(nullptr,&_h); _h+=ui_boxinnermarginx*2; if(lb&&!lb->_hidden) _h+=lb->_h; } if(sb) sb->set_range (std::max(_num_lines-_visible_lines,0),0); if(lb) //HACK: Position the listbar. { lb->pos("abs"); lb->_x_abs = _x_abs+_x_lr; lb->_y_abs = _y_abs+_y_off_top; lb->_w = mw-2; if(lc) { //REMINDER: Can't rely on lc->_x_abs to be computed. int cmp = lb->_w-(lbw-lc->_w); if(std::abs(cmp-lc->_w)>1) //Odds squares focus rect. { if(cmp%2==0) cmp--; //Odds squares focus rect. lc->_w = cmp; } } } } /************************************ WIDGETS_95_List::update_area() **********/ void ui::listbox::_update_live() { //NOTE: I'm mainly keeping this code here because it //already existed, and I don't want to just scrap it. if(~es()&es_update_live) return; int line; switch(es()&es_live_item) { case 0: if(find_listbar()) { case es_live_id: case es_live_item: //Matching unique ID. line = find_line_num(_int_val); } else { //Note: Matching 0th column by design. case es_live_text: line = find_line_num(_text); } } if(line==-1) return; while(line>_start_line+_visible_lines) _start_line++; if(line<_start_line) _start_line--; _curr_line = line; } /**************************************** WIDGETS_95_Listbox::add_item() **********/ bool ui::listbox::_add_item(item *new_node) { if(!new_node) return false; if(new_node->_parent_node) { //Not sure how to properly remove this item? if(&_items_list!=new_node->_parent_node) return false; _num_lines--; //HACK } new_node->_link_this_to_parent_last(&_items_list); _num_lines++; _post_repack(); return true; } /************************************** WIDGETS_95_Listbox::delete_item() **********/ bool ui::listbox::_delete_item(node *ptr) { if(!ptr) return false; if(ptr->_parent_node==&_items_list) { int ln = find_line_num((item*)ptr); //NEW //node->_unlink(); delete node; ptr->_delete(); _num_lines--; if(ln<_start_line) _start_line--; if(ln<_curr_line) _curr_line--; } else if(ptr==&_items_list) { _delete_all(); _num_lines = _curr_line = _start_line = 0; } else return false; if(bar*sb=scrollbar) //NEW { if(_start_line!=sb->_int_val) sb->set_int_val(_start_line); sb->set_range(std::max(_num_lines-_visible_lines,0),0); } return true; } //////////// INTERLINK SYSTEM /////////////////////// static const char *xcv_listbox_column (const char* &p, li::item *it, int j) { //enum{ N=15 }; assert(j=N) return 0; const char *e,*row[N+1]; e = it->text().c_row(j+2,row); for(int jj=0;++jj<=j;) { //YUCK: Must rule out improperly //formatted string. if(row[jj]==e+1) return 0; } p = row[j]; assert(!row[j+1][-1]); return row[j+1]; } struct xcv_listbox_interlink : interlink { void *item; int subitem, dock; xcv_listbox_interlink (ui::listbox *f, ui::control *s):interlink(f,s){} virtual c_bool operator()(c_bool cmp) { cmp&=dock; auto f = (ui::listbox*)first; auto s = second; int did = 0; if(cmp&(v|c)) { int compile[_paste==v]; (void)compile; //HACK: The void-pointer is considered to match //if the same pointer remains present. I suppose //it could be reclaimed memory. It seems like the //best that can be done. //TODO: Perhaps _delete_item should check _docked //and scan for and remove the item then. Deleting //is not an optimal operation. li::item *i = f->find_item_ptr((li::item*)item); if(!i) return 0; const char *pp,*p,*e = xcv_listbox_column(p,i,subitem); if(cmp&v) //Paste? { string &t = i->_text; pp = i->_text.c_str(); t.erase(p-pp,e-p); size_t v = s->_text.size(); if(size_t(p-pp)c_str(),v); } else second->set_text(p); //Copy? did|=v|c; } return cmp&did; } }; c_bool ui::listbox::_dock(control *c, int line, int col, c_bool dock) { xcv_listbox_interlink *l; int all_implemented = l->m|l->i|l->c|l->v; if(!c) return all_implemented; switch(dock) //Defaults? { case 0: dock = l->m; break; case 1: dock = all_implemented; break; } listbar *lb = find_listbar(); if(line==-1) line = _curr_line; if(col==-1) col = lb&&e.curr_impl_get?lb->_int_val:0; const int i = line; const int j = std::max(0,col); if(l->i) { if(j!=col||i<0||i>=_num_lines) return 0; } item *it = nullptr; const char *p = nullptr; if(dock&(l->c|l->i)) { it = find_line_ptr(i); if(!it) return 0; if(dock&l->c) //Copy? xcv_listbox_column(p,it,j); } //NOTICE: Trying to not disturb c if an error //occurs. Can it return a partial result? if(dock&l->m) //Move? { //TODO: Have xcv_listbox_interlink implement //this so it can track a scrollbar. int m[4]; //active_area(m,ui_boxinnermarginx); //FIX ME //What about left-aligned children? m[0] = _x_abs+_x_lr+ui_boxinnermarginx; m[3] = _font.height; m[1] = _y_abs+_y_off_top+ui_boxinnermarginx; m[1]+=m[3]*(line-_start_line); m[2] = box_span(); if(!j) //Checkbox? { if(!it) it = first_item(); if(it&&item::impl_checkbox&it->impl(item::impl_features)) { m[0]+=18; m[2]-=18; } } if(lb) { if(!lb->_hidden) m[1]+=lb->_h; control *ch = lb->first_child(); while(col-->0) ch = ch->next(); ////POINT-OF-NO-RETURN//// if(!ch) return 0; int x = ch->_x_abs; m[2]+=m[0]; m[0] = std::max(m[0],x); m[2] = std::min(m[2]-x,x+ch->_w-m[0]); } m[0]--; m[2]+=3; //BOX_WHITE_ON_WHITE? m[1]--; m[3]+=3; memcpy(c->area(),m,sizeof(m)); } if(p) c->set_text(p); //Copy? if(l->i) //Interlink? { l = new xcv_listbox_interlink(this,c); l->item = it; l->subitem = j; l->dock = dock; } return dock; } //---. }//<-'