]> git.tdb.fi Git - libs/gltk.git/blob - source/list.cpp
Use size_t to represent counts and indices
[libs/gltk.git] / source / list.cpp
1 #include <msp/core/raii.h>
2 #include <msp/debug/demangle.h>
3 #include <msp/gl/matrix.h>
4 #include <msp/gl/meshbuilder.h>
5 #include <msp/input/keys.h>
6 #include <msp/strings/format.h>
7 #include "graphic.h"
8 #include "list.h"
9 #include "part.h"
10 #include "style.h"
11 #include "text.h"
12
13 using namespace std;
14
15 namespace Msp {
16 namespace GLtk {
17
18 incompatible_data::incompatible_data(const type_info &ti):
19         logic_error("expected "+Debug::demangle(ti.name()))
20 { }
21
22
23 List::List():
24         List(*new BasicListData<string>)
25 {
26         own_data = true;
27 }
28
29 List::List(ListData &d):
30         data(&d),
31         observer(new DataObserver(*this))
32 {
33         input_type = INPUT_NAVIGATION;
34
35         add(slider);
36         slider.set_step(1);
37         slider.signal_value_changed.connect(sigc::mem_fun(this, &List::slider_value_changed));
38 }
39
40 List::~List()
41 {
42         delete item_factory;
43         delete observer;
44         if(own_data)
45                 delete data;
46 }
47
48 void List::autosize_special(const Part &part, Geometry &ageom) const
49 {
50         if(part.get_name()=="items")
51         {
52                 const Sides &margin = part.get_margin();
53
54                 unsigned items_w = 0;
55                 unsigned items_h = 0;
56                 for(size_t i=0; i<items.size(); ++i)
57                 {
58                         Geometry igeom;
59                         items[i]->autosize(igeom);
60                         items_w = max(items_w, igeom.w);
61                         items_h = max(items_h, igeom.h);
62                 }
63
64                 if(view_mode==GRID)
65                 {
66                         unsigned r = view_rows;
67                         unsigned c = view_columns;
68                         if(r==0 && c==0)
69                                 r = sqrt(items.size());
70                         if(r==0)
71                                 r = (items.size()+c-1)/c;
72                         if(c==0)
73                                 c = (items.size()+r-1)/r;
74                         items_w *= c;
75                         items_h *= r;
76                 }
77                 else
78                         items_h *= (view_rows==0 ? items.size() : view_rows);
79
80                 ageom.w = max(ageom.w, items_w+margin.left+margin.right);
81                 ageom.h = max(ageom.h, items_h+margin.top+margin.bottom);
82         }
83         else if(part.get_name()=="slider")
84                 autosize_child(slider, part, ageom);
85 }
86
87 void List::set_data(ListData &d)
88 {
89         if(item_factory)
90                 item_factory->set_data(d);
91
92         delete observer;
93         if(own_data)
94                 delete data;
95
96         data = &d;
97         own_data = false;
98         observer = new DataObserver(*this);
99
100         for(Item *i: items)
101                 delete i;
102         items.clear();
103         size_t n_items = data->size();
104         for(size_t i=0; i<n_items; ++i)
105         {
106                 Item *item = create_item(i);
107                 items.push_back(item);
108         }
109
110         items_changed();
111 }
112
113 void List::items_changed()
114 {
115         signal_autosize_changed.emit();
116         mark_rebuild();
117 }
118
119 List::Item *List::create_item(size_t index)
120 {
121         Item *item = nullptr;
122         if(item_factory)
123                 item = item_factory->create_item(index);
124         else
125                 item = new BasicItem(data->get_string(index));
126         if(index==sel_index)
127                 item->set_active(true);
128         add(*item);
129         item->autosize();
130         item->signal_autosize_changed.connect(sigc::bind(sigc::mem_fun(this, &List::item_autosize_changed), item));
131         return item;
132 }
133
134 void List::set_view_size(unsigned s)
135 {
136         set_view_size(s, s);
137 }
138
139 void List::set_view_size(unsigned r, unsigned c)
140 {
141         view_rows = r;
142         view_columns = c;
143         signal_autosize_changed.emit();
144 }
145
146 void List::set_view_all()
147 {
148         set_view_size(0);
149 }
150
151 void List::set_selected_index(size_t i)
152 {
153         if(i>=data->size() && i!=INVALID_INDEX)
154                 throw out_of_range("List::set_selected_index");
155
156         if(i==sel_index)
157                 return;
158
159         if(sel_index!=INVALID_INDEX)
160                 items[sel_index]->set_active(false);
161
162         sel_index = i;
163         focus_index = i;
164         if(i==INVALID_INDEX)
165         {
166                 set_input_focus(nullptr);
167                 signal_selection_cleared.emit();
168         }
169         else
170         {
171                 items[sel_index]->set_active(true);
172                 if(state&FOCUS)
173                         set_input_focus(items[focus_index]);
174                 signal_item_selected.emit(sel_index);
175         }
176 }
177
178 void List::set_selected_item(Widget *item)
179 {
180         for(size_t i=rows[first_row].first; (i<items.size() && items[i]->is_visible()); ++i)
181                 if(item==items[i])
182                         return set_selected_index(i);
183 }
184
185 void List::rebuild_special(const Part &part)
186 {
187         if(part.get_name()=="slider")
188                 reposition_child(slider, part);
189         else if(part.get_name()=="items")
190         {
191                 SetFlag flag(ignore_slider_change);
192                 reposition_items(true);
193                 size_t old_first_row = first_row;
194                 size_t old_max_scroll = max_scroll;
195                 check_view_range();
196                 if(first_row!=old_first_row || max_scroll!=old_max_scroll)
197                         reposition_items(false);
198         }
199
200         Widget::rebuild_special(part);
201 }
202
203 void List::render_special(const Part &part, GL::Renderer &renderer) const
204 {
205         if(part.get_name()=="items")
206         {
207                 for(size_t i=rows[first_row].first; (i<items.size() && items[i]->is_visible()); ++i)
208                         items[i]->render(renderer);
209         }
210         else if(part.get_name()=="slider")
211                 slider.render(renderer);
212 }
213
214 bool List::key_press(unsigned key, unsigned mod)
215 {
216         if(key==Input::KEY_UP && mod==MOD_CTRL)
217                 move_focus(NAV_UP, false);
218         else if(key==Input::KEY_DOWN && mod==MOD_CTRL)
219                 move_focus(NAV_DOWN, false);
220         else
221                 return false;
222
223         return true;
224 }
225
226 void List::button_press(int x, int y, unsigned btn)
227 {
228         if(btn==4 || btn==5)
229         {
230                 size_t change = 3;
231                 if(btn==4)
232                 {
233                         change = min(first_row, change);
234                         slider.set_value(max_scroll-(first_row-change));
235                 }
236                 else if(btn==5)
237                 {
238                         change = min(max_scroll-first_row, change);
239                         slider.set_value(max_scroll-(first_row+change));
240                 }
241         }
242         else
243         {
244                 Container::button_press(x, y, btn);
245                 if(click_focus && btn==1)
246                         set_selected_item(click_focus);
247         }
248 }
249
250 void List::touch_press(int x, int y, unsigned finger)
251 {
252         if(finger==0)
253         {
254                 dragging = true;
255                 drag_start_x = x;
256                 drag_start_y = y;
257         }
258 }
259
260 void List::touch_release(int x, int y, unsigned finger)
261 {
262         if(finger==0)
263         {
264                 int dx = x-drag_start_x;
265                 int dy = y-drag_start_y;
266                 if(dx*dx+dy*dy<25)
267                 {
268                         Container::touch_press(drag_start_x, drag_start_y, finger);
269                         if(touch_focus)
270                                 set_selected_item(touch_focus);
271                         Container::touch_motion(x, y, finger);
272                         Container::touch_release(x, y, finger);
273                 }
274                 dragging = false;
275         }
276 }
277
278 void List::touch_motion(int, int y, unsigned finger)
279 {
280         if(finger==0 && !items.empty() && dragging)
281         {
282                 int dy = y-drag_start_y;
283                 if(dy>0 && first_row<max_scroll)
284                 {
285                         int row_h = rows[first_row].height;
286                         if(dy>row_h)
287                         {
288                                 drag_start_y += row_h;
289                                 slider.set_value(max_scroll-(first_row+1));
290                         }
291                 }
292                 else if(dy<0 && first_row>0)
293                 {
294                         int row_h = rows[first_row-1].height;
295                         if(-dy>row_h)
296                         {
297                                 drag_start_y -= row_h;
298                                 slider.set_value(max_scroll-(first_row-1));
299                         }
300                 }
301         }
302 }
303
304 void List::focus_in()
305 {
306         Container::focus_in();
307         if(focus_index!=INVALID_INDEX && items[focus_index]->is_visible())
308                 set_input_focus(items[focus_index]);
309         else
310         {
311                 if(sel_index!=INVALID_INDEX && items[sel_index]->is_visible())
312                         set_focus_index(sel_index);
313                 else if(!items.empty())
314                         set_focus_index(rows[first_row].first);
315         }
316 }
317
318 bool List::navigate(Navigation nav)
319 {
320         if((nav==NAV_UP || nav==NAV_DOWN || ((nav==NAV_LEFT || nav==NAV_RIGHT) && view_mode==GRID)) && !items.empty())
321                 move_focus(nav, true);
322         else if(nav==NAV_ACTIVATE)
323                 set_selected_index(focus_index);
324         else
325                 return false;
326
327         return true;
328 }
329
330 void List::on_style_change()
331 {
332         items_part = (style ? style->find_part("items") : nullptr);
333 }
334
335 void List::move_focus(Navigation nav, bool select)
336 {
337         if(nav==NAV_UP && view_mode==GRID)
338         {
339                 size_t row = item_index_to_row(focus_index);
340                 if(row>0)
341                         set_focus_index(rows[row-1].first+focus_index-rows[row].first);
342                 else
343                         set_focus_index(0);
344         }
345         else if(nav==NAV_DOWN && view_mode==GRID)
346         {
347                 size_t row = item_index_to_row(focus_index);
348                 if(row+1<rows.size())
349                         set_focus_index(rows[row+1].first+focus_index-rows[row].first);
350                 else
351                         set_focus_index(items.size()-1);
352         }
353         else if(nav==NAV_UP || (nav==NAV_LEFT && view_mode==GRID))
354         {
355                 if(focus_index>0)
356                         set_focus_index(focus_index-1);
357         }
358         else if(nav==NAV_DOWN || (nav==NAV_RIGHT && view_mode==GRID))
359         {
360                 if(focus_index+1<items.size())
361                         set_focus_index(focus_index+1);
362         }
363
364         if(select)
365                 set_selected_index(focus_index);
366 }
367
368 void List::set_focus_index(size_t i)
369 {
370         focus_index = i;
371         if(focus_index!=INVALID_INDEX)
372         {
373                 scroll_to_focus();
374                 if(state&FOCUS)
375                         set_input_focus(items[focus_index]);
376         }
377 }
378
379 void List::item_autosize_changed(Item *item)
380 {
381         item->autosize();
382         signal_autosize_changed.emit();
383         mark_rebuild();
384 }
385
386 void List::reposition_items(bool record_rows)
387 {
388         if(!items_part)
389                 return;
390
391         if(record_rows)
392         {
393                 rows.clear();
394                 rows.push_back(0);
395         }
396
397         const Sides &margin = items_part->get_margin();
398         unsigned view_w = geom.w-min(geom.w, margin.left+margin.right);
399         unsigned x = 0;
400         unsigned y = 0;
401         unsigned row_h = 0;
402         for(size_t i=0; i<items.size(); ++i)
403         {
404                 const Geometry &igeom = items[i]->get_geometry();
405
406                 if(view_mode!=GRID || (x>0 && x+igeom.w>view_w))
407                 {
408                         x = 0;
409                         if(y)
410                                 y -= row_h;
411                         if(record_rows && i>0)
412                         {
413                                 rows.back().height = row_h;
414                                 rows.push_back(i);
415                         }
416                         row_h = 0;
417                 }
418
419                 if(first_row<rows.size() && i==rows[first_row].first)
420                         y = geom.h-min(geom.h, margin.top);
421
422                 if(!y)
423                         items[i]->set_visible(false);
424                 else if(igeom.h+margin.bottom<=y)
425                 {
426                         items[i]->set_visible(true);
427                         unsigned iw = (view_mode==GRID ? igeom.w : view_w);
428                         items[i]->set_geometry(Geometry(margin.left+x, y-igeom.h, iw, igeom.h));
429                 }
430                 else
431                 {
432                         for(size_t j=rows.back().first; j<=i; ++j)
433                                 items[j]->set_visible(false);
434                         y = 0;
435                 }
436
437                 x += igeom.w;
438                 row_h = max(row_h, igeom.h);
439         }
440
441         if(record_rows)
442                 rows.back().height = row_h;
443 }
444
445 size_t List::last_to_first_row(size_t last) const
446 {
447         if(!items_part)
448                 return last;
449
450         const Sides &margin = items_part->get_margin();
451         unsigned view_h = geom.h-min(geom.h, margin.top+margin.bottom);
452
453         unsigned items_h = 0;
454         for(size_t i=last; i<rows.size(); --i)
455         {
456                 items_h += rows[i].height;
457                 if(items_h>view_h)
458                         return min(i+1, last);
459         }
460
461         return 0;
462 }
463
464 size_t List::item_index_to_row(size_t index) const
465 {
466         for(size_t i=0; i+1<rows.size(); ++i)
467                 if(rows[i+1].first>index)
468                         return i;
469         return rows.size()-1;
470 }
471
472 void List::check_view_range()
473 {
474         if(!style)
475                 return;
476
477         if(items.empty())
478                 max_scroll = 0;
479         else
480                 max_scroll = last_to_first_row(rows.size()-1);
481
482         if(first_row>max_scroll)
483                 first_row = max_scroll;
484
485         slider.set_range(0, max_scroll);
486         slider.set_page_size(rows.size()-max_scroll);
487         slider.set_value(max_scroll-first_row);
488 }
489
490 void List::scroll_to_focus()
491 {
492         if(focus_index==INVALID_INDEX || items[focus_index]->is_visible())
493                 return;
494
495         size_t focus_row = item_index_to_row(focus_index);
496         if(focus_row<first_row)
497                 slider.set_value(max_scroll-focus_row);
498         else
499                 slider.set_value(max_scroll-last_to_first_row(focus_row));
500 }
501
502 void List::slider_value_changed(double value)
503 {
504         if(max_scroll>0 && !ignore_slider_change)
505         {
506                 first_row = max_scroll-static_cast<size_t>(value);
507                 mark_rebuild();
508         }
509 }
510
511 void List::adjust_index(size_t &index, size_t pos, ptrdiff_t change)
512 {
513         if(index==INVALID_INDEX)
514                 return;
515         else if(index>pos)
516                 index += change;
517         else if(index==pos)
518                 index = (change>0 ? index+change : INVALID_INDEX);
519 }
520
521
522 List::DataObserver::DataObserver(List &l):
523         list(l)
524 {
525         list.data->signal_item_added.connect(sigc::mem_fun(this, &DataObserver::item_added));
526         list.data->signal_item_removed.connect(sigc::mem_fun(this, &DataObserver::item_removed));
527         list.data->signal_cleared.connect(sigc::mem_fun(this, &DataObserver::cleared));
528         list.data->signal_refresh_item.connect(sigc::mem_fun(this, &DataObserver::refresh_item));
529 }
530
531 void List::DataObserver::item_added(size_t i)
532 {
533         adjust_index(list.sel_index, i, 1);
534         adjust_index(list.focus_index, i, 1);
535
536         Item *item = list.create_item(i);
537         list.items.insert(list.items.begin()+i, item);
538         list.items_changed();
539 }
540
541 void List::DataObserver::item_removed(size_t i)
542 {
543         bool had_selection = (list.sel_index!=INVALID_INDEX);
544         adjust_index(list.sel_index, i, -1);
545         adjust_index(list.focus_index, i, -1);
546
547         delete list.items[i];
548         list.items.erase(list.items.begin()+i);
549         list.items_changed();
550
551         if(had_selection && list.sel_index==INVALID_INDEX)
552                 list.signal_selection_cleared.emit();
553 }
554
555 void List::DataObserver::cleared()
556 {
557         list.sel_index = INVALID_INDEX;
558         list.focus_index = INVALID_INDEX;
559         for(Item *i: list.items)
560                 delete i;
561         list.items.clear();
562         list.items_changed();
563
564         list.signal_selection_cleared.emit();
565 }
566
567 void List::DataObserver::refresh_item(size_t i)
568 {
569         delete list.items[i];
570         // Avoid stale pointer while create_item is executing
571         list.items[i] = nullptr;
572         list.items[i] = list.create_item(i);
573         list.items_changed();
574 }
575
576
577 List::Item::Item()
578 {
579         input_type = INPUT_NAVIGATION;
580 }
581
582 void List::Item::autosize_special(const Part &part, Geometry &ageom) const
583 {
584         if(part.get_name()=="children")
585         {
586                 const Sides &margin = part.get_margin();
587                 for(const Child *c: children)
588                 {
589                         Geometry cgeom;
590                         c->widget->autosize(cgeom);
591                         ageom.w = max(ageom.w, cgeom.x+cgeom.w+margin.right);
592                         ageom.h = max(ageom.h, cgeom.y+cgeom.h+margin.top);
593                 }
594         }
595 }
596
597 void List::Item::set_active(bool a)
598 {
599         set_state(ACTIVE, (a ? ACTIVE : NORMAL));
600 }
601
602 void List::Item::render_special(const Part &part, GL::Renderer &renderer) const
603 {
604         if(part.get_name()=="children")
605         {
606                 for(const Child *c: children)
607                         c->widget->render(renderer);
608         }
609 }
610
611
612 void List::SimpleItem::on_style_change()
613 {
614         if(!style || children.empty())
615                 return;
616
617         Widget *child = children.front()->widget;
618         child->autosize();
619         if(const Part *part = style->find_part("children"))
620         {
621                 const Sides &margin = part->get_margin();
622                 child->set_position(margin.left, margin.bottom);
623         }
624 }
625
626
627 void List::MultiColumnItem::check_widths(vector<unsigned> &widths) const
628 {
629         if(widths.size()<children.size())
630                 widths.resize(children.size(), 0);
631
632         size_t n = 0;
633         for(const Child *c: children)
634         {
635                 Geometry cgeom;
636                 c->widget->autosize(cgeom);
637                 // TODO invent a better way to specify spacings
638                 widths[n] = max(widths[n], cgeom.w+8);
639                 ++n;
640         }
641 }
642
643 void List::MultiColumnItem::set_widths(const vector<unsigned> &widths)
644 {
645         if(!style)
646                 return;
647
648         const Part *part = style->find_part("children");
649         if(!part)
650                 return;
651
652         const Sides &margin = part->get_margin();
653         int x = margin.left;
654         size_t n = 0;
655         for(const Child *c: children)
656         {
657                 c->widget->set_position(x, margin.bottom);
658                 x += widths[n++];
659         }
660 }
661
662 void List::MultiColumnItem::on_style_change()
663 {
664         if(!style)
665                 return;
666
667         for(const Child *c: children)
668                 c->widget->autosize();
669
670         vector<unsigned> widths;
671         List *list = static_cast<List *>(parent);
672         for(Item *i: list->items)
673                 if(i!=this)
674                         if(MultiColumnItem *mci = dynamic_cast<MultiColumnItem *>(i))
675                                 mci->check_widths(widths);
676
677         vector<unsigned> self_widths(widths);
678         check_widths(self_widths);
679         bool update_all = false;
680         for(size_t i=0; (!update_all && i<widths.size() && i<self_widths.size()); ++i)
681                 update_all = self_widths[i]>widths[i];
682
683         if(update_all)
684         {
685                 for(Item *i: list->items)
686                         if(MultiColumnItem *mci = dynamic_cast<MultiColumnItem *>(i))
687                                 mci->set_widths(self_widths);
688         }
689
690         set_widths(self_widths);
691 }
692
693
694 List::BasicItem::BasicItem(const string &text):
695         label(text)
696 {
697         add(label);
698 }
699
700
701 List::Loader::Loader(List &l):
702         DataFile::DerivedObjectLoader<List, Widget::Loader>(l)
703 {
704         add("item", &Loader::item);
705         add("view_mode", &List::view_mode);
706         add("view_size", &List::view_rows);
707         add("view_size", &List::view_rows, &List::view_columns);
708 }
709
710 void List::Loader::item(const string &v)
711 {
712         dynamic_cast<BasicListData<string> &>(*obj.data).append(v);
713 }
714
715
716 void operator>>(const LexicalConverter &conv, List::ViewMode &vm)
717 {
718         const string &str = conv.get();
719         if(str=="LIST")
720                 vm = List::LIST;
721         else if(str=="GRID")
722                 vm = List::GRID;
723         else
724                 throw lexical_error(format("conversion of '%s' to List::ViewMode", str));
725 }
726
727 } // namespace GLtk
728 } // namespace Msp