diff --git a/src/MainWindow.vala b/src/MainWindow.vala index f87d7cf5..646b0526 100644 --- a/src/MainWindow.vala +++ b/src/MainWindow.vala @@ -101,21 +101,10 @@ public class Monitor.MainWindow : Gtk.ApplicationWindow { MonitorApp.settings.bind ("opened-view", stack, "visible-child-name", DEFAULT); search_entry.search_changed.connect (() => { - // collapse tree only when search is focused and changed - if (search_entry.is_focus ()) { - process_view.process_tree_view.collapse_all (); - } - - process_view.needle = search_entry.text; - - // focus on child row to avoid the app crashes by clicking "Kill/End Process" buttons in headerbar - process_view.process_tree_view.focus_on_child_row (); + process_view.treeview_model.filtered.needle = search_entry.text; search_entry.grab_focus (); }); - search_entry.activate.connect (() => { - process_view.process_tree_view.focus_on_first_row (); - }); var search_action = new GLib.SimpleAction ("search", null); search_action.activate.connect (() => { diff --git a/src/Models/TreeViewModel.vala b/src/Models/TreeViewModel.vala index aecb1c76..aa1aa619 100644 --- a/src/Models/TreeViewModel.vala +++ b/src/Models/TreeViewModel.vala @@ -12,21 +12,102 @@ public enum Monitor.Column { CMD } -public class Monitor.TreeViewModel : Gtk.TreeStore { +public class Monitor.ProcessRowData : GLib.Object { + public Icon icon { get; set; } + public string name { get; set; } + public int cpu { get; set; } + public uint64 memory { get; set; } + public int pid { get; set; } + public string cmd { get; set; } +} + +public class Monitor.TreeViewFilter : GLib.Object { + private string _needle; + public string needle { + get { + return _needle; + } + set { + name_filter.search = value; + cmd_filter.search = value; + _needle = value; + } + } + public Gtk.FilterListModel model_out; + private Gtk.AnyFilter any_filter; + public Gtk.StringFilter name_filter; + public Gtk.StringFilter cmd_filter; + public Gtk.CustomFilter pid_filter; + + public TreeViewFilter (GLib.ListModel? model) { + name_filter = build_str_filter ("name"); + cmd_filter = build_str_filter ("cmd"); + + // since the pid property is an int, we need to use a custom filter to convert it to a string + pid_filter = new Gtk.CustomFilter ((obj) => { + var item = (ProcessRowData) obj; + bool pid_found = item.pid.to_string ().contains (needle.casefold ()) || false; + return pid_found; + }); + + any_filter = new Gtk.AnyFilter (); + any_filter.append (name_filter); + any_filter.append (cmd_filter); + any_filter.append (pid_filter); + + model_out = new Gtk.FilterListModel (model, any_filter); + + } + + private Gtk.StringFilter build_str_filter (string column_name) { + var expression = new Gtk.PropertyExpression (typeof (ProcessRowData), null, column_name); + return new Gtk.StringFilter (expression) { + ignore_case = true, + match_mode = SUBSTRING, + search = needle + }; + } + +} + +public class Monitor.TreeViewModel : GLib.Object { + public ProcessManager process_manager; - private Gee.Map process_rows; + + public TreeViewFilter filtered; + public Gtk.SingleSelection selection_model; + public signal void added_first_row (); + public signal void process_selected (Process process); + + public Gtk.Sorter sorter { + get { return sorted.sorter; } + set { + sorted.sorter = value; + } + } + + private GLib.ListStore store; + private Gtk.SortListModel sorted; + + private Gee.Map process_rows; + construct { - process_rows = new Gee.HashMap (); - - set_column_types (new Type[] { - typeof (string), - typeof (string), - typeof (double), - typeof (int64), - typeof (int), - typeof (string), + process_rows = new Gee.HashMap (); + store = new GLib.ListStore (typeof (ProcessRowData)); + sorted = new Gtk.SortListModel (store, null); + + filtered = new TreeViewFilter (sorted); + + selection_model = new Gtk.SingleSelection (filtered.model_out) { + autoselect = true + }; + + selection_model.notify["selected-item"].connect ((sender, property) => { + var row_data = selection_model.get_selected_item () as ProcessRowData; + Process process = process_manager.get_process (row_data.pid); + process_selected (process); }); process_manager = ProcessManager.get_default (); @@ -37,6 +118,16 @@ public class Monitor.TreeViewModel : Gtk.TreeStore { Idle.add (() => { add_running_processes (); return false; }); } + public Gtk.StringSorter str_sorter (string column_name) { + return new Gtk.StringSorter (new Gtk.PropertyExpression (typeof (ProcessRowData), null, column_name)); + } + + public Gtk.NumericSorter num_sorter (string column_name) { + return new Gtk.NumericSorter (new Gtk.PropertyExpression (typeof (ProcessRowData), null, column_name)) { + sort_order = Gtk.SortType.DESCENDING + }; + } + private void add_running_processes () { debug ("add_running_processes"); var running_processes = process_manager.get_process_list (); @@ -49,24 +140,22 @@ public class Monitor.TreeViewModel : Gtk.TreeStore { if (process != null && !process_rows.has_key (process.stat.pid)) { debug ("Add process %d Parent PID: %d", process.stat.pid, process.stat.ppid); // add the process to the model - Gtk.TreeIter iter; - append (out iter, null); // null means top-level - - // donno what is going on, but maybe just use a string instead of Icon ?? - // coz it lagz - // string icon_name = process.icon.to_string (); - - set (iter, - Column.NAME, process.application_name, - Column.ICON, process.icon.to_string (), - Column.PID, process.stat.pid, - Column.CMD, process.command, - -1); + var row = new ProcessRowData () { + icon = process.icon, + name = process.application_name, + cpu = (int) process.cpu_percentage, + memory = process.mem_usage, + pid = process.stat.pid, + cmd = process.command + }; + + store.append (row); + if (process_rows.size < 1) { added_first_row (); } // add the process to our cache of process_rows - process_rows.set (process.stat.pid, iter); + process_rows.set (process.stat.pid, row); return true; } return false; @@ -75,11 +164,17 @@ public class Monitor.TreeViewModel : Gtk.TreeStore { private void update_model () { foreach (int pid in process_rows.keys) { Process process = process_manager.get_process (pid); - Gtk.TreeIter iter = process_rows[pid]; - set (iter, - Column.CPU, process.cpu_percentage, - Column.MEMORY, process.mem_usage, - -1); + var process_row = process_rows.get (pid); + + uint pos; + if (store.find (process_row, out pos)) { + var item = store.get_item (pos) as ProcessRowData; + item.cpu = (int) process.cpu_percentage; + item.memory = process.mem_usage; + store.items_changed (pos, 1, 1); + } else { + debug ("Failed to find process row for pid %d", pid); + } } } @@ -87,8 +182,11 @@ public class Monitor.TreeViewModel : Gtk.TreeStore { debug ("remove process %d from model".printf (pid)); // if process rows has pid if (process_rows.has_key (pid)) { - var cached_iter = process_rows.get (pid); - remove (ref cached_iter); + uint pos; + var process_row = process_rows.get (pid); + if (store.find (process_row, out pos)) { + store.remove (pos); + } process_rows.unset (pid); } } diff --git a/src/Monitor.vala b/src/Monitor.vala index 5a71ae5f..a0c84d1c 100644 --- a/src/Monitor.vala +++ b/src/Monitor.vala @@ -128,7 +128,6 @@ namespace Monitor { settings.bind ("is-maximized", window, "maximized", SettingsBindFlags.SET); - window.process_view.process_tree_view.focus_on_first_row (); } public static int main (string[] args) { diff --git a/src/Views/ProcessView/ProcessTreeView/CPUProcessTreeView.vala b/src/Views/ProcessView/ProcessTreeView/CPUProcessTreeView.vala deleted file mode 100644 index 51d3c2ef..00000000 --- a/src/Views/ProcessView/ProcessTreeView/CPUProcessTreeView.vala +++ /dev/null @@ -1,217 +0,0 @@ -/* - * Copyright 2025 elementary, Inc. (https://elementary.io) - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -public class Monitor.CPUProcessTreeView : Gtk.TreeView { - private new TreeViewModel model; - private Gtk.TreeViewColumn name_column; - private Gtk.TreeViewColumn pid_column; - private Gtk.TreeViewColumn cpu_column; - private Gtk.TreeViewColumn memory_column; - - public signal void process_selected (Process process); - - public CPUProcessTreeView (TreeViewModel model) { - this.model = model; - - enable_search = false; - - // setup name column - name_column = new Gtk.TreeViewColumn (); - name_column.title = _("Process Name"); - name_column.expand = true; - name_column.min_width = 250; - name_column.set_sort_column_id (Column.NAME); - - var icon_cell = new Gtk.CellRendererPixbuf (); - name_column.pack_start (icon_cell, false); - // name_column.add_attribute (icon_cell, "icon_name", Column.ICON); - name_column.set_cell_data_func (icon_cell, icon_cell_layout); - - var name_cell = new Gtk.CellRendererText (); - name_cell.ellipsize = Pango.EllipsizeMode.END; - name_cell.set_fixed_height_from_font (1); - name_column.pack_start (name_cell, false); - name_column.add_attribute (name_cell, "text", Column.NAME); - insert_column (name_column, -1); - - // setup cpu column - var cpu_cell = new Gtk.CellRendererText (); - cpu_cell.xalign = 0.5f; - - cpu_column = new Gtk.TreeViewColumn.with_attributes (_("CPU"), cpu_cell); - cpu_column.expand = false; - cpu_column.set_cell_data_func (cpu_cell, cpu_usage_cell_layout); - cpu_column.alignment = 0.5f; - cpu_column.set_sort_column_id (Column.CPU); - insert_column (cpu_column, -1); - - // setup memory column - var memory_cell = new Gtk.CellRendererText (); - memory_cell.xalign = 0.5f; - - memory_column = new Gtk.TreeViewColumn.with_attributes (_("Memory"), memory_cell); - memory_column.expand = false; - memory_column.set_cell_data_func (memory_cell, memory_usage_cell_layout); - memory_column.alignment = 0.5f; - memory_column.set_sort_column_id (Column.MEMORY); - insert_column (memory_column, -1); - - // setup PID column - var pid_cell = new Gtk.CellRendererText (); - pid_cell.xalign = 0.5f; - pid_column = new Gtk.TreeViewColumn.with_attributes (_("PID"), pid_cell); - pid_column.set_cell_data_func (pid_cell, pid_cell_layout); - pid_column.expand = false; - pid_column.alignment = 0.5f; - pid_column.set_sort_column_id (Column.PID); - pid_column.add_attribute (pid_cell, "text", Column.PID); - insert_column (pid_column, -1); - - // resize all of the columns - columns_autosize (); - - set_model (model); - - model.added_first_row.connect (() => { - focus_on_first_row (); - }); - - cursor_changed.connect (_cursor_changed); - // model.process_manager.updated.connect (_cursor_changed); - - var key_controller = new Gtk.EventControllerKey (); - add_controller (key_controller); - key_controller.key_released.connect ((keyval, keycode, state) => { - switch (keyval) { - case Gdk.Key.Left: - collapse (); - break; - case Gdk.Key.Right: - expanded (); - break; - } - }); - } - public void icon_cell_layout (Gtk.CellLayout cell_layout, Gtk.CellRenderer icon_cell, Gtk.TreeModel model, Gtk.TreeIter iter) { - Value icon_name; - model.get_value (iter, Column.ICON, out icon_name); - string path = ((string) icon_name); - - ((Gtk.CellRendererPixbuf)icon_cell).icon_name = path; - } - - public void cpu_usage_cell_layout (Gtk.CellLayout cell_layout, Gtk.CellRenderer cell, Gtk.TreeModel model, Gtk.TreeIter iter) { - // grab the value that was store in the model and convert it down to a usable format - Value cpu_usage_value; - model.get_value (iter, Column.CPU, out cpu_usage_value); - double cpu_usage = cpu_usage_value.get_double (); - - // format the double into a string - if (cpu_usage < 0.0) - ((Gtk.CellRendererText)cell).text = Utils.NO_DATA; - else - ((Gtk.CellRendererText)cell).text = "%.0f%%".printf (cpu_usage); - } - - public void memory_usage_cell_layout (Gtk.CellLayout cell_layout, Gtk.CellRenderer cell, Gtk.TreeModel model, Gtk.TreeIter iter) { - // grab the value that was store in the model and convert it down to a usable format - Value memory_usage_value; - model.get_value (iter, Column.MEMORY, out memory_usage_value); - int64 memory_usage = memory_usage_value.get_int64 (); - double memory_usage_double = (double) memory_usage; - string units = _("KiB"); - - // convert to MiB if needed - if (memory_usage_double > 1024.0) { - memory_usage_double /= 1024.0; - units = _("MiB"); - } - - // convert to GiB if needed - if (memory_usage_double > 1024.0) { - memory_usage_double /= 1024.0; - units = _("GiB"); - } - - // format the double into a string - if (memory_usage == 0) - ((Gtk.CellRendererText)cell).text = Utils.NO_DATA; - else - ((Gtk.CellRendererText)cell).text = "%.1f %s".printf (memory_usage_double, units); - } - - private void pid_cell_layout (Gtk.CellLayout cell_layout, Gtk.CellRenderer cell, Gtk.TreeModel model, Gtk.TreeIter iter) { - Value pid_value; - model.get_value (iter, Column.PID, out pid_value); - int pid = pid_value.get_int (); - // format the double into a string - if (pid == 0) { - ((Gtk.CellRendererText)cell).text = Utils.NO_DATA; - } - } - - public void focus_on_first_row () { - Gtk.TreePath tree_path = new Gtk.TreePath.from_indices (0); - this.set_cursor (tree_path, null, false); - grab_focus (); - } - - public void focus_on_child_row () { - Gtk.TreePath tree_path = new Gtk.TreePath.from_indices (0, 0); - this.set_cursor (tree_path, null, false); - grab_focus (); - } - - public int get_pid_of_selected () { - Gtk.TreeIter iter; - Gtk.TreeModel model; - int pid = 0; - var selection = this.get_selection ().get_selected_rows (out model).nth_data (0); - model.get_iter (out iter, selection); - model.get (iter, Column.PID, out pid); - return pid; - } - - // How about GtkTreeSelection ? - - public void expanded () { - Gtk.TreeModel model; - var selection = this.get_selection ().get_selected_rows (out model).nth_data (0); - this.expand_row (selection, false); - } - - public void collapse () { - Gtk.TreeModel model; - var selection = this.get_selection ().get_selected_rows (out model).nth_data (0); - this.collapse_row (selection); - } - - public void kill_process () { - int pid = get_pid_of_selected (); - model.kill_process (pid); - } - - public void end_process () { - int pid = get_pid_of_selected (); - model.end_process (pid); - } - - // when row is selected send signal to update process_info_view - public void _cursor_changed () { - Gtk.TreeModel tree_model; - Gtk.TreeIter iter; - int pid = 0; - var selection = get_selection ().get_selected_rows (out tree_model).nth_data (0); - - if (selection != null) { - tree_model.get_iter (out iter, selection); - tree_model.get (iter, Column.PID, out pid); - Process process = model.process_manager.get_process (pid); - process_selected (process); - debug ("cursor changed"); - } - } - -} diff --git a/src/Views/ProcessView/ProcessTreeView/ProcessTreeView.vala b/src/Views/ProcessView/ProcessTreeView/ProcessTreeView.vala new file mode 100644 index 00000000..18614a81 --- /dev/null +++ b/src/Views/ProcessView/ProcessTreeView/ProcessTreeView.vala @@ -0,0 +1,118 @@ + +public class Monitor.ProcessTreeView : Granite.Bin { + + public ProcessTreeView (TreeViewModel model) { + + var column_view = new Gtk.ColumnView (model.selection_model) { + name = "monitor-process-column-view", + reorderable = false, + hexpand = true, + vexpand = true + }; + model.sorter = column_view.sorter; + + child = new Gtk.ScrolledWindow () { + child = column_view + }; + + var name_item_factory = new Gtk.SignalListItemFactory (); + name_item_factory.setup.connect (name_item_setup_factory); + name_item_factory.bind.connect (name_item_bind_factory); + + var cpu_item_factory = new Gtk.SignalListItemFactory (); + cpu_item_factory.setup.connect (generic_item_setup_factory); + cpu_item_factory.bind.connect (cpu_item_bind_factory); + + var memory_item_factory = new Gtk.SignalListItemFactory (); + memory_item_factory.setup.connect (generic_item_setup_factory); + memory_item_factory.bind.connect (memory_item_bind_factory); + + var pid_item_factory = new Gtk.SignalListItemFactory (); + pid_item_factory.setup.connect (generic_item_setup_factory); + pid_item_factory.bind.connect (pid_item_bind_factory); + + + var name_column = new Gtk.ColumnViewColumn (_("Process Name"), name_item_factory) { + sorter = model.str_sorter ("name"), + expand = true + }; + column_view.append_column (name_column); + + var cpu_column = new Gtk.ColumnViewColumn (_("CPU"), cpu_item_factory) { + sorter = model.num_sorter ("cpu"), + expand = false + }; + column_view.append_column (cpu_column); + + + var mem_column = new Gtk.ColumnViewColumn (_("Memory"), memory_item_factory) { + sorter = model.num_sorter ("memory"), + expand = false + }; + column_view.append_column (mem_column); + + var pid_column = new Gtk.ColumnViewColumn (_("PID"), pid_item_factory) { + sorter = model.num_sorter ("pid"), + expand = false + }; + column_view.append_column (pid_column); + + } + + private void generic_item_setup_factory (Object object) { + var cell = (Gtk.ColumnViewCell) object; + cell.child = new Gtk.Label (Utils.NO_DATA) { + hexpand = true, + halign = START + }; + } + + private void name_item_setup_factory (Object object) { + var cell = (Gtk.ColumnViewCell) object; + + var box = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 4) { + hexpand = true, + halign = START + }; + var icon = new Gtk.Image.from_icon_name ("application-x-executable") { + pixel_size = 16 + }; + + box.append (icon); + box.append (new Gtk.Label (Utils.NO_DATA)); + cell.child = box; + } + + private void name_item_bind_factory (Object object) { + var cell = (Gtk.ColumnViewCell) object; + var box = (Gtk.Box) cell.child; + var label = (Gtk.Label) box.get_last_child (); + var icon = (Gtk.Image) box.get_first_child (); + + var item = (ProcessRowData) cell.item; + label.label = item.name; + icon.gicon = item.icon; + } + + private void cpu_item_bind_factory (Object object) { + var cell = (Gtk.ColumnViewCell) object; + var label = (Gtk.Label) cell.child; + var item = (ProcessRowData) cell.item; + label.label = "%.0f%%".printf (item.cpu); + } + + private void memory_item_bind_factory (Object object) { + var cell = (Gtk.ColumnViewCell) object; + var label = (Gtk.Label) cell.child; + var item = (ProcessRowData) cell.item; + label.label = format_size (item.memory * 1024, IEC_UNITS); + } + + private void pid_item_bind_factory (Object object) { + var cell = (Gtk.ColumnViewCell) object; + var label = (Gtk.Label) cell.child; + var item = (ProcessRowData) cell.item; + label.label = "%d".printf (item.pid); + } + +} diff --git a/src/Views/ProcessView/ProcessView.vala b/src/Views/ProcessView/ProcessView.vala index 0b7abda9..27999edf 100644 --- a/src/Views/ProcessView/ProcessView.vala +++ b/src/Views/ProcessView/ProcessView.vala @@ -4,12 +4,11 @@ */ public class Monitor.ProcessView : Granite.Bin { - public string needle = ""; - - public CPUProcessTreeView process_tree_view { get; private set; } + public ProcessTreeView process_tree_view { get; private set; } private ProcessInfoView process_info_view; - private TreeViewModel treeview_model; + public TreeViewModel treeview_model { get; private set; } + private SimpleAction end_action; private SimpleAction kill_action; @@ -17,18 +16,8 @@ public class Monitor.ProcessView : Granite.Bin { construct { treeview_model = new TreeViewModel (); - var filter_model = new Gtk.TreeModelFilter (treeview_model, null); - filter_model.set_visible_func (filter_func); - - var sort_model = new Gtk.TreeModelSort.with_model (filter_model); - - process_tree_view = new CPUProcessTreeView (treeview_model); - process_tree_view.process_selected.connect ((process) => on_process_selected (process)); - process_tree_view.set_model (sort_model); - - var process_tree_view_scrolled = new Gtk.ScrolledWindow () { - child = process_tree_view - }; + process_tree_view = new ProcessTreeView (treeview_model); + treeview_model.process_selected.connect ((process) => on_process_selected (process)); process_info_view = new ProcessInfoView () { // This might be useless since first process is selected @@ -39,7 +28,7 @@ public class Monitor.ProcessView : Granite.Bin { }; var paned = new Gtk.Paned (HORIZONTAL) { - start_child = process_tree_view_scrolled, + start_child = process_tree_view, end_child = process_info_view, shrink_end_child = false, resize_end_child = false, @@ -49,7 +38,6 @@ public class Monitor.ProcessView : Granite.Bin { child = paned; - notify["needle"].connect (filter_model.refilter); kill_action = new SimpleAction ("kill", null); kill_action.activate.connect (action_kill); @@ -157,41 +145,4 @@ public class Monitor.ProcessView : Granite.Bin { } - private bool filter_func (Gtk.TreeModel model, Gtk.TreeIter iter) { - string name_haystack; - int pid_haystack; - string cmd_haystack; - bool found = false; - - if (needle.length == 0) { - return true; - } - - model.get (iter, Column.NAME, out name_haystack, -1); - model.get (iter, Column.PID, out pid_haystack, -1); - model.get (iter, Column.CMD, out cmd_haystack, -1); - - // sometimes name_haystack is null - if (name_haystack != null) { - bool name_found = name_haystack.casefold ().contains (needle.casefold ()) || false; - bool pid_found = pid_haystack.to_string ().casefold ().contains (needle.casefold ()) || false; - bool cmd_found = cmd_haystack.casefold ().contains (needle.casefold ()) || false; - found = name_found || pid_found || cmd_found; - } - - Gtk.TreeIter child_iter; - bool child_found = false; - - if (model.iter_children (out child_iter, iter)) { - do { - child_found = filter_func (model, child_iter); - } while (model.iter_next (ref child_iter) && !child_found); - } - - if (child_found && needle.length > 0) { - process_tree_view.expand_all (); - } - - return found || child_found; - } } diff --git a/src/meson.build b/src/meson.build index 41ff3c6c..c2146791 100644 --- a/src/meson.build +++ b/src/meson.build @@ -7,7 +7,7 @@ source_app_files = [ # Views 'Views/ProcessView/ProcessView.vala', 'Views/ProcessView/ProcessInfoView/ProcessInfoView.vala', - 'Views/ProcessView/ProcessTreeView/CPUProcessTreeView.vala', + 'Views/ProcessView/ProcessTreeView/ProcessTreeView.vala', 'Views/PreferencesView.vala',