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