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