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).
This commit is contained in:
Clement Tsang 2021-02-18 17:10:51 -05:00 committed by GitHub
parent ce9818d935
commit 4db39da75e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 135 additions and 26 deletions

View File

@ -97,6 +97,7 @@
"nuget", "nuget",
"nvme", "nvme",
"paren", "paren",
"pcpu",
"pids", "pids",
"pmem", "pmem",
"powerpc", "powerpc",

View File

@ -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. - [#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 ## Changes
- [#372](https://github.com/ClementTsang/bottom/pull/372): Hides the SWAP graph and legend in normal mode if SWAP is 0. - [#372](https://github.com/ClementTsang/bottom/pull/372): Hides the SWAP graph and legend in normal mode if SWAP is 0.

View File

@ -445,9 +445,10 @@ Note that the `and` operator takes precedence over the `or` operator.
#### Process bindings #### 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 ## Features

View File

@ -399,7 +399,7 @@ impl App {
// If it just opened, move left // If it just opened, move left
proc_widget_state proc_widget_state
.columns .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); self.move_widget_selection(&WidgetDirection::Left);
} else { } else {
// Otherwise, move right if currently on the sort widget // 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); 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.delete_dialog_state.is_showing_dd = false;
} }
self.is_force_redraw = true; self.is_force_redraw = true;
} else if let BottomWidgetType::ProcSort = self.current_widget.widget_type { } else if !self.is_in_dialog() {
if let Some(proc_widget_state) = self if let BottomWidgetType::ProcSort = self.current_widget.widget_type {
.proc_state if let Some(proc_widget_state) = self
.widget_states .proc_state
.get_mut(&(self.current_widget.widget_id - 2)) .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.proc_state.force_update = Some(self.current_widget.widget_id - 2);
self.toggle_sort(); proc_widget_state.update_sorting_with_columns();
self.toggle_sort();
}
} }
} }
} }
@ -1470,6 +1473,8 @@ impl App {
} }
'c' => { 'c' => {
if let BottomWidgetType::Proc = self.current_widget.widget_type { 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 if let Some(proc_widget_state) = self
.proc_state .proc_state
.get_mut_widget_state(self.current_widget.widget_id) .get_mut_widget_state(self.current_widget.widget_id)
@ -2932,13 +2937,13 @@ impl App {
// Get our index... // Get our index...
let clicked_entry = y - *tlc_y; let clicked_entry = y - *tlc_y;
// + 1 so we start at 0. // + 1 so we start at 0.
let offset = 1 let border_offset = if self.is_drawing_border() { 1 } else { 0 };
+ if self.is_drawing_border() { 1 } else { 0 } let header_gap_offset = 1 + if self.is_drawing_gap(&self.current_widget) {
+ if self.is_drawing_gap(&self.current_widget) { self.app_config_fields.table_gap
self.app_config_fields.table_gap } else {
} else { 0
0 };
}; let offset = border_offset + header_gap_offset;
if clicked_entry >= offset { if clicked_entry >= offset {
let offset_clicked_entry = clicked_entry - offset; let offset_clicked_entry = clicked_entry - offset;
match &self.current_widget.widget_type { 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 => { BottomWidgetType::Battery => {

View File

@ -178,6 +178,10 @@ pub struct ColumnInfo {
pub struct ProcColumn { pub struct ProcColumn {
pub ordered_columns: Vec<ProcessSorting>, pub ordered_columns: Vec<ProcessSorting>,
/// The y location of headers. Since they're all aligned, it's just one value.
pub column_header_y_loc: Option<u16>,
/// The x start and end bounds for each header.
pub column_header_x_locs: Option<Vec<(u16, u16)>>,
pub column_mapping: HashMap<ProcessSorting, ColumnInfo>, pub column_mapping: HashMap<ProcessSorting, ColumnInfo>,
pub longest_header_len: u16, pub longest_header_len: u16,
pub column_state: TableState, pub column_state: TableState,
@ -294,6 +298,8 @@ impl Default for ProcColumn {
current_scroll_position: 0, current_scroll_position: 0,
previous_scroll_position: 0, previous_scroll_position: 0,
backup_prev_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() .sum()
} }
/// ALWAYS call this when opening the sorted window. /// NOTE: ALWAYS call this when opening the sorted window.
pub fn set_to_sorted_index(&mut self, proc_sorting_type: &ProcessSorting) { 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! // 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; let mut true_index = 0;
for column in &self.ordered_columns { for column in &self.ordered_columns {
@ -352,6 +358,12 @@ impl ProcColumn {
self.backup_prev_scroll_position = self.previous_scroll_position; 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( pub fn get_column_headers(
&self, proc_sorting_type: &ProcessSorting, sort_reverse: bool, &self, proc_sorting_type: &ProcessSorting, sort_reverse: bool,
) -> Vec<String> { ) -> Vec<String> {
@ -432,7 +444,7 @@ impl ProcWidgetState {
// TODO: If we add customizable columns, this should pull from config // TODO: If we add customizable columns, this should pull from config
let mut columns = ProcColumn::default(); 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 { if is_grouped {
// Normally defaults to showing by PID, toggle count on instead. // Normally defaults to showing by PID, toggle count on instead.
columns.toggle(&ProcessSorting::Count); columns.toggle(&ProcessSorting::Count);

View File

@ -327,13 +327,19 @@ impl Painter {
widget.bottom_right_corner = None; widget.bottom_right_corner = None;
} }
// And reset dd_dialog... // Reset dd_dialog...
app_state.delete_dialog_state.button_positions = vec![]; 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() { for battery_widget in app_state.battery_state.widget_states.values_mut() {
battery_widget.tab_click_locs = None; 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 { if app_state.help_dialog_state.is_showing_help {

View File

@ -507,6 +507,42 @@ impl ProcessTableWidget for Painter {
f.render_widget(process_block, margined_draw_loc); 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() { if app_state.should_get_widget_bounds() {
// Update draw loc in widget map // Update draw loc in widget map
if let Some(widget) = app_state.widget_map.get_mut(&widget_id) { if let Some(widget) = app_state.widget_map.get_mut(&widget_id) {

View File

@ -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", "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", "3 - Process widget",
"dd Kill the selected process", "dd Kill the selected process",
"c Sort by CPU usage, press again to reverse sorting order", "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", "% Toggle between values and percentages for memory usage",
"t, F5 Toggle tree mode", "t, F5 Toggle tree mode",
"+, -, click Collapse/expand a branch while in 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] = [ pub const SEARCH_HELP_TEXT: [&str; 48] = [