From 4db39da75e00e45cc50c8143ff5eba3d92525dbd Mon Sep 17 00:00:00 2001 From: Clement Tsang <34804052+ClementTsang@users.noreply.github.com> Date: Thu, 18 Feb 2021 17:10:51 -0500 Subject: [PATCH] feature: Add mouse support to sorting columns (#413) Adds mouse support for sorting columns within the process widget. You can now click on the column header to sort (or invert the sort). --- .vscode/settings.json | 1 + CHANGELOG.md | 2 + README.md | 7 +-- src/app.rs | 84 +++++++++++++++++++++++------ src/app/states.rs | 18 +++++-- src/canvas.rs | 10 +++- src/canvas/widgets/process_table.rs | 36 +++++++++++++ src/constants.rs | 3 +- 8 files changed, 135 insertions(+), 26 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index e8c56d7e..2c88c1a2 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -97,6 +97,7 @@ "nuget", "nvme", "paren", + "pcpu", "pids", "pmem", "powerpc", diff --git a/CHANGELOG.md b/CHANGELOG.md index fae70500..eb4bf6c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [#409](https://github.com/ClementTsang/bottom/pull/409): Adds `Ctrl-w` and `Ctrl-h` shortcuts in search, to delete a word and delete a character respectively. +- [#413](https://github.com/ClementTsang/bottom/pull/413): Adds mouse support for sorting process columns. + ## Changes - [#372](https://github.com/ClementTsang/bottom/pull/372): Hides the SWAP graph and legend in normal mode if SWAP is 0. diff --git a/README.md b/README.md index 4388bc97..a899ef27 100644 --- a/README.md +++ b/README.md @@ -445,9 +445,10 @@ Note that the `and` operator takes precedence over the `or` operator. #### Process bindings -| | | -| ----- | --------------------------------------------------------------------------------------------------- | -| Click | If in tree mode and you click on a selected entry, it toggles whether the branch is expanded or not | +| | | +| ---------------------- | --------------------------------------------------------------------------------------------------- | +| Click on process entry | If in tree mode and you click on a selected entry, it toggles whether the branch is expanded or not | +| Click on table header | Sorts the widget by that column, or inverts the sort if already selected | ## Features diff --git a/src/app.rs b/src/app.rs index 7b9d0758..e980e1a0 100644 --- a/src/app.rs +++ b/src/app.rs @@ -399,7 +399,7 @@ impl App { // If it just opened, move left proc_widget_state .columns - .set_to_sorted_index(&proc_widget_state.process_sorting_type); + .set_to_sorted_index_from_type(&proc_widget_state.process_sorting_type); self.move_widget_selection(&WidgetDirection::Left); } else { // Otherwise, move right if currently on the sort widget @@ -469,6 +469,7 @@ impl App { } } + proc_widget_state.requires_redraw = true; self.proc_state.force_update = Some(self.current_widget.widget_id); } } @@ -692,15 +693,17 @@ impl App { self.delete_dialog_state.is_showing_dd = false; } self.is_force_redraw = true; - } else if let BottomWidgetType::ProcSort = self.current_widget.widget_type { - if let Some(proc_widget_state) = self - .proc_state - .widget_states - .get_mut(&(self.current_widget.widget_id - 2)) - { - self.proc_state.force_update = Some(self.current_widget.widget_id - 2); - proc_widget_state.update_sorting_with_columns(); - self.toggle_sort(); + } else if !self.is_in_dialog() { + if let BottomWidgetType::ProcSort = self.current_widget.widget_type { + if let Some(proc_widget_state) = self + .proc_state + .widget_states + .get_mut(&(self.current_widget.widget_id - 2)) + { + self.proc_state.force_update = Some(self.current_widget.widget_id - 2); + proc_widget_state.update_sorting_with_columns(); + self.toggle_sort(); + } } } } @@ -1470,6 +1473,8 @@ impl App { } 'c' => { if let BottomWidgetType::Proc = self.current_widget.widget_type { + // FIXME: There's a mismatch bug with this and all sorting types when using the keybind vs the sorting menu. + // If the sorting menu is open, it won't update when using this! if let Some(proc_widget_state) = self .proc_state .get_mut_widget_state(self.current_widget.widget_id) @@ -2932,13 +2937,13 @@ impl App { // Get our index... let clicked_entry = y - *tlc_y; // + 1 so we start at 0. - let offset = 1 - + if self.is_drawing_border() { 1 } else { 0 } - + if self.is_drawing_gap(&self.current_widget) { - self.app_config_fields.table_gap - } else { - 0 - }; + let border_offset = if self.is_drawing_border() { 1 } else { 0 }; + let header_gap_offset = 1 + if self.is_drawing_gap(&self.current_widget) { + self.app_config_fields.table_gap + } else { + 0 + }; + let offset = border_offset + header_gap_offset; if clicked_entry >= offset { let offset_clicked_entry = clicked_entry - offset; match &self.current_widget.widget_type { @@ -3030,6 +3035,51 @@ impl App { } _ => {} } + } else { + // We might have clicked on a header! Check if we only exceeded the table + border offset, and + // it's implied we exceeded the gap offset. + if clicked_entry == border_offset { + #[allow(clippy::single_match)] + match &self.current_widget.widget_type { + BottomWidgetType::Proc => { + if let Some(proc_widget_state) = self + .proc_state + .get_mut_widget_state(self.current_widget.widget_id) + { + // Let's now check if it's a column header. + if let (Some(y_loc), Some(x_locs)) = ( + &proc_widget_state.columns.column_header_y_loc, + &proc_widget_state.columns.column_header_x_locs, + ) { + // debug!("x, y: {}, {}", x, y); + // debug!("y_loc: {}", y_loc); + // debug!("x_locs: {:?}", x_locs); + + if y == *y_loc { + for (itx, (x_left, x_right)) in + x_locs.iter().enumerate() + { + if x >= *x_left && x <= *x_right { + // Found our column! + proc_widget_state + .columns + .set_to_sorted_index_from_visual_index( + itx, + ); + proc_widget_state + .update_sorting_with_columns(); + self.proc_state.force_update = + Some(self.current_widget.widget_id); + break; + } + } + } + } + } + } + _ => {} + } + } } } BottomWidgetType::Battery => { diff --git a/src/app/states.rs b/src/app/states.rs index d30adcc4..37fbd8d1 100644 --- a/src/app/states.rs +++ b/src/app/states.rs @@ -178,6 +178,10 @@ pub struct ColumnInfo { pub struct ProcColumn { pub ordered_columns: Vec, + /// The y location of headers. Since they're all aligned, it's just one value. + pub column_header_y_loc: Option, + /// The x start and end bounds for each header. + pub column_header_x_locs: Option>, pub column_mapping: HashMap, pub longest_header_len: u16, pub column_state: TableState, @@ -294,6 +298,8 @@ impl Default for ProcColumn { current_scroll_position: 0, previous_scroll_position: 0, backup_prev_scroll_position: 0, + column_header_y_loc: None, + column_header_x_locs: None, } } } @@ -335,8 +341,8 @@ impl ProcColumn { .sum() } - /// ALWAYS call this when opening the sorted window. - pub fn set_to_sorted_index(&mut self, proc_sorting_type: &ProcessSorting) { + /// NOTE: ALWAYS call this when opening the sorted window. + pub fn set_to_sorted_index_from_type(&mut self, proc_sorting_type: &ProcessSorting) { // TODO [Custom Columns]: If we add custom columns, this may be needed! Since column indices will change, this runs the risk of OOB. So, when you change columns, CALL THIS AND ADAPT! let mut true_index = 0; for column in &self.ordered_columns { @@ -352,6 +358,12 @@ impl ProcColumn { self.backup_prev_scroll_position = self.previous_scroll_position; } + /// This function sets the scroll position based on the index. + pub fn set_to_sorted_index_from_visual_index(&mut self, visual_index: usize) { + self.current_scroll_position = visual_index; + self.backup_prev_scroll_position = self.previous_scroll_position; + } + pub fn get_column_headers( &self, proc_sorting_type: &ProcessSorting, sort_reverse: bool, ) -> Vec { @@ -432,7 +444,7 @@ impl ProcWidgetState { // TODO: If we add customizable columns, this should pull from config let mut columns = ProcColumn::default(); - columns.set_to_sorted_index(&process_sorting_type); + columns.set_to_sorted_index_from_type(&process_sorting_type); if is_grouped { // Normally defaults to showing by PID, toggle count on instead. columns.toggle(&ProcessSorting::Count); diff --git a/src/canvas.rs b/src/canvas.rs index 2e669d46..cf7d6af4 100644 --- a/src/canvas.rs +++ b/src/canvas.rs @@ -327,13 +327,19 @@ impl Painter { widget.bottom_right_corner = None; } - // And reset dd_dialog... + // Reset dd_dialog... app_state.delete_dialog_state.button_positions = vec![]; - // And battery dialog... + // Reset battery dialog... for battery_widget in app_state.battery_state.widget_states.values_mut() { battery_widget.tab_click_locs = None; } + + // Reset column headers for sorting in process widget... + for proc_widget in app_state.proc_state.widget_states.values_mut() { + proc_widget.columns.column_header_y_loc = None; + proc_widget.columns.column_header_x_locs = None; + } } if app_state.help_dialog_state.is_showing_help { diff --git a/src/canvas/widgets/process_table.rs b/src/canvas/widgets/process_table.rs index bacba555..fa5f3393 100644 --- a/src/canvas/widgets/process_table.rs +++ b/src/canvas/widgets/process_table.rs @@ -507,6 +507,42 @@ impl ProcessTableWidget for Painter { f.render_widget(process_block, margined_draw_loc); } + // Check if we need to update columnar bounds... + if recalculate_column_widths + || proc_widget_state.columns.column_header_x_locs.is_none() + || proc_widget_state.columns.column_header_y_loc.is_none() + { + // y location is just the y location of the widget + border size (1 normally, 0 in basic) + proc_widget_state.columns.column_header_y_loc = + Some(draw_loc.y + if draw_border { 1 } else { 0 }); + + // x location is determined using the x locations of the widget; just offset from the left bound + // as appropriate, and use the right bound as limiter. + + let mut current_x_left = draw_loc.x + 1; + let max_x_right = draw_loc.x + draw_loc.width - 1; + + let mut x_locs = vec![]; + + for width in proc_widget_state + .table_width_state + .calculated_column_widths + .iter() + { + let right_bound = current_x_left + width; + + if right_bound < max_x_right { + x_locs.push((current_x_left, right_bound)); + current_x_left = right_bound + 1; + } else { + x_locs.push((current_x_left, max_x_right)); + break; + } + } + + proc_widget_state.columns.column_header_x_locs = Some(x_locs); + } + if app_state.should_get_widget_bounds() { // Update draw loc in widget map if let Some(widget) = app_state.widget_map.get_mut(&widget_id) { diff --git a/src/constants.rs b/src/constants.rs index 70131acb..e660125e 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -254,7 +254,7 @@ pub const CPU_HELP_TEXT: [&str; 2] = [ "Mouse scroll Scrolling over an CPU core/average shows only that entry on the chart", ]; -pub const PROCESS_HELP_TEXT: [&str; 14] = [ +pub const PROCESS_HELP_TEXT: [&str; 15] = [ "3 - Process widget", "dd Kill the selected process", "c Sort by CPU usage, press again to reverse sorting order", @@ -269,6 +269,7 @@ pub const PROCESS_HELP_TEXT: [&str; 14] = [ "% Toggle between values and percentages for memory usage", "t, F5 Toggle tree mode", "+, -, click Collapse/expand a branch while in tree mode", + "click on header Sorts the entries by that column, click again to invert the sort", ]; pub const SEARCH_HELP_TEXT: [&str; 48] = [