Skip to content

feature: Add mouse support to sorting columns #413

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Feb 18, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@
"nuget",
"nvme",
"paren",
"pcpu",
"pids",
"pmem",
"powerpc",
Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
84 changes: 67 additions & 17 deletions src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -469,6 +469,7 @@ impl App {
}
}

proc_widget_state.requires_redraw = true;
self.proc_state.force_update = Some(self.current_widget.widget_id);
}
}
Expand Down Expand Up @@ -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();
}
}
}
}
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 => {
Expand Down
18 changes: 15 additions & 3 deletions src/app/states.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
}
}
}
Expand Down Expand Up @@ -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 {
Expand All @@ -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> {
Expand Down Expand Up @@ -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);
Expand Down
10 changes: 8 additions & 2 deletions src/canvas.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
36 changes: 36 additions & 0 deletions src/canvas/widgets/process_table.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
3 changes: 2 additions & 1 deletion src/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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] = [
Expand Down