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.hh 30 */ 31 32 #ifndef listview_curses_hh 33 #define listview_curses_hh 34 35 #include <sys/types.h> 36 37 #include <list> 38 #include <string> 39 #include <utility> 40 #include <vector> 41 42 #include "base/func_util.hh" 43 #include "view_curses.hh" 44 #include "vis_line.hh" 45 46 class listview_curses; 47 48 /** 49 * Data source for lines to be displayed by the listview_curses object. 50 */ 51 class list_data_source { 52 public: 53 virtual ~list_data_source() = default; 54 55 /** @return The number of rows in the list. */ 56 virtual size_t listview_rows(const listview_curses &lv) = 0; 57 listview_width(const listview_curses & lv)58 virtual size_t listview_width(const listview_curses &lv) { 59 return INT_MAX; 60 }; 61 62 /** 63 * Get the string value for a row in the list. 64 * 65 * @param row The row number. 66 * @param value_out The destination for the string value. 67 */ 68 virtual void listview_value_for_rows(const listview_curses &lv, 69 vis_line_t start_row, 70 std::vector<attr_line_t> &rows_out) = 0; 71 72 virtual size_t listview_size_for_row(const listview_curses &lv, 73 vis_line_t row) = 0; 74 listview_source_name(const listview_curses & lv)75 virtual std::string listview_source_name(const listview_curses &lv) { 76 return ""; 77 }; 78 }; 79 80 class list_gutter_source { 81 public: 82 virtual ~list_gutter_source() = default; 83 listview_gutter_value_for_range(const listview_curses & lv,int start,int end,chtype & ch_out,view_colors::role_t & role_out,view_colors::role_t & bar_role_out)84 virtual void listview_gutter_value_for_range( 85 const listview_curses &lv, int start, int end, 86 chtype &ch_out, 87 view_colors::role_t &role_out, 88 view_colors::role_t &bar_role_out) { 89 ch_out = ACS_VLINE; 90 }; 91 }; 92 93 class list_overlay_source { 94 public: 95 virtual ~list_overlay_source() = default; 96 97 virtual bool list_value_for_overlay(const listview_curses &lv, 98 int y, int bottom, 99 vis_line_t line, 100 attr_line_t &value_out) = 0; 101 }; 102 103 class list_input_delegate { 104 public: 105 virtual ~list_input_delegate() = default; 106 107 virtual bool list_input_handle_key(listview_curses &lv, int ch) = 0; 108 list_input_handle_scroll_out(listview_curses & lv)109 virtual void list_input_handle_scroll_out(listview_curses &lv) { 110 }; 111 }; 112 113 /** 114 * View that displays a list of lines that can optionally contain highlighting. 115 */ 116 class listview_curses 117 : public view_curses, private log_state_dumper { 118 public: 119 using action = std::function<void(listview_curses*)>; 120 121 listview_curses(); 122 set_title(const std::string & title)123 void set_title(const std::string &title) { 124 this->lv_title = title; 125 }; 126 get_title() const127 const std::string &get_title() const { 128 return this->lv_title; 129 }; 130 131 /** @param src The data source delegate. */ set_data_source(list_data_source * src)132 void set_data_source(list_data_source *src) 133 { 134 this->lv_source = src; 135 this->invoke_scroll(); 136 this->reload_data(); 137 }; 138 139 /** @return The data source delegate. */ get_data_source() const140 list_data_source *get_data_source() const { return this->lv_source; }; 141 set_gutter_source(list_gutter_source * src)142 void set_gutter_source(list_gutter_source *src) { 143 this->lv_gutter_source = src; 144 }; 145 146 /** @param src The data source delegate. */ set_overlay_source(list_overlay_source * src)147 listview_curses &set_overlay_source(list_overlay_source *src) { 148 this->lv_overlay_source = src; 149 this->reload_data(); 150 151 return *this; 152 }; 153 154 /** @return The overlay source delegate. */ get_overlay_source()155 list_overlay_source *get_overlay_source() 156 { 157 return this->lv_overlay_source; 158 }; 159 add_input_delegate(list_input_delegate & lid)160 listview_curses &add_input_delegate(list_input_delegate &lid) { 161 this->lv_input_delegates.push_back(&lid); 162 163 return *this; 164 }; 165 166 /** 167 * @param va The action to invoke when the view is scrolled. 168 * @todo Allow multiple observers. 169 */ set_scroll_action(action va)170 void set_scroll_action(action va) { this->lv_scroll = std::move(va); }; 171 set_show_scrollbar(bool ss)172 void set_show_scrollbar(bool ss) { this->lv_show_scrollbar = ss; }; get_show_scrollbar() const173 bool get_show_scrollbar() const { return this->lv_show_scrollbar; }; 174 set_show_bottom_border(bool val)175 void set_show_bottom_border(bool val) { 176 if (this->lv_show_bottom_border != val) { 177 this->lv_show_bottom_border = val; 178 this->set_needs_update(); 179 } 180 }; get_show_bottom_border() const181 bool get_show_bottom_border() const { return this->lv_show_bottom_border; }; 182 set_selectable(bool sel)183 void set_selectable(bool sel) { 184 this->lv_selectable = sel; 185 }; 186 is_selectable() const187 bool is_selectable() const { 188 return this->lv_selectable; 189 }; 190 set_selection(vis_line_t sel)191 void set_selection(vis_line_t sel) { 192 if (this->lv_selection != sel) { 193 this->lv_selection = sel; 194 this->scroll_selection_into_view(); 195 this->set_needs_update(); 196 } 197 } 198 scroll_selection_into_view()199 void scroll_selection_into_view() { 200 unsigned long width; 201 vis_line_t height; 202 203 this->get_dimensions(height, width); 204 if (height <= 0) { 205 return; 206 } 207 if (this->lv_selection >= (this->lv_top + height - 1)) { 208 this->set_top(this->lv_selection - height + 2_vl, true); 209 } else if (this->lv_selection < this->lv_top) { 210 this->set_top(this->lv_selection, true); 211 } 212 } 213 214 void shift_selection(int offset); 215 get_selection() const216 vis_line_t get_selection() const { 217 return this->lv_selection; 218 } 219 set_word_wrap(bool ww)220 listview_curses &set_word_wrap(bool ww) { 221 bool scroll_down = this->lv_top >= this->get_top_for_last_row(); 222 223 this->lv_word_wrap = ww; 224 if (ww && scroll_down && this->lv_top < this->get_top_for_last_row()) { 225 this->lv_top = this->get_top_for_last_row(); 226 } 227 if (ww) { 228 this->lv_left = 0; 229 } 230 this->set_needs_update(); 231 232 return *this; 233 }; 234 get_word_wrap() const235 bool get_word_wrap() const { return this->lv_word_wrap; }; 236 237 enum row_direction_t { 238 RD_UP = -1, 239 RD_DOWN = 1, 240 }; 241 rows_available(vis_line_t line,row_direction_t dir) const242 vis_line_t rows_available(vis_line_t line, row_direction_t dir) const { 243 unsigned long width; 244 vis_line_t height; 245 vis_line_t retval(0); 246 247 this->get_dimensions(height, width); 248 if (this->lv_word_wrap) { 249 size_t row_count = this->lv_source->listview_rows(*this); 250 251 width -= 1; 252 while ((height > 0) && (line >= 0) && ((size_t)line < row_count)) { 253 size_t len = this->lv_source->listview_size_for_row(*this, line); 254 255 do { 256 len -= std::min((size_t)width, len); 257 --height; 258 } while (len > 0); 259 line += vis_line_t(dir); 260 if (height >= 0) { 261 ++retval; 262 } 263 } 264 } 265 else { 266 switch (dir) { 267 case RD_UP: 268 retval = std::min(height, line + 1_vl); 269 break; 270 case RD_DOWN: 271 retval = std::min(height, 272 vis_line_t(this->lv_source->listview_rows(*this) - line)); 273 break; 274 } 275 } 276 277 return retval; 278 }; 279 280 template<typename F> map_top_row(F func)281 auto map_top_row(F func) -> typename std::result_of<F(const attr_line_t&)>::type { 282 if (this->get_inner_height() == 0) { 283 return nonstd::nullopt; 284 } 285 286 std::vector<attr_line_t> top_line{1}; 287 288 this->lv_source->listview_value_for_rows(*this, this->lv_top, top_line); 289 return func(top_line[0]); 290 } 291 292 /** @param win The curses window this view is attached to. */ set_window(WINDOW * win)293 void set_window(WINDOW *win) { this->lv_window = win; }; 294 295 /** @return The curses window this view is attached to. */ get_window() const296 WINDOW *get_window() const { return this->lv_window; }; 297 set_y(unsigned int y)298 void set_y(unsigned int y) 299 { 300 if (y != this->lv_y) { 301 this->lv_y = y; 302 this->set_needs_update(); 303 } 304 }; get_y() const305 unsigned int get_y() const { return this->lv_y; }; 306 set_x(unsigned int x)307 void set_x(unsigned int x) 308 { 309 if (x != this->lv_x) { 310 this->lv_x = x; 311 this->set_needs_update(); 312 } 313 }; get_x() const314 unsigned int get_x() const { return this->lv_x; }; 315 316 /** 317 * Set the line number to be displayed at the top of the view. If the 318 * value is invalid, flash() will be called. If the value is valid, the 319 * new value will be set and the scroll action called. 320 * 321 * @param top The new value for top. 322 * @param suppress_flash Don't call flash() if the top is out-of-bounds. 323 */ 324 void set_top(vis_line_t top, bool suppress_flash = false); 325 326 /** @return The line number that is displayed at the top. */ get_top() const327 vis_line_t get_top() const { return this->lv_top; }; 328 get_top_opt() const329 nonstd::optional<vis_line_t> get_top_opt() const { 330 if (this->get_inner_height() == 0_vl) { 331 return nonstd::nullopt; 332 } 333 334 return this->lv_top; 335 } 336 337 /** @return The line number that is displayed at the bottom. */ get_bottom() const338 vis_line_t get_bottom() const 339 { 340 auto retval = this->lv_top; 341 auto avail = this->rows_available(retval, RD_DOWN); 342 343 if (avail > 0) { 344 retval += vis_line_t(avail - 1); 345 } else { 346 retval = -1_vl; 347 } 348 349 return retval; 350 }; 351 get_top_for_last_row()352 vis_line_t get_top_for_last_row() { 353 auto retval = 0_vl; 354 355 if (this->get_inner_height() > 0) { 356 vis_line_t last_line(this->get_inner_height() - 1); 357 358 retval = last_line - vis_line_t(this->rows_available(last_line, RD_UP) - 1); 359 if ((retval + this->lv_tail_space) < this->get_inner_height()) { 360 retval += this->lv_tail_space; 361 } 362 } 363 364 return retval; 365 }; 366 367 /** @return True if the given line is visible. */ is_line_visible(vis_line_t line) const368 bool is_line_visible(vis_line_t line) const 369 { 370 return this->get_top() <= line && line <= this->get_bottom(); 371 }; 372 373 /** 374 * Shift the value of top by the given value. 375 * 376 * @param offset The amount to change top by. 377 * @param suppress_flash Don't call flash() if the offset is out-of-bounds. 378 * @return The final value of top. 379 */ shift_top(vis_line_t offset,bool suppress_flash=false)380 vis_line_t shift_top(vis_line_t offset, bool suppress_flash = false) 381 { 382 if (offset < 0 && this->lv_top == 0) { 383 if (suppress_flash == false) { 384 alerter::singleton().chime(); 385 } 386 } 387 else { 388 this->set_top(std::max(0_vl, this->lv_top + offset), suppress_flash); 389 } 390 391 return this->lv_top; 392 }; 393 394 395 /** 396 * Set the column number to be displayed at the left of the view. If the 397 * value is invalid, flash() will be called. If the value is valid, the 398 * new value will be set and the scroll action called. 399 * 400 * @param left The new value for left. 401 */ set_left(unsigned int left)402 void set_left(unsigned int left) 403 { 404 if (this->lv_left == left) { 405 return; 406 } 407 408 if (left > this->lv_left) { 409 unsigned long width; 410 vis_line_t height; 411 412 this->get_dimensions(height, width); 413 if ((this->get_inner_width() - this->lv_left) <= width) { 414 alerter::singleton().chime(); 415 return; 416 } 417 } 418 419 this->lv_left = left; 420 this->invoke_scroll(); 421 this->set_needs_update(); 422 }; 423 424 /** @return The column number that is displayed at the left. */ get_left() const425 unsigned int get_left() const { return this->lv_left; }; 426 427 /** 428 * Shift the value of left by the given value. 429 * 430 * @param offset The amount to change top by. 431 * @return The final value of top. 432 */ shift_left(int offset)433 unsigned int shift_left(int offset) 434 { 435 if (this->lv_word_wrap) { 436 alerter::singleton().chime(); 437 } 438 else if (offset < 0 && this->lv_left < (unsigned int)-offset) { 439 this->set_left(0); 440 } 441 else { 442 this->set_left(this->lv_left + offset); 443 } 444 445 return this->lv_left; 446 }; 447 448 /** 449 * Set the height of the view. A value greater than one is considered to 450 * be an absolute size. A value less than or equal to zero makes the 451 * height relative to the size of the enclosing window. 452 * 453 * @height The new height. 454 */ set_height(vis_line_t height)455 void set_height(vis_line_t height) 456 { 457 if (this->lv_height != height) { 458 this->lv_height = height; 459 this->set_needs_update(); 460 } 461 }; 462 463 /** @return The absolute or relative height of the window. */ get_height() const464 vis_line_t get_height() const { return this->lv_height; }; 465 466 /** @return The number of rows of data in this view's source data. */ get_inner_height() const467 vis_line_t get_inner_height() const 468 { 469 return vis_line_t(this->lv_source == nullptr ? 0 : 470 this->lv_source->listview_rows(*this)); 471 }; 472 get_inner_width() const473 size_t get_inner_width() const { 474 return this->lv_source == nullptr ? 0 : 475 this->lv_source->listview_width(*this); 476 }; 477 set_overlay_needs_update()478 void set_overlay_needs_update() { this->lv_overlay_needs_update = true; }; 479 480 /** 481 * Get the actual dimensions of the view. 482 * 483 * @param height_out The actual height of the view in lines. 484 * @param width_out The actual width of the view in columns. 485 */ get_dimensions(vis_line_t & height_out,unsigned long & width_out) const486 void get_dimensions(vis_line_t &height_out, unsigned long &width_out) const 487 { 488 unsigned long height; 489 490 if (this->lv_window == nullptr) { 491 height_out = std::max(this->lv_height, 1_vl); 492 if (this->lv_source) { 493 width_out = this->lv_source->listview_width(*this); 494 } else { 495 width_out = 80; 496 } 497 } 498 else { 499 getmaxyx(this->lv_window, height, width_out); 500 if (this->lv_height < 0) { 501 height_out = vis_line_t(height) + 502 this->lv_height - 503 vis_line_t(this->lv_y); 504 } 505 else { 506 height_out = this->lv_height; 507 } 508 } 509 if (this->lv_x < width_out) { 510 width_out -= this->lv_x; 511 } else { 512 width_out = 0; 513 } 514 }; 515 get_dimensions() const516 std::pair<vis_line_t, unsigned long> get_dimensions() const { 517 unsigned long width; 518 vis_line_t height; 519 520 this->get_dimensions(height, width); 521 return std::make_pair(height, width); 522 } 523 524 /** This method should be called when the data source has changed. */ 525 virtual void reload_data(); 526 527 /** 528 * @param ch The input to be handled. 529 * @return True if the key was eaten by this view. 530 */ 531 bool handle_key(int ch); 532 533 /** 534 * Query the data source and draw the visible lines on the display. 535 */ 536 void do_update(); 537 538 bool handle_mouse(mouse_event &me); 539 set_tail_space(vis_line_t space)540 listview_curses &set_tail_space(vis_line_t space) { 541 this->lv_tail_space = space; 542 543 return *this; 544 }; 545 log_state()546 void log_state() { 547 log_debug("listview_curses=%p", this); 548 log_debug(" lv_title=%s", this->lv_title.c_str()); 549 log_debug(" lv_y=%u", this->lv_y); 550 log_debug(" lv_top=%d", (int) this->lv_top); 551 }; 552 invoke_scroll()553 virtual void invoke_scroll() { 554 this->lv_scroll(this); 555 } 556 557 protected: delegate_scroll_out()558 void delegate_scroll_out() { 559 for (auto &lv_input_delegate : this->lv_input_delegates) { 560 lv_input_delegate->list_input_handle_scroll_out(*this); 561 } 562 } 563 564 enum lv_mode_t { 565 LV_MODE_NONE, 566 LV_MODE_DOWN, 567 LV_MODE_UP, 568 LV_MODE_DRAG 569 }; 570 571 static list_gutter_source DEFAULT_GUTTER_SOURCE; 572 573 std::string lv_title; 574 list_data_source *lv_source{nullptr}; /*< The data source delegate. */ 575 std::list<list_input_delegate *> lv_input_delegates; 576 list_overlay_source *lv_overlay_source{nullptr}; 577 action lv_scroll; /*< The scroll action. */ 578 WINDOW * lv_window{nullptr}; /*< The window that contains this view. */ 579 unsigned int lv_x{0}; 580 unsigned int lv_y{0}; /*< The y offset of this view. */ 581 vis_line_t lv_top{0}; /*< The line at the top of the view. */ 582 unsigned int lv_left{0}; /*< The column at the left of the view. */ 583 vis_line_t lv_height{0}; /*< The abs/rel height of the view. */ 584 int lv_history_position{0}; 585 bool lv_overlay_needs_update{true}; 586 bool lv_show_scrollbar{true}; /*< Draw the scrollbar in the view. */ 587 bool lv_show_bottom_border{false}; 588 list_gutter_source *lv_gutter_source{&DEFAULT_GUTTER_SOURCE}; 589 bool lv_word_wrap{false}; 590 bool lv_selectable{false}; 591 vis_line_t lv_selection{0}; 592 593 struct timeval lv_mouse_time; 594 int lv_scroll_accel{1}; 595 int lv_scroll_velo; 596 int lv_mouse_y{-1}; 597 lv_mode_t lv_mouse_mode{LV_MODE_NONE}; 598 vis_line_t lv_tail_space{1}; 599 }; 600 #endif 601