]> git.tdb.fi Git - libs/gltk.git/blob - source/entry.cpp
Use size_t to represent counts and indices
[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                         slider = new VSlider;
122                         add(*slider);
123                         slider->set_step(1);
124                         slider->signal_value_changed.connect(sigc::mem_fun(this, &Entry::slider_value_changed));
125                         mark_rebuild();
126                 }
127                 check_view_range();
128         }
129 }
130
131 void Entry::rebuild_special(const Part &part)
132 {
133         if(part.get_name()=="text")
134                 text.build(part, state, geom, first_row, part_cache);
135         else if(part.get_name()=="cursor")
136         {
137                 State cursor_state = (cursor_blink ? ACTIVE : NORMAL);
138                 const Graphic *graphic = part.get_graphic(state|cursor_state);
139                 if(!text_part || !graphic || !graphic->get_texture())
140                         return;
141
142                 size_t row, col;
143                 text.offset_to_coords(edit_pos, row, col);
144
145                 if(row<first_row || row>=first_row+visible_rows)
146                         return;
147
148                 Geometry rgeom = text.coords_to_geometry(*text_part, geom, first_row, row, col);
149
150                 GL::MeshBuilder bld(part_cache.create_mesh(part, *graphic->get_texture()));
151                 bld.transform(GL::Matrix::translation(rgeom.x, rgeom.y, 0));
152                 graphic->build(part.get_geometry().w, part.get_geometry().h, bld);
153         }
154         else if(part.get_name()=="selection")
155         {
156                 if(!selection_active)
157                         return;
158
159                 const Graphic *graphic = part.get_graphic(state);
160                 if(!text_part || !graphic || !graphic->get_texture())
161                         return;
162
163                 size_t start, end;
164                 get_selection(start, end);
165
166                 size_t row, col;
167                 text.offset_to_coords(start, row, col);
168                 size_t end_row, end_col;
169                 text.offset_to_coords(end, end_row, end_col);
170
171                 if(end_row<first_row || row>=first_row+visible_rows)
172                         return;
173
174                 if(row<first_row)
175                 {
176                         row = first_row;
177                         col = 0;
178                 }
179
180                 if(end_row>=first_row+visible_rows)
181                 {
182                         end_row = first_row+visible_rows-1;
183                         end_col = text.get_line_length(end_row);
184                 }
185
186                 while(row<=end_row)
187                 {
188                         size_t ec = (row==end_row ? end_col : text.get_line_length(row));
189                         if(ec>col)
190                         {
191                                 Geometry rgeom = text.coords_to_geometry(*text_part, geom, first_row, row, col);
192                                 Geometry egeom = text.coords_to_geometry(*text_part, geom, first_row, row, ec);
193
194                                 GL::MeshBuilder bld(part_cache.create_mesh(part, *graphic->get_texture()));
195                                 bld.transform(GL::Matrix::translation(rgeom.x, rgeom.y, 0));
196                                 graphic->build(egeom.x-rgeom.x, part.get_geometry().h, bld);
197                         }
198
199                         ++row;
200                         col = 0;
201                 }
202         }
203         else if(part.get_name()=="slider")
204         {
205                 if(multiline)
206                 {
207                         reposition_child(*slider, part);
208                         Widget::rebuild_special(part);
209                 }
210         }
211         else
212                 Widget::rebuild_special(part);
213 }
214
215 void Entry::render_special(const Part &part, GL::Renderer &renderer) const
216 {
217         if(part.get_name()=="slider" && multiline)
218                 slider->render(renderer);
219 }
220
221 void Entry::touch_press(int x, int y, unsigned finger)
222 {
223         if(finger==0)
224                 set_focus();
225         Widget::touch_press(x, y, finger);
226 }
227
228 bool Entry::key_press(unsigned key, unsigned mod)
229 {
230         got_key_press = true;
231         if(key==Input::KEY_BACKSPACE)
232         {
233                 if(selection_active)
234                         erase_selection(true);
235                 else if(edit_pos>0)
236                 {
237                         size_t start_pos = text.move_offset(edit_pos, -1);
238                         erase(start_pos, edit_pos-start_pos);
239                 }
240         }
241         else if(key==Input::KEY_DELETE)
242         {
243                 if(selection_active)
244                         erase_selection(true);
245                 else
246                 {
247                         size_t end_pos = text.move_offset(edit_pos, 1);
248                         erase(edit_pos, end_pos-edit_pos);
249                 }
250         }
251         else if(key==Input::KEY_ENTER && multiline)
252                 insert(edit_pos, "\n");
253         else if(key==Input::KEY_END)
254         {
255                 size_t row, col;
256                 text.offset_to_coords(edit_pos, row, col);
257                 set_edit_position(text.coords_to_offset(row, text.get_line_length(row)), mod==MOD_SHIFT);
258         }
259         else if(key==Input::KEY_HOME)
260         {
261                 size_t row, col;
262                 text.offset_to_coords(edit_pos, row, col);
263                 set_edit_position(text.coords_to_offset(row, 0), mod==MOD_SHIFT);
264         }
265         else if(key==Input::KEY_PGUP)
266         {
267                 size_t row, col;
268                 text.offset_to_coords(edit_pos, row, col);
269                 set_edit_position(text.coords_to_offset((row<visible_rows ? 0 : row-visible_rows), col), mod==MOD_SHIFT);
270         }
271         else if(key==Input::KEY_PGDN)
272         {
273                 size_t row, col;
274                 text.offset_to_coords(edit_pos, row, col);
275                 set_edit_position(text.coords_to_offset(row+visible_rows, col), mod==MOD_SHIFT);
276         }
277         else if(key==Input::KEY_LEFT && mod==MOD_SHIFT)
278                 move_edit_position(NAV_LEFT, true);
279         else if(key==Input::KEY_RIGHT && mod==MOD_SHIFT)
280                 move_edit_position(NAV_RIGHT, true);
281         else if(key==Input::KEY_UP && mod==MOD_SHIFT && multiline)
282                 move_edit_position(NAV_UP, true);
283         else if(key==Input::KEY_DOWN && mod==MOD_SHIFT && multiline)
284                 move_edit_position(NAV_DOWN, true);
285         else
286                 return false;
287
288         return true;
289 }
290
291 bool Entry::character(wchar_t ch)
292 {
293         if(got_key_press && ch>=' ' && ch!=0x7F)
294         {
295                 if(selection_active)
296                         erase_selection(false);
297                 insert(edit_pos, StringCodec::encode<StringCodec::Utf8>(StringCodec::ustring(1, ch)));
298                 return true;
299         }
300
301         return false;
302 }
303
304 void Entry::focus_in()
305 {
306         cursor_blink = true;
307         Widget::focus_in();
308         check_cursor_blink();
309 }
310
311 void Entry::focus_out()
312 {
313         Widget::focus_out();
314         got_key_press = false;
315         check_cursor_blink();
316 }
317
318 bool Entry::navigate(Navigation nav)
319 {
320         if(nav==NAV_LEFT || nav==NAV_RIGHT || ((nav==NAV_DOWN || nav==NAV_UP) && multiline))
321                 move_edit_position(nav, false);
322         else
323                 return false;
324
325         return true;
326 }
327
328 void Entry::animate(const Time::TimeDelta &)
329 {
330         cursor_blink = !cursor_blink;
331         mark_rebuild();
332 }
333
334 void Entry::on_size_change()
335 {
336         if(multiline)
337                 check_view_range();
338 }
339
340 void Entry::on_style_change()
341 {
342         text.set_style(style);
343
344         if(!style)
345         {
346                 text_part = nullptr;
347                 return;
348         }
349
350         text_part = style->find_part("text");
351
352         if(multiline)
353                 check_view_range();
354
355         check_cursor_blink();
356 }
357
358 void Entry::move_edit_position(Navigation nav, bool select)
359 {
360         if(nav==NAV_LEFT)
361                 set_edit_position(text.move_offset(edit_pos, -1), select);
362         else if(nav==NAV_RIGHT)
363                 set_edit_position(text.move_offset(edit_pos, 1), select);
364         else if(nav==NAV_DOWN)
365         {
366                 size_t row, col;
367                 text.offset_to_coords(edit_pos, row, col);
368                 set_edit_position(text.coords_to_offset(row+1, col), select);
369         }
370         else if(nav==NAV_UP)
371         {
372                 size_t row, col;
373                 text.offset_to_coords(edit_pos, row, col);
374                 set_edit_position((row>0 ? text.coords_to_offset(row-1, col) : 0), select);
375         }
376         else
377                 throw invalid_argument("Entry::move_edit_position");
378 }
379
380 void Entry::adjust_edit_position_for_change(size_t pos, ptrdiff_t change)
381 {
382         size_t old_edit_pos = edit_pos;
383         size_t old_select_pos = selection_pos;
384
385         if(change>0)
386         {
387                 if(edit_pos>=pos)
388                         edit_pos += change;
389                 if(selection_active && selection_pos>=pos)
390                         selection_pos += change;
391         }
392         else if(change<0)
393         {
394                 if(edit_pos>=pos)
395                         edit_pos -= min<size_t>(edit_pos-pos, -change);
396                 if(selection_active && selection_pos>=pos)
397                         selection_pos -= min<size_t>(selection_pos-pos, -change);
398         }
399
400         if(edit_pos!=old_edit_pos)
401                 signal_edit_position_changed.emit(edit_pos);
402         if(selection_active && (edit_pos!=old_edit_pos || selection_pos!=old_select_pos))
403         {
404                 size_t start, end;
405                 if(get_selection(start, end))
406                         signal_selection_changed.emit(start, end);
407         }
408 }
409
410 void Entry::set_edit_position(size_t ep, bool select)
411 {
412         bool selection_was_active = selection_active;
413         if(select && !selection_active)
414                 selection_pos = edit_pos;
415         selection_active = select;
416
417         size_t old_edit_pos = edit_pos;
418         edit_pos = min(ep, text.size());
419
420         if(edit_pos!=old_edit_pos)
421         {
422                 signal_edit_position_changed.emit(edit_pos);
423                 size_t start, end;
424                 if(get_selection(start, end))
425                         signal_selection_changed.emit(start, end);
426                 else if(selection_was_active)
427                         signal_selection_changed.emit(edit_pos, edit_pos);
428         }
429
430         if(multiline)
431                 check_view_range();
432         mark_rebuild();
433 }
434
435 void Entry::erase_selection(bool emit_change)
436 {
437         size_t start, end;
438         if(!get_selection(start, end))
439                 return;
440
441         text.erase(start, end-start);
442         if(emit_change)
443                 signal_text_changed.emit(text.get());
444         set_edit_position(start, false);
445 }
446
447 void Entry::check_cursor_blink()
448 {
449         const Part *cursor_part = style->find_part("cursor");
450         bool has_blink = (cursor_part && cursor_part->get_graphic(ACTIVE|FOCUS)!=cursor_part->get_graphic(NORMAL|FOCUS));
451
452         cursor_blink = (state&FOCUS);
453         if((state&FOCUS) && style && has_blink)
454         {
455                 set_animation_interval(Time::sec/2);
456                 mark_rebuild();
457         }
458         else
459         {
460                 if(has_blink)
461                         mark_rebuild();
462                 stop_animation();
463         }
464 }
465
466 void Entry::check_view_range()
467 {
468         if(!multiline || !text_part)
469                 return;
470
471         visible_rows = text.get_visible_lines(*text_part, geom, nullptr);
472
473         size_t row, col;
474         text.offset_to_coords(edit_pos, row, col);
475
476         size_t old_first_row = first_row;
477         if(first_row>row)
478                 first_row = row;
479         else if(row>=first_row+visible_rows)
480                 first_row = row+1-visible_rows;
481
482         size_t scroll = max<size_t>(text.get_n_lines(), visible_rows)-visible_rows;
483         if(first_row>scroll)
484                 first_row = scroll;
485
486         if(first_row!=old_first_row)
487                 signal_scroll_position_changed.emit(first_row);
488
489         slider->set_range(0, scroll);
490         slider->set_page_size(visible_rows);
491         slider->set_value(scroll-first_row);
492 }
493
494 void Entry::slider_value_changed(double value)
495 {
496         if(text.get_n_lines()>visible_rows)
497         {
498                 size_t old_first_row = first_row;
499                 first_row = text.get_n_lines()-visible_rows-static_cast<size_t>(value);
500                 if(first_row!=old_first_row)
501                         signal_scroll_position_changed.emit(first_row);
502                 mark_rebuild();
503         }
504 }
505
506
507 Entry::Loader::Loader(Entry &e):
508         DataFile::DerivedObjectLoader<Entry, Widget::Loader>(e)
509 {
510         add("edit_size", &Entry::edit_width, &Entry::edit_height);
511         add("multiline", &Loader::multiline);
512 }
513
514 void Entry::Loader::multiline(bool m)
515 {
516         obj.set_multiline(m);
517 }
518
519 } // namespace GLtk
520 } // namespace Msp