1 /**
2  * Copyright (c) 2007-2012, Timothy Stack
3  *
4  * All rights reserved.
5  *
6  * Redistribution and use in source and binary forms, with or without
7  * modification, are permitted provided that the following conditions are met:
8  *
9  * * Redistributions of source code must retain the above copyright notice, this
10  * list of conditions and the following disclaimer.
11  * * Redistributions in binary form must reproduce the above copyright notice,
12  * this list of conditions and the following disclaimer in the documentation
13  * and/or other materials provided with the distribution.
14  * * Neither the name of Timothy Stack nor the names of its contributors
15  * may be used to endorse or promote products derived from this software
16  * without specific prior written permission.
17  *
18  * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
19  * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
20  * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21  * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
22  * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
23  * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
24  * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
25  * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
27  * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28  *
29  * @file listview_curses.cc
30  */
31 
32 #include "config.h"
33 
34 #include <time.h>
35 #include <sys/time.h>
36 
37 #include <cmath>
38 
39 #include "base/lnav_log.hh"
40 #include "listview_curses.hh"
41 
42 using namespace std;
43 
44 list_gutter_source listview_curses::DEFAULT_GUTTER_SOURCE;
45 
listview_curses()46 listview_curses::listview_curses()
47     : lv_scroll(noop_func{})
48 {
49 }
50 
reload_data()51 void listview_curses::reload_data()
52 {
53     if (this->lv_source == nullptr) {
54         this->lv_top  = 0_vl;
55         this->lv_left = 0;
56     }
57     else {
58         if (this->lv_top >= this->get_inner_height()) {
59             this->lv_top = max(0_vl, vis_line_t(this->get_inner_height() - 1));
60         }
61         if (this->get_inner_height() == 0) {
62             this->lv_selection = 0_vl;
63         } else if (this->lv_selection >= this->get_inner_height()) {
64             this->lv_selection = this->get_inner_height() - 1_vl;
65         }
66     }
67     this->vc_needs_update = true;
68 }
69 
handle_key(int ch)70 bool listview_curses::handle_key(int ch)
71 {
72     for (auto &lv_input_delegate : this->lv_input_delegates) {
73         if (lv_input_delegate->list_input_handle_key(*this, ch)) {
74             return true;
75         }
76     }
77 
78     auto height = 0_vl;
79 
80     unsigned long width;
81     bool          retval = true;
82 
83     this->get_dimensions(height, width);
84     switch (ch) {
85     case 'l':
86     case KEY_RIGHT:
87         this->shift_left(width / 2);
88         break;
89 
90     case 'h':
91     case KEY_LEFT:
92         this->shift_left(-(width / 2));
93         break;
94     case 'L':
95     case KEY_SRIGHT:
96         this->shift_left(10);
97         break;
98     case 'H':
99     case KEY_SLEFT:
100         this->shift_left(-10);
101         break;
102 
103     case '\r':
104     case 'j':
105     case KEY_DOWN:
106         if (this->is_selectable()) {
107             this->shift_selection(1);
108         } else {
109             this->shift_top(1_vl);
110         }
111         break;
112 
113     case 'k':
114     case KEY_UP:
115         if (this->is_selectable()) {
116             this->shift_selection(-1);
117         } else {
118             this->shift_top(-1_vl);
119         }
120         break;
121 
122     case 'b':
123     case KEY_BACKSPACE:
124     case KEY_PPAGE:
125         this->shift_top(-(this->rows_available(this->lv_top, RD_UP) - 1_vl));
126         break;
127 
128     case ' ':
129     case KEY_NPAGE:
130         this->shift_top(this->rows_available(this->lv_top, RD_DOWN) - 1_vl);
131         break;
132 
133     case 'g':
134     case KEY_HOME:
135         if (this->is_selectable()) {
136             this->set_selection(0_vl);
137         } else {
138             this->set_top(0_vl);
139         }
140         break;
141 
142     case 'G':
143     case KEY_END: {
144             vis_line_t last_line(this->get_inner_height() - 1);
145             vis_line_t tail_bottom(this->get_top_for_last_row());
146 
147             if (this->is_selectable()) {
148                 this->set_selection(last_line);
149             } else if (this->get_top() == last_line) {
150                 this->set_top(tail_bottom);
151             }
152             else if (tail_bottom <= this->get_top()) {
153                 this->set_top(last_line);
154             }
155             else {
156                 this->set_top(tail_bottom);
157             }
158         }
159         break;
160 
161     case ']':
162         {
163             double tenth = ((double)this->get_inner_height()) / 10.0;
164 
165             this->shift_top(vis_line_t((int)tenth));
166         }
167         break;
168 
169     case '[':
170     case 'B':
171         {
172             double tenth = ((double)this->get_inner_height()) / 10.0;
173 
174             this->shift_top(vis_line_t((int)-tenth));
175         }
176         break;
177 
178     default:
179         retval = false;
180         break;
181     }
182 
183     return retval;
184 }
185 
do_update()186 void listview_curses::do_update()
187 {
188     if (this->lv_window == nullptr || this->lv_height == 0) {
189         view_curses::do_update();
190         return;
191     }
192 
193     if (this->vc_needs_update) {
194         view_colors &vc = view_colors::singleton();
195         vis_line_t        height, row;
196         attr_line_t       overlay_line;
197         struct line_range lr;
198         unsigned long     width, wrap_width;
199         size_t            row_count;
200         int y = this->lv_y, bottom;
201         attr_t role_attrs = vc.attrs_for_role(this->vc_default_role);
202 
203         this->get_dimensions(height, width);
204 
205         if (this->vc_width > 0) {
206             width = std::min((unsigned long) this->vc_width, width);
207         }
208 
209         wrap_width = width - (this->lv_word_wrap ? 1 : this->lv_show_scrollbar ? 1 : 0);
210 
211         row_count = this->get_inner_height();
212         row   = this->lv_top;
213         bottom = y + height;
214         vector<attr_line_t> rows(min((size_t) height, row_count - (int) this->lv_top));
215         this->lv_source->listview_value_for_rows(*this, row, rows);
216         while (y < bottom) {
217             lr.lr_start = this->lv_left;
218             lr.lr_end   = this->lv_left + wrap_width;
219             if (this->lv_overlay_source != nullptr &&
220                 this->lv_overlay_source->list_value_for_overlay(
221                     *this,
222                     y - this->lv_y, bottom - this->lv_y,
223                     row,
224                     overlay_line)) {
225                 mvwattrline(this->lv_window, y, this->lv_x, overlay_line, lr);
226                 overlay_line.clear();
227                 ++y;
228             }
229             else if (row < (int)row_count) {
230                 attr_line_t &al = rows[row - this->lv_top];
231 
232                 do {
233                     mvwattrline(this->lv_window, y, this->lv_x, al, lr,
234                                 this->vc_default_role);
235                     if (this->lv_word_wrap) {
236                         mvwhline(this->lv_window, y, this->lv_x + wrap_width, ' ', width - wrap_width);
237                     }
238                     lr.lr_start += wrap_width;
239                     lr.lr_end += wrap_width;
240                     ++y;
241                 } while (this->lv_word_wrap && y < bottom && lr.lr_start < (int)al.length());
242                 ++row;
243             }
244             else {
245                 wattron(this->lv_window, role_attrs);
246                 mvwhline(this->lv_window, y, this->lv_x, ' ', width);
247                 wattroff(this->lv_window, role_attrs);
248                 ++y;
249             }
250         }
251 
252         if (this->lv_show_scrollbar) {
253             double progress = 1.0;
254             double coverage = 1.0;
255             double adjusted_height = (double)row_count / (double)height;
256             vis_line_t lines;
257 
258             if (row_count > 0) {
259                 progress = (double)this->lv_top / (double)row_count;
260                 coverage = (double)height / (double)row_count;
261             }
262 
263             y = this->lv_y + (int)(progress * (double)height);
264             lines = vis_line_t(y + min((int) height, (int)(coverage * (double)height)));
265 
266             for (unsigned int gutter_y = this->lv_y;
267                  gutter_y < (this->lv_y + height);
268                  gutter_y++) {
269                 int range_start = 0, range_end;
270                 view_colors::role_t role = this->vc_default_role;
271                 view_colors::role_t bar_role = view_colors::VCR_SCROLLBAR;
272                 int attrs;
273                 chtype ch = ACS_VLINE;
274 
275                 if (row_count > 0) {
276                     range_start = (double)(gutter_y - this->lv_y) * adjusted_height;
277                 }
278                 range_end = range_start + adjusted_height;
279 
280                 this->lv_gutter_source->listview_gutter_value_for_range(
281                     *this, range_start, range_end, ch, role, bar_role);
282                 if (gutter_y >= (unsigned int)y && gutter_y <= (unsigned int)lines) {
283                     role = bar_role;
284                 }
285                 attrs = vc.attrs_for_role(role);
286                 wattron(this->lv_window, attrs);
287                 mvwaddch(this->lv_window, gutter_y, this->lv_x + width - 1, ch);
288                 wattroff(this->lv_window, attrs);
289             }
290             wmove(this->lv_window, this->lv_y + height - 1, this->lv_x);
291         }
292 
293         if (this->lv_show_bottom_border) {
294             cchar_t row_ch[width];
295             int y = this->lv_y + height - 1;
296 
297             mvwin_wchnstr(this->lv_window, y, this->lv_x, row_ch, width - 1);
298             for (unsigned long lpc = 0; lpc < width - 1; lpc++) {
299                 row_ch[lpc].attr |= A_UNDERLINE;
300             }
301             mvwadd_wchnstr(this->lv_window, y, this->lv_x, row_ch, width - 1);
302         }
303 
304         this->vc_needs_update = false;
305     }
306 
307     view_curses::do_update();
308 
309 #if 0
310     else if (this->lv_overlay_needs_update && this->lv_overlay_source != NULL) {
311         vis_line_t y(this->lv_y), height, bottom;
312         attr_line_t overlay_line;
313         unsigned long width, wrap_width;
314         struct line_range lr;
315 
316         this->lv_overlay_source->list_overlay_count(*this);
317         this->get_dimensions(height, width);
318         wrap_width = width - (this->lv_word_wrap ? 1 : this->lv_show_scrollbar ? 1 : 0);
319 
320         lr.lr_start = this->lv_left;
321         lr.lr_end   = this->lv_left + wrap_width;
322 
323         bottom = y + height;
324         while (y < bottom) {
325             if (this->lv_overlay_source->list_value_for_overlay(
326                     *this,
327                     y - vis_line_t(this->lv_y),
328                 overlay_line)) {
329                 this->mvwattrline(this->lv_window, y, this->lv_x, overlay_line, lr);
330                 overlay_line.clear();
331             }
332             ++y;
333         }
334     }
335 #endif
336 }
337 
shift_selection(int offset)338 void listview_curses::shift_selection(int offset)
339 {
340     vis_line_t new_selection = this->lv_selection + vis_line_t(offset);
341 
342     if (new_selection >= 0_vl &&
343         new_selection < this->get_inner_height()) {
344         this->set_selection(new_selection);
345         this->scroll_selection_into_view();
346     } else if (!alerter::singleton().chime()) {
347         // XXX Disabling for now...
348         // this->delegate_scroll_out();
349     }
350 }
351 
scroll_polarity(mouse_button_t button)352 static int scroll_polarity(mouse_button_t button)
353 {
354     return button == mouse_button_t::BUTTON_SCROLL_UP ? -1 : 1;
355 }
356 
handle_mouse(mouse_event & me)357 bool listview_curses::handle_mouse(mouse_event &me)
358 {
359     vis_line_t inner_height, height;
360     struct timeval diff;
361     unsigned long width;
362 
363     timersub(&me.me_time, &this->lv_mouse_time, &diff);
364     this->get_dimensions(height, width);
365     inner_height = this->get_inner_height();
366 
367     switch (me.me_button) {
368         case mouse_button_t::BUTTON_SCROLL_UP:
369         case mouse_button_t::BUTTON_SCROLL_DOWN:
370             if (diff.tv_sec > 0 || diff.tv_usec > 80000) {
371                 this->lv_scroll_accel = 1;
372                 this->lv_scroll_velo = 0;
373             }
374             else {
375                 this->lv_scroll_accel += 2;
376             }
377             this->lv_scroll_velo += this->lv_scroll_accel;
378 
379             this->shift_top(vis_line_t(scroll_polarity(me.me_button) *
380                                        this->lv_scroll_velo),
381                             true);
382             break;
383         default:
384             break;
385     }
386     this->lv_mouse_time = me.me_time;
387 
388     if (me.me_button != mouse_button_t::BUTTON_LEFT ||
389         inner_height == 0 ||
390         (this->lv_mouse_mode != LV_MODE_DRAG && me.me_x < (int)(width - 2))) {
391         return false;
392     }
393 
394     if (me.me_state == mouse_button_state_t::BUTTON_STATE_RELEASED) {
395         this->lv_mouse_y = -1;
396         this->lv_mouse_mode = LV_MODE_NONE;
397         return true;
398     }
399 
400     int scroll_top, scroll_bottom, shift_amount = 0, new_top = 0;
401     double top_pct, bot_pct, pct;
402 
403     top_pct = (double)this->get_top() / (double)inner_height;
404     bot_pct = (double)this->get_bottom() / (double)inner_height;
405     scroll_top = (this->get_y() + (int)(top_pct * (double)height));
406     scroll_bottom = (this->get_y() + (int)(bot_pct * (double)height));
407 
408     if (this->lv_mouse_mode == LV_MODE_NONE) {
409         if ((scroll_top - 1) <= me.me_y && me.me_y <= (scroll_bottom + 1)) {
410             this->lv_mouse_mode        = LV_MODE_DRAG;
411             this->lv_mouse_y = me.me_y - scroll_top;
412         }
413         else if (me.me_y < scroll_top) {
414             this->lv_mouse_mode = LV_MODE_UP;
415         }
416         else {
417             this->lv_mouse_mode = LV_MODE_DOWN;
418         }
419     }
420 
421     switch (this->lv_mouse_mode) {
422     case LV_MODE_NONE:
423         require(0);
424         break;
425 
426     case LV_MODE_UP:
427         if (me.me_y < scroll_top) {
428             shift_amount = -1 * height;
429         }
430         break;
431 
432     case LV_MODE_DOWN:
433         if (me.me_y > scroll_bottom) {
434             shift_amount = height;
435         }
436         break;
437 
438     case LV_MODE_DRAG:
439         pct     = (double)inner_height / (double)height;
440         new_top = me.me_y - this->get_y() - this->lv_mouse_y;
441         new_top = (int)floor(((double)new_top * pct) + 0.5);
442         this->set_top(vis_line_t(new_top));
443         break;
444     }
445 
446     if (shift_amount != 0) {
447         this->shift_top(vis_line_t(shift_amount));
448     }
449 
450     return true;
451 }
452 
set_top(vis_line_t top,bool suppress_flash)453 void listview_curses::set_top(vis_line_t top, bool suppress_flash)
454 {
455     auto inner_height = this->get_inner_height();
456 
457     if (inner_height > 0 && top >= inner_height) {
458         top = vis_line_t(inner_height - 1);
459     }
460     if (top < 0 || (top > 0 && top >= inner_height)) {
461         if (suppress_flash == false) {
462             alerter::singleton().chime();
463         }
464     }
465     else if (this->lv_top != top) {
466         this->lv_top = top;
467         if (this->lv_selectable) {
468             if (this->lv_selection < top) {
469                 this->lv_selection = top;
470             } else {
471                 auto bot = this->get_bottom();
472 
473                 if (bot != -1_vl) {
474                     if (this->lv_selection > bot) {
475                         this->lv_selection = bot;
476                     }
477                 }
478             }
479         }
480         this->invoke_scroll();
481         this->set_needs_update();
482     }
483 }
484