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