]> git.tdb.fi Git - libs/gltk.git/blob - source/entry.cpp
Rework how widget ownership works in Container
[libs/gltk.git] / source / entry.cpp
1 #include <msp/gl/matrix.h>
2 #include <msp/gl/meshbuilder.h>
3 #include <msp/gl/texture.h>
4 #include <msp/input/keys.h>
5 #include "entry.h"
6 #include "graphic.h"
7 #include "part.h"
8 #include "slider.h"
9 #include "style.h"
10
11 using namespace std;
12
13 namespace Msp {
14 namespace GLtk {
15
16 Entry::Entry(const string &t)
17 {
18         input_type = INPUT_TEXT;
19         set_text(t);
20 }
21
22 void Entry::autosize_special(const Part &part, Geometry &ageom) const
23 {
24         if(part.get_name()=="text")
25         {
26                 const Sides &margin = part.get_margin();
27                 const GL::Font &font = style->get_font();
28                 unsigned en_width = static_cast<unsigned>(font.get_string_width("n")*style->get_font_size());
29                 ageom.w = max(ageom.w, edit_width*en_width+margin.left+margin.right);
30
31                 unsigned line_height = static_cast<unsigned>((font.get_ascent()-font.get_descent())*style->get_font_size());
32                 if(multiline)
33                 {
34                         unsigned line_spacing = style->get_font_size()*6/5;
35                         ageom.h = max(ageom.h, line_height+line_spacing*(edit_height-1)+margin.top+margin.bottom);
36                 }
37                 else
38                         ageom.h = max(ageom.h, line_height+margin.top+margin.bottom);
39         }
40         else if(part.get_name()=="slider" && multiline)
41                 autosize_child(*slider, part, ageom);
42 }
43
44 void Entry::set_text(const string &t)
45 {
46         if(t!=text.get())
47         {
48                 text = t;
49                 signal_text_changed.emit(text.get());
50         }
51         set_edit_position(text.size());
52 }
53
54 void Entry::insert(size_t pos, const string &t)
55 {
56         if(t.empty())
57                 return;
58
59         text.insert(pos, t);
60         signal_text_changed.emit(text.get());
61
62         adjust_edit_position_for_change(pos, t.size());
63         if(multiline)
64                 check_view_range();
65
66         mark_rebuild();
67 }
68
69 void Entry::erase(size_t pos, size_t len)
70 {
71         if(!len)
72                 return;
73
74         text.erase(pos, len);
75         signal_text_changed.emit(text.get());
76
77         adjust_edit_position_for_change(pos, -len);
78         if(multiline)
79                 check_view_range();
80
81         mark_rebuild();
82 }
83
84 bool Entry::get_selection(size_t &start, size_t &end) const
85 {
86         if(!selection_active)
87                 return false;
88
89         start = selection_pos;
90         end = edit_pos;
91         if(start>end)
92                 swap(start, end);
93
94         return true;
95 }
96
97 void Entry::translate_position(size_t pos, size_t &row, size_t &col) const
98 {
99         text.offset_to_coords(pos, row, col);
100 }
101
102 size_t Entry::translate_position(size_t row, size_t col) const
103 {
104         return text.coords_to_offset(row, col);
105 }
106
107 void Entry::set_edit_size(unsigned w, unsigned h)
108 {
109         edit_width = w;
110         edit_height = h;
111         signal_autosize_changed.emit();
112 }
113
114 void Entry::set_multiline(bool m)
115 {
116         multiline = m;
117         if(multiline)
118         {
119                 if(!slider)
120                 {
121                         unique_ptr<VSlider> s = make_unique<VSlider>();
122                         slider = s.get();
123                         add(move(s));
124                         slider->set_step(1);
125                         slider->signal_value_changed.connect(sigc::mem_fun(this, &Entry::slider_value_changed));
126                         mark_rebuild();
127                 }
128                 check_view_range();
129         }
130 }
131
132 void Entry::rebuild_special(const Part &part)
133 {
134         if(part.get_name()=="text")
135                 text.build(part, state, geom, first_row, part_cache);
136         else if(part.get_name()=="cursor")
137         {
138                 State cursor_state = (cursor_blink ? ACTIVE : NORMAL);
139                 const Graphic *graphic = part.get_graphic(state|cursor_state);
140                 if(!text_part || !graphic || !graphic->get_texture())
141                         return;
142
143                 size_t row, col;
144                 text.offset_to_coords(edit_pos, row, col);
145
146                 if(row<first_row || row>=first_row+visible_rows)
147                         return;
148
149                 Geometry rgeom = text.coords_to_geometry(*text_part, geom, first_row, row, col);
150
151                 GL::MeshBuilder bld(part_cache.create_mesh(part, *graphic->get_texture()));
152                 bld.transform(GL::Matrix::translation(rgeom.x, rgeom.y, 0));
153                 graphic->build(part.get_geometry().w, part.get_geometry().h, bld);
154         }
155         else if(part.get_name()=="selection")
156         {
157                 if(!selection_active)
158                         return;
159
160                 const Graphic *graphic = part.get_graphic(state);
161                 if(!text_part || !graphic || !graphic->get_texture())
162                         return;
163
164                 size_t start, end;
165                 get_selection(start, end);
166
167                 size_t row, col;
168                 text.offset_to_coords(start, row, col);
169                 size_t end_row, end_col;
170                 text.offset_to_coords(end, end_row, end_col);
171
172                 if(end_row<first_row || row>=first_row+visible_rows)
173                         return;
174
175                 if(row<first_row)
176                 {
177                         row = first_row;
178                         col = 0;
179                 }
180
181                 if(end_row>=first_row+visible_rows)
182                 {
183                         end_row = first_row+visible_rows-1;
184                         end_col = text.get_line_length(end_row);
185                 }
186
187                 while(row<=end_row)
188                 {
189                         size_t ec = (row==end_row ? end_col : text.get_line_length(row));
190                         if(ec>col)
191                         {
192                                 Geometry rgeom = text.coords_to_geometry(*text_part, geom, first_row, row, col);
193                                 Geometry egeom = text.coords_to_geometry(*text_part, geom, first_row, row, ec);
194
195                                 GL::MeshBuilder bld(part_cache.create_mesh(part, *graphic->get_texture()));
196                                 bld.transform(GL::Matrix::translation(rgeom.x, rgeom.y, 0));
197                                 graphic->build(egeom.x-rgeom.x, part.get_geometry().h, bld);
198                         }
199
200                         ++row;
201                         col = 0;
202                 }
203         }
204         else if(part.get_name()=="slider")
205         {
206                 if(multiline)
207                 {
208                         reposition_child(*slider, part);
209                         Widget::rebuild_special(part);
210                 }
211         }
212         else
213                 Widget::rebuild_special(part);
214 }
215
216 void Entry::render_special(const Part &part, GL::Renderer &renderer) const
217 {
218         if(part.get_name()=="slider" && multiline)
219                 slider->render(renderer);
220 }
221
222 void Entry::touch_press(int x, int y, unsigned finger)
223 {
224         if(finger==0)
225                 set_focus();
226         Widget::touch_press(x, y, finger);
227 }
228
229 bool Entry::key_press(unsigned key, unsigned mod)
230 {
231         got_key_press = true;
232         if(key==Input::KEY_BACKSPACE)
233         {
234                 if(selection_active)
235                         erase_selection(true);
236                 else if(edit_pos>0)
237                 {
238                         size_t start_pos = text.move_offset(edit_pos, -1);
239                         erase(start_pos, edit_pos-start_pos);
240                 }
241         }
242         else if(key==Input::KEY_DELETE)
243         {
244                 if(selection_active)
245                         erase_selection(true);
246                 else
247                 {
248                         size_t end_pos = text.move_offset(edit_pos, 1);
249                         erase(edit_pos, end_pos-edit_pos);
250                 }
251         }
252         else if(key==Input::KEY_ENTER && multiline)
253                 insert(edit_pos, "\n");
254         else if(key==Input::KEY_END)
255         {
256                 size_t row, col;
257                 text.offset_to_coords(edit_pos, row, col);
258                 set_edit_position(text.coords_to_offset(row, text.get_line_length(row)), mod==MOD_SHIFT);
259         }
260         else if(key==Input::KEY_HOME)
261         {
262                 size_t row, col;
263                 text.offset_to_coords(edit_pos, row, col);
264                 set_edit_position(text.coords_to_offset(row, 0), mod==MOD_SHIFT);
265         }
266         else if(key==Input::KEY_PGUP)
267         {
268                 size_t row, col;
269                 text.offset_to_coords(edit_pos, row, col);
270                 set_edit_position(text.coords_to_offset((row<visible_rows ? 0 : row-visible_rows), col), mod==MOD_SHIFT);
271         }
272         else if(key==Input::KEY_PGDN)
273         {
274                 size_t row, col;
275                 text.offset_to_coords(edit_pos, row, col);
276                 set_edit_position(text.coords_to_offset(row+visible_rows, col), mod==MOD_SHIFT);
277         }
278         else if(key==Input::KEY_LEFT && mod==MOD_SHIFT)
279                 move_edit_position(NAV_LEFT, true);
280         else if(key==Input::KEY_RIGHT && mod==MOD_SHIFT)
281                 move_edit_position(NAV_RIGHT, true);
282         else if(key==Input::KEY_UP && mod==MOD_SHIFT && multiline)
283                 move_edit_position(NAV_UP, true);
284         else if(key==Input::KEY_DOWN && mod==MOD_SHIFT && multiline)
285                 move_edit_position(NAV_DOWN, true);
286         else
287                 return false;
288
289         return true;
290 }
291
292 bool Entry::character(wchar_t ch)
293 {
294         if(got_key_press && ch>=' ' && ch!=0x7F)
295         {
296                 if(selection_active)
297                         erase_selection(false);
298                 insert(edit_pos, StringCodec::encode<StringCodec::Utf8>(StringCodec::ustring(1, ch)));
299                 return true;
300         }
301
302         return false;
303 }
304
305 void Entry::focus_in()
306 {
307         cursor_blink = true;
308         Widget::focus_in();
309         check_cursor_blink();
310 }
311
312 void Entry::focus_out()
313 {
314         Widget::focus_out();
315         got_key_press = false;
316         check_cursor_blink();
317 }
318
319 bool Entry::navigate(Navigation nav)
320 {
321         if(nav==NAV_LEFT || nav==NAV_RIGHT || ((nav==NAV_DOWN || nav==NAV_UP) && multiline))
322                 move_edit_position(nav, false);
323         else
324                 return false;
325
326         return true;
327 }
328
329 void Entry::animate(const Time::TimeDelta &)
330 {
331         cursor_blink = !cursor_blink;
332         mark_rebuild();
333 }
334
335 void Entry::on_size_change()
336 {
337         if(multiline)
338                 check_view_range();
339 }
340
341 void Entry::on_style_change()
342 {
343         text.set_style(style);
344
345         if(!style)
346         {
347                 text_part = nullptr;
348                 return;
349         }
350
351         text_part = style->find_part("text");
352
353         if(multiline)
354                 check_view_range();
355
356         check_cursor_blink();
357 }
358
359 void Entry::move_edit_position(Navigation nav, bool select)
360 {
361         if(nav==NAV_LEFT)
362                 set_edit_position(text.move_offset(edit_pos, -1), select);
363         else if(nav==NAV_RIGHT)
364                 set_edit_position(text.move_offset(edit_pos, 1), select);
365         else if(nav==NAV_DOWN)
366         {
367                 size_t row, col;
368                 text.offset_to_coords(edit_pos, row, col);
369                 set_edit_position(text.coords_to_offset(row+1, col), select);
370         }
371         else if(nav==NAV_UP)
372         {
373                 size_t row, col;
374                 text.offset_to_coords(edit_pos, row, col);
375                 set_edit_position((row>0 ? text.coords_to_offset(row-1, col) : 0), select);
376         }
377         else
378                 throw invalid_argument("Entry::move_edit_position");
379 }
380
381 void Entry::adjust_edit_position_for_change(size_t pos, ptrdiff_t change)
382 {
383         size_t old_edit_pos = edit_pos;
384         size_t old_select_pos = selection_pos;
385
386         if(change>0)
387         {
388                 if(edit_pos>=pos)
389                         edit_pos += change;
390                 if(selection_active && selection_pos>=pos)
391                         selection_pos += change;
392         }
393         else if(change<0)
394         {
395                 if(edit_pos>=pos)
396                         edit_pos -= min<size_t>(edit_pos-pos, -change);
397                 if(selection_active && selection_pos>=pos)
398                         selection_pos -= min<size_t>(selection_pos-pos, -change);
399         }
400
401         if(edit_pos!=old_edit_pos)
402                 signal_edit_position_changed.emit(edit_pos);
403         if(selection_active && (edit_pos!=old_edit_pos || selection_pos!=old_select_pos))
404         {
405                 size_t start, end;
406                 if(get_selection(start, end))
407                         signal_selection_changed.emit(start, end);
408         }
409 }
410
411 void Entry::set_edit_position(size_t ep, bool select)
412 {
413         bool selection_was_active = selection_active;
414         if(select && !selection_active)
415                 selection_pos = edit_pos;
416         selection_active = select;
417
418         size_t old_edit_pos = edit_pos;
419         edit_pos = min(ep, text.size());
420
421         if(edit_pos!=old_edit_pos)
422         {
423                 signal_edit_position_changed.emit(edit_pos);
424                 size_t start, end;
425                 if(get_selection(start, end))
426                         signal_selection_changed.emit(start, end);
427                 else if(selection_was_active)
428                         signal_selection_changed.emit(edit_pos, edit_pos);
429         }
430
431         if(multiline)
432                 check_view_range();
433         mark_rebuild();
434 }
435
436 void Entry::erase_selection(bool emit_change)
437 {
438         size_t start, end;
439         if(!get_selection(start, end))
440                 return;
441
442         text.erase(start, end-start);
443         if(emit_change)
444                 signal_text_changed.emit(text.get());
445         set_edit_position(start, false);
446 }
447
448 void Entry::check_cursor_blink()
449 {
450         const Part *cursor_part = style->find_part("cursor");
451         bool has_blink = (cursor_part && cursor_part->get_graphic(ACTIVE|FOCUS)!=cursor_part->get_graphic(NORMAL|FOCUS));
452
453         cursor_blink = (state&FOCUS);
454         if((state&FOCUS) && style && has_blink)
455         {
456                 set_animation_interval(Time::sec/2);
457                 mark_rebuild();
458         }
459         else
460         {
461                 if(has_blink)
462                         mark_rebuild();
463                 stop_animation();
464         }
465 }
466
467 void Entry::check_view_range()
468 {
469         if(!multiline || !text_part)
470                 return;
471
472         visible_rows = text.get_visible_lines(*text_part, geom, nullptr);
473
474         size_t row, col;
475         text.offset_to_coords(edit_pos, row, col);
476
477         size_t old_first_row = first_row;
478         if(first_row>row)
479                 first_row = row;
480         else if(row>=first_row+visible_rows)
481                 first_row = row+1-visible_rows;
482
483         size_t scroll = max<size_t>(text.get_n_lines(), visible_rows)-visible_rows;
484         if(first_row>scroll)
485                 first_row = scroll;
486
487         if(first_row!=old_first_row)
488                 signal_scroll_position_changed.emit(first_row);
489
490         slider->set_range(0, scroll);
491         slider->set_page_size(visible_rows);
492         slider->set_value(scroll-first_row);
493 }
494
495 void Entry::slider_value_changed(double value)
496 {
497         if(text.get_n_lines()>visible_rows)
498         {
499                 size_t old_first_row = first_row;
500                 first_row = text.get_n_lines()-visible_rows-static_cast<size_t>(value);
501                 if(first_row!=old_first_row)
502                         signal_scroll_position_changed.emit(first_row);
503                 mark_rebuild();
504         }
505 }
506
507
508 Entry::Loader::Loader(Entry &e):
509         DataFile::DerivedObjectLoader<Entry, Widget::Loader>(e)
510 {
511         add("edit_size", &Entry::edit_width, &Entry::edit_height);
512         add("multiline", &Loader::multiline);
513 }
514
515 void Entry::Loader::multiline(bool m)
516 {
517         obj.set_multiline(m);
518 }
519
520 } // namespace GLtk
521 } // namespace Msp