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",
"nvme",
"paren",
"pcpu",
"pids",
"pmem",
"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.
- [#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.

View File

@ -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

View File

@ -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 => {

View File

@ -178,6 +178,10 @@ pub struct ColumnInfo {
pub struct ProcColumn {
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 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<String> {
@ -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);

View File

@ -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 {

View File

@ -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) {

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",
];
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] = [