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