summaryrefslogtreecommitdiffstats
path: root/src/listview_curses.cc
diff options
context:
space:
mode:
Diffstat (limited to 'src/listview_curses.cc')
-rw-r--r--src/listview_curses.cc631
1 files changed, 631 insertions, 0 deletions
diff --git a/src/listview_curses.cc b/src/listview_curses.cc
new file mode 100644
index 0000000..c619861
--- /dev/null
+++ b/src/listview_curses.cc
@@ -0,0 +1,631 @@
+/**
+ * Copyright (c) 2007-2012, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @file listview_curses.cc
+ */
+
+#include <cmath>
+
+#include "listview_curses.hh"
+
+#include <sys/time.h>
+#include <time.h>
+
+#include "base/lnav_log.hh"
+#include "config.h"
+
+list_gutter_source listview_curses::DEFAULT_GUTTER_SOURCE;
+
+listview_curses::listview_curses() : lv_scroll(noop_func{}) {}
+
+void
+listview_curses::reload_data()
+{
+ if (this->lv_source == nullptr) {
+ this->lv_top = 0_vl;
+ this->lv_left = 0;
+ } else {
+ if (this->lv_top >= this->get_inner_height()) {
+ this->lv_top
+ = std::max(0_vl, vis_line_t(this->get_inner_height() - 1));
+ }
+ if (this->lv_selectable) {
+ if (this->get_inner_height() == 0) {
+ this->lv_selection = 0_vl;
+ } else if (this->lv_selection >= this->get_inner_height()) {
+ this->set_selection(this->get_inner_height() - 1_vl);
+ } else {
+ auto curr_sel = this->lv_selection;
+
+ this->lv_selection = -1_vl;
+ this->set_selection(curr_sel);
+ }
+ }
+ }
+ this->vc_needs_update = true;
+ this->invoke_scroll();
+}
+
+bool
+listview_curses::handle_key(int ch)
+{
+ for (auto& lv_input_delegate : this->lv_input_delegates) {
+ if (lv_input_delegate->list_input_handle_key(*this, ch)) {
+ return true;
+ }
+ }
+
+ auto height = 0_vl;
+
+ unsigned long width;
+ bool retval = true;
+
+ this->get_dimensions(height, width);
+ switch (ch) {
+ case 'l':
+ case KEY_RIGHT:
+ this->shift_left(width / 2);
+ break;
+
+ case 'h':
+ case KEY_LEFT:
+ this->shift_left(-(width / 2));
+ break;
+ case 'L':
+ case KEY_SRIGHT:
+ this->shift_left(10);
+ break;
+ case 'H':
+ case KEY_SLEFT:
+ this->shift_left(-10);
+ break;
+
+ case '\r':
+ case 'j':
+ case KEY_DOWN:
+ if (this->is_selectable()) {
+ this->shift_selection(1);
+ } else {
+ this->shift_top(1_vl);
+ }
+ break;
+
+ case 'k':
+ case KEY_UP:
+ if (this->is_selectable()) {
+ this->shift_selection(-1);
+ } else {
+ this->shift_top(-1_vl);
+ }
+ break;
+
+ case 'b':
+ case KEY_BACKSPACE:
+ case KEY_PPAGE:
+ this->shift_top(
+ -(this->rows_available(this->lv_top, RD_UP) - 1_vl));
+ break;
+
+ case ' ':
+ case KEY_NPAGE:
+ this->shift_top(this->rows_available(this->lv_top, RD_DOWN) - 1_vl);
+ break;
+
+ case 'g':
+ case KEY_HOME:
+ if (this->is_selectable()) {
+ this->set_selection(0_vl);
+ } else {
+ this->set_top(0_vl);
+ }
+ break;
+
+ case 'G':
+ case KEY_END: {
+ vis_line_t last_line(this->get_inner_height() - 1);
+ vis_line_t tail_bottom(this->get_top_for_last_row());
+
+ if (this->is_selectable()) {
+ this->set_selection(last_line);
+ } else if (this->get_top() == last_line) {
+ this->set_top(tail_bottom);
+ } else if (tail_bottom <= this->get_top()) {
+ this->set_top(last_line);
+ } else {
+ this->set_top(tail_bottom);
+ }
+ } break;
+
+ case ']': {
+ double tenth = ((double) this->get_inner_height()) / 10.0;
+
+ this->shift_top(vis_line_t((int) tenth));
+ } break;
+
+ case '[':
+ case 'B': {
+ double tenth = ((double) this->get_inner_height()) / 10.0;
+
+ this->shift_top(vis_line_t((int) -tenth));
+ } break;
+
+ default:
+ retval = false;
+ break;
+ }
+
+ return retval;
+}
+
+void
+listview_curses::do_update()
+{
+ if (this->lv_window == nullptr || this->lv_height == 0) {
+ view_curses::do_update();
+ return;
+ }
+
+ if (this->vc_needs_update) {
+ view_colors& vc = view_colors::singleton();
+ vis_line_t height, row;
+ attr_line_t overlay_line;
+ struct line_range lr;
+ unsigned long width, wrap_width;
+ int y = this->lv_y, bottom;
+ auto role_attrs = vc.attrs_for_role(this->vc_default_role);
+
+ this->get_dimensions(height, width);
+
+ if (this->vc_width > 0) {
+ width = std::min((unsigned long) this->vc_width, width);
+ }
+
+ wrap_width = width;
+ if (this->lv_show_scrollbar) {
+ wrap_width -= 1;
+ }
+
+ size_t row_count = this->get_inner_height();
+ row = this->lv_top;
+ bottom = y + height;
+ std::vector<attr_line_t> rows(
+ std::min((size_t) height, row_count - (int) this->lv_top));
+ this->lv_source->listview_value_for_rows(*this, row, rows);
+ while (y < bottom) {
+ lr.lr_start = this->lv_left;
+ lr.lr_end = this->lv_left + wrap_width;
+ if (this->lv_overlay_source != nullptr
+ && this->lv_overlay_source->list_value_for_overlay(
+ *this,
+ y - this->lv_y,
+ bottom - this->lv_y,
+ row,
+ overlay_line))
+ {
+ mvwattrline(this->lv_window, y, this->lv_x, overlay_line, lr);
+ overlay_line.clear();
+ ++y;
+ } else if (row < (int) row_count) {
+ auto& al = rows[row - this->lv_top];
+
+ size_t remaining = 0;
+ do {
+ remaining = mvwattrline(this->lv_window,
+ y,
+ this->lv_x,
+ al,
+ lr,
+ this->vc_default_role);
+ if (this->lv_word_wrap) {
+ mvwhline(this->lv_window,
+ y,
+ this->lv_x + wrap_width,
+ ' ',
+ width - wrap_width);
+ }
+ lr.lr_start += wrap_width;
+ lr.lr_end += wrap_width;
+ ++y;
+ } while (this->lv_word_wrap && y < bottom && remaining > 0);
+ ++row;
+ } else {
+ wattr_set(this->lv_window,
+ role_attrs.ta_attrs,
+ vc.ensure_color_pair(role_attrs.ta_fg_color,
+ role_attrs.ta_bg_color),
+ nullptr);
+ mvwhline(this->lv_window, y, this->lv_x, ' ', width);
+ ++y;
+ }
+ }
+
+ if (this->lv_show_scrollbar) {
+ double progress = 1.0;
+ double coverage = 1.0;
+ double adjusted_height = (double) row_count / (double) height;
+ vis_line_t lines;
+
+ if (row_count > 0) {
+ progress = (double) this->lv_top / (double) row_count;
+ coverage = (double) height / (double) row_count;
+ }
+
+ y = this->lv_y + (int) (progress * (double) height);
+ lines = vis_line_t(
+ y + std::min((int) height, (int) (coverage * (double) height)));
+
+ for (unsigned int gutter_y = this->lv_y;
+ gutter_y < (this->lv_y + height);
+ gutter_y++)
+ {
+ int range_start = 0, range_end;
+ role_t role = this->vc_default_role;
+ role_t bar_role = role_t::VCR_SCROLLBAR;
+ text_attrs attrs;
+ chtype ch = ACS_VLINE;
+
+ if (row_count > 0) {
+ range_start
+ = (double) (gutter_y - this->lv_y) * adjusted_height;
+ }
+ range_end = range_start + adjusted_height;
+
+ this->lv_gutter_source->listview_gutter_value_for_range(
+ *this, range_start, range_end, ch, role, bar_role);
+ if (gutter_y >= (unsigned int) y
+ && gutter_y <= (unsigned int) lines)
+ {
+ role = bar_role;
+ }
+ attrs = vc.attrs_for_role(role);
+ wattr_set(
+ this->lv_window,
+ attrs.ta_attrs,
+ vc.ensure_color_pair(attrs.ta_fg_color, attrs.ta_bg_color),
+ nullptr);
+ mvwaddch(this->lv_window, gutter_y, this->lv_x + width - 1, ch);
+ }
+ wmove(this->lv_window, this->lv_y + height - 1, this->lv_x);
+ }
+
+ if (this->lv_show_bottom_border) {
+ cchar_t row_ch[width];
+ int y = this->lv_y + height - 1;
+
+ mvwin_wchnstr(this->lv_window, y, this->lv_x, row_ch, width - 1);
+ for (unsigned long lpc = 0; lpc < width - 1; lpc++) {
+ row_ch[lpc].attr |= A_UNDERLINE;
+ }
+ mvwadd_wchnstr(this->lv_window, y, this->lv_x, row_ch, width - 1);
+ }
+
+ this->vc_needs_update = false;
+ }
+
+ view_curses::do_update();
+
+#if 0
+ else if (this->lv_overlay_needs_update && this->lv_overlay_source != NULL) {
+ vis_line_t y(this->lv_y), height, bottom;
+ attr_line_t overlay_line;
+ unsigned long width, wrap_width;
+ struct line_range lr;
+
+ this->lv_overlay_source->list_overlay_count(*this);
+ this->get_dimensions(height, width);
+ wrap_width = width - (this->lv_word_wrap ? 1 : this->lv_show_scrollbar ? 1 : 0);
+
+ lr.lr_start = this->lv_left;
+ lr.lr_end = this->lv_left + wrap_width;
+
+ bottom = y + height;
+ while (y < bottom) {
+ if (this->lv_overlay_source->list_value_for_overlay(
+ *this,
+ y - vis_line_t(this->lv_y),
+ overlay_line)) {
+ this->mvwattrline(this->lv_window, y, this->lv_x, overlay_line, lr);
+ overlay_line.clear();
+ }
+ ++y;
+ }
+ }
+#endif
+}
+
+void
+listview_curses::shift_selection(int offset)
+{
+ vis_line_t new_selection = this->lv_selection + vis_line_t(offset);
+
+ if (new_selection >= 0_vl && new_selection < this->get_inner_height()) {
+ this->set_selection(new_selection);
+ this->scroll_selection_into_view();
+ }
+}
+
+static int
+scroll_polarity(mouse_button_t button)
+{
+ return button == mouse_button_t::BUTTON_SCROLL_UP ? -1 : 1;
+}
+
+bool
+listview_curses::handle_mouse(mouse_event& me)
+{
+ vis_line_t inner_height, height;
+ struct timeval diff;
+ unsigned long width;
+
+ timersub(&me.me_time, &this->lv_mouse_time, &diff);
+ this->get_dimensions(height, width);
+ inner_height = this->get_inner_height();
+
+ switch (me.me_button) {
+ case mouse_button_t::BUTTON_SCROLL_UP:
+ case mouse_button_t::BUTTON_SCROLL_DOWN:
+ if (diff.tv_sec > 0 || diff.tv_usec > 80000) {
+ this->lv_scroll_accel = 1;
+ this->lv_scroll_velo = 0;
+ } else {
+ this->lv_scroll_accel += 2;
+ }
+ this->lv_scroll_velo += this->lv_scroll_accel;
+
+ this->shift_top(vis_line_t(scroll_polarity(me.me_button)
+ * this->lv_scroll_velo),
+ true);
+ break;
+ default:
+ break;
+ }
+ this->lv_mouse_time = me.me_time;
+
+ if (me.me_button != mouse_button_t::BUTTON_LEFT || inner_height == 0
+ || (this->lv_mouse_mode != lv_mode_t::DRAG
+ && me.me_x < (int) (width - 2)))
+ {
+ return false;
+ }
+
+ if (me.me_state == mouse_button_state_t::BUTTON_STATE_RELEASED) {
+ this->lv_mouse_y = -1;
+ this->lv_mouse_mode = lv_mode_t::NONE;
+ return true;
+ }
+
+ int scroll_top, scroll_bottom, shift_amount = 0, new_top = 0;
+ double top_pct, bot_pct, pct;
+
+ top_pct = (double) this->get_top() / (double) inner_height;
+ bot_pct = (double) this->get_bottom() / (double) inner_height;
+ scroll_top = (this->get_y() + (int) (top_pct * (double) height));
+ scroll_bottom = (this->get_y() + (int) (bot_pct * (double) height));
+
+ if (this->lv_mouse_mode == lv_mode_t::NONE) {
+ if ((scroll_top - 1) <= me.me_y && me.me_y <= (scroll_bottom + 1)) {
+ this->lv_mouse_mode = lv_mode_t::DRAG;
+ this->lv_mouse_y = me.me_y - scroll_top;
+ } else if (me.me_y < scroll_top) {
+ this->lv_mouse_mode = lv_mode_t::UP;
+ } else {
+ this->lv_mouse_mode = lv_mode_t::DOWN;
+ }
+ }
+
+ switch (this->lv_mouse_mode) {
+ case lv_mode_t::NONE:
+ require(0);
+ break;
+
+ case lv_mode_t::UP:
+ if (me.me_y < scroll_top) {
+ shift_amount = -1 * height;
+ }
+ break;
+
+ case lv_mode_t::DOWN:
+ if (me.me_y > scroll_bottom) {
+ shift_amount = height;
+ }
+ break;
+
+ case lv_mode_t::DRAG:
+ pct = (double) inner_height / (double) height;
+ new_top = me.me_y - this->get_y() - this->lv_mouse_y;
+ new_top = (int) floor(((double) new_top * pct) + 0.5);
+ this->set_top(vis_line_t(new_top));
+ break;
+ }
+
+ if (shift_amount != 0) {
+ this->shift_top(vis_line_t(shift_amount));
+ }
+
+ return true;
+}
+
+void
+listview_curses::set_top(vis_line_t top, bool suppress_flash)
+{
+ auto inner_height = this->get_inner_height();
+
+ if (inner_height > 0 && top >= inner_height) {
+ top = vis_line_t(inner_height - 1);
+ }
+ if (top < 0 || (top > 0 && top >= inner_height)) {
+ if (suppress_flash == false) {
+ alerter::singleton().chime("invalid top");
+ }
+ } else if (this->lv_top != top) {
+ this->lv_top = top;
+ if (this->lv_selectable) {
+ if (this->lv_selection < 0_vl) {
+ } else if (this->lv_selection < top) {
+ this->set_selection(top);
+ } else {
+ auto bot = this->get_bottom();
+ unsigned long width;
+ vis_line_t height;
+
+ this->get_dimensions(height, width);
+
+ if (bot != -1_vl && (bot - top) >= (height - 1)) {
+ if (this->lv_selection > (bot - this->lv_tail_space)) {
+ this->set_selection(bot - this->lv_tail_space);
+ }
+ }
+ }
+ }
+ this->invoke_scroll();
+ this->set_needs_update();
+ }
+}
+
+vis_line_t
+listview_curses::get_bottom() const
+{
+ auto retval = this->lv_top;
+ auto avail = this->rows_available(retval, RD_DOWN);
+
+ if (avail > 0) {
+ retval += vis_line_t(avail - 1);
+ }
+
+ return retval;
+}
+
+vis_line_t
+listview_curses::rows_available(vis_line_t line,
+ listview_curses::row_direction_t dir) const
+{
+ unsigned long width;
+ vis_line_t height;
+ vis_line_t retval(0);
+
+ this->get_dimensions(height, width);
+ if (this->lv_word_wrap) {
+ size_t row_count = this->lv_source->listview_rows(*this);
+
+ width -= 1;
+ while ((height > 0) && (line >= 0) && ((size_t) line < row_count)) {
+ size_t len = this->lv_source->listview_size_for_row(*this, line);
+
+ do {
+ len -= std::min((size_t) width, len);
+ --height;
+ } while (len > 0);
+ line += vis_line_t(dir);
+ if (height >= 0) {
+ ++retval;
+ }
+ }
+ } else {
+ switch (dir) {
+ case RD_UP:
+ retval = std::min(height, line + 1_vl);
+ break;
+ case RD_DOWN:
+ retval = std::min(
+ height,
+ vis_line_t(this->lv_source->listview_rows(*this) - line));
+ break;
+ }
+ }
+
+ return retval;
+}
+
+void
+listview_curses::scroll_selection_into_view()
+{
+ unsigned long width;
+ vis_line_t height;
+
+ this->get_dimensions(height, width);
+ if (height <= 0) {
+ return;
+ }
+ if (this->lv_selection < 0_vl) {
+ this->set_top(0_vl);
+ } else if (this->lv_selection
+ >= (this->lv_top + height - this->lv_tail_space - 1_vl))
+ {
+ this->set_top(this->lv_selection - height + 1_vl + this->lv_tail_space,
+ true);
+ } else if (this->lv_selection < this->lv_top) {
+ this->set_top(this->lv_selection, true);
+ }
+}
+
+void
+listview_curses::set_selection(vis_line_t sel)
+{
+ if (this->lv_selectable) {
+ if (this->lv_selection == sel) {
+ return;
+ }
+ if (sel == -1_vl) {
+ this->lv_selection = sel;
+ this->lv_source->listview_selection_changed(*this);
+ this->set_needs_update();
+ return;
+ }
+
+ auto inner_height = this->get_inner_height();
+ if (sel >= 0_vl && sel < inner_height) {
+ auto found = false;
+ vis_line_t step;
+
+ if (sel == 0_vl) {
+ step = 1_vl;
+ } else if (sel == inner_height - 1_vl) {
+ step = -1_vl;
+ } else if (sel < this->lv_selection) {
+ step = -1_vl;
+ } else {
+ step = 1_vl;
+ }
+ while (sel < inner_height) {
+ if (this->lv_source->listview_is_row_selectable(*this, sel)) {
+ found = true;
+ break;
+ }
+ sel += step;
+ }
+ if (found) {
+ this->lv_selection = sel;
+ this->scroll_selection_into_view();
+ this->lv_source->listview_selection_changed(*this);
+ this->set_needs_update();
+ }
+ }
+ } else {
+ this->set_top(sel);
+ }
+}