diff --git a/data/Application.css b/data/Application.css index f607b3d3ea..7bab5b8cab 100644 --- a/data/Application.css +++ b/data/Application.css @@ -69,3 +69,11 @@ textview.scrubber { .fuzzy-item.preselect-fuzzy label { opacity: 0.7; } + +.symbol-outline > box.horizontal { + margin: 1em; +} + +.symbol-outline .sidebar { + background: inherit; +} diff --git a/data/icons/filter-symbolic.svg b/data/icons/filter-symbolic.svg new file mode 100644 index 0000000000..8be0c63741 --- /dev/null +++ b/data/icons/filter-symbolic.svg @@ -0,0 +1,282 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/io.elementary.code.gresource.xml b/data/io.elementary.code.gresource.xml index ff90ecb696..0167807d9f 100644 --- a/data/io.elementary.code.gresource.xml +++ b/data/io.elementary.code.gresource.xml @@ -29,4 +29,7 @@ icons/panel-right-rtl-symbolic.svg icons/panel-right-symbolic.svg + + icons/filter-symbolic.svg + diff --git a/src/SymbolPane/C/CtagsSymbol.vala b/src/SymbolPane/C/CtagsSymbol.vala index 1b9530f01f..97f0d4228c 100644 --- a/src/SymbolPane/C/CtagsSymbol.vala +++ b/src/SymbolPane/C/CtagsSymbol.vala @@ -18,10 +18,25 @@ public class Scratch.Services.CtagsSymbol : Code.Widgets.SourceList.ExpandableItem { public Scratch.Services.Document doc { get; construct set; } + public SymbolType symbol_type { get; set; default = SymbolType.OTHER; } public int line { get; construct set; } - public CtagsSymbol (Scratch.Services.Document doc, string name, int line, Icon? _icon) { - Object (doc: doc, name: name, line: line); + public CtagsSymbol ( + Scratch.Services.Document doc, + string name, + int line, + Icon? _icon, + SymbolType? s_type = null) { + + Object ( + doc: doc, + name: name, + line: line + ); + icon = _icon; + if (s_type != null) { + symbol_type = s_type; + } } } diff --git a/src/SymbolPane/C/CtagsSymbolIter.vala b/src/SymbolPane/C/CtagsSymbolIter.vala index 829a9abeac..91960fd24d 100644 --- a/src/SymbolPane/C/CtagsSymbolIter.vala +++ b/src/SymbolPane/C/CtagsSymbolIter.vala @@ -21,8 +21,22 @@ public class Scratch.Services.CtagsSymbolIter : Object { public string parent { get; construct set; } public int line { get; construct set; } public Icon? icon { get; construct set; } + public SymbolType? symbol_type; - public CtagsSymbolIter (string name, string parent, int line, Icon? icon) { - Object (name: name, parent: parent, line: line, icon: icon); + public CtagsSymbolIter ( + string name, + string parent, + int line, + Icon? icon, + SymbolType? s_type = SymbolType.OTHER) { + + Object ( + name: name, + parent: parent, + line: line, + icon: icon + ); + + symbol_type = s_type; } } diff --git a/src/SymbolPane/C/CtagsSymbolOutline.vala b/src/SymbolPane/C/CtagsSymbolOutline.vala index 8efb71666f..bcaf86e00a 100644 --- a/src/SymbolPane/C/CtagsSymbolOutline.vala +++ b/src/SymbolPane/C/CtagsSymbolOutline.vala @@ -21,10 +21,25 @@ public class Scratch.Services.CtagsSymbolOutline : Scratch.Services.SymbolOutlin public CtagsSymbolOutline (Scratch.Services.Document _doc) { Object ( + orientation: Gtk.Orientation.VERTICAL, + hexpand: true, doc: _doc ); } + static construct { + // Array of symbol types that could be assigned to a CtagsSymbol + // by parse output () + filters = { + SymbolType.CLASS, + SymbolType.CONSTRUCTOR, + SymbolType.PROPERTY, + SymbolType.METHOD, + SymbolType.STRUCT, + SymbolType.ENUM, + SymbolType.CONSTANT + }; + } construct { store.item_selected.connect ((selected) => { doc.goto (((CtagsSymbol)selected).line); @@ -74,66 +89,92 @@ public class Scratch.Services.CtagsSymbolOutline : Scratch.Services.SymbolOutlin int line = int.parse (parts[4].offset ("line:".length)); string? parent = null; GLib.Icon? parent_icon = null; + SymbolType? parent_s_type = null; if (parts.length > 5 && parts[5] != null) { if ("typeref:" in parts[5]) { parent = parts[5].offset ("typeref:".length); } else if ("class:" in parts[5]) { parent = parts[5].offset ("class:".length); parent_icon = new ThemedIcon ("lang-class"); + parent_s_type = SymbolType.CLASS; } else if ("struct:" in parts[5]) { parent = parts[5].offset ("struct:".length); parent_icon = new ThemedIcon ("lang-struct"); + parent_s_type = SymbolType.STRUCT; } else if ("enum:" in parts[5]) { parent = parts[5].offset ("enum:".length); parent_icon = new ThemedIcon ("lang-enum"); + parent_s_type = SymbolType.ENUM; } } Icon? icon = null; + SymbolType? s_type = null; switch (type) { case "class": icon = new ThemedIcon ("lang-class"); + s_type = SymbolType.CLASS; break; case "struct": icon = new ThemedIcon ("lang-struct"); + s_type = SymbolType.STRUCT; break; case "field": case "member": case "variable": icon = new ThemedIcon ("lang-property"); + s_type = SymbolType.PROPERTY; break; case "enum": case "enumerator": icon = new ThemedIcon ("lang-enum"); + s_type = SymbolType.ENUM; break; case "macro": case "constant": case "typedef": icon = new ThemedIcon ("lang-constant"); + s_type = SymbolType.CONSTANT; break; case "constructor": icon = new ThemedIcon ("lang-constructor"); + s_type = SymbolType.CONSTRUCTOR; break; case "destructor": case "method": case "function": icon = new ThemedIcon ("lang-method"); + s_type = SymbolType.METHOD; break; case "namespace": icon = new ThemedIcon ("lang-namespace"); + s_type = SymbolType.NAMESPACE; break; case "package": break; case "property": icon = new ThemedIcon ("lang-property"); + s_type = SymbolType.PROPERTY; break; } if (parent == null) { - var s = new CtagsSymbol (doc, name, line, icon); + var s = new CtagsSymbol ( + doc, + name, + line, + icon, + s_type + ); new_root.add (s); } else { - parent_dependent.add (new CtagsSymbolIter (name, parent, line, parent_icon)); + parent_dependent.add (new CtagsSymbolIter ( + name, + parent, + line, + parent_icon, + parent_s_type + )); } } } catch (Error e) { @@ -152,7 +193,13 @@ public class Scratch.Services.CtagsSymbolOutline : Scratch.Services.SymbolOutlin var parent = find_existing (i.parent, new_root); if (parent != null) { found_something = true; - parent.add (new CtagsSymbol (doc, i.name, i.line, i.icon)); + parent.add (new CtagsSymbol ( + doc, + i.name, + i.line, + i.icon, + i.symbol_type + )); iter.remove (); } else { if (":" in i.parent) { @@ -177,10 +224,22 @@ public class Scratch.Services.CtagsSymbolOutline : Scratch.Services.SymbolOutlin } // anonymous enum if (i.parent.has_prefix ("__anon")) { - var e = new CtagsSymbol (doc, i.parent, i.line - 1, new ThemedIcon ("lang-enum")); + var e = new CtagsSymbol ( + doc, + i.parent, + i.line - 1, + new ThemedIcon ("lang-enum"), + SymbolType.ENUM + ); new_root.add (e); - e.add (new CtagsSymbol (doc, i.name, i.line, i.icon)); + e.add (new CtagsSymbol ( + doc, + i.name, + i.line, + i.icon, + i.symbol_type + )); iter.remove (); } } @@ -189,7 +248,13 @@ public class Scratch.Services.CtagsSymbolOutline : Scratch.Services.SymbolOutlin // just add the rest foreach (var symbol in parent_dependent) { - new_root.add (new CtagsSymbol (doc, symbol.name, symbol.line, symbol.icon)); + new_root.add (new CtagsSymbol ( + doc, + symbol.name, + symbol.line, + symbol.icon, + symbol.symbol_type + )); } Idle.add (() => { diff --git a/src/SymbolPane/SymbolOutline.vala b/src/SymbolPane/SymbolOutline.vala index 74b2a221e9..a42a79f289 100644 --- a/src/SymbolPane/SymbolOutline.vala +++ b/src/SymbolPane/SymbolOutline.vala @@ -16,21 +16,216 @@ * along with this program. If not, see . */ -public abstract class Scratch.Services.SymbolOutline : Object { +public enum Scratch.Services.SymbolType { + CLASS, + PROPERTY, + SIGNAL, + METHOD, + STRUCT, + ENUM, + CONSTANT, + CONSTRUCTOR, + INTERFACE, + NAMESPACE, + OTHER; + + public unowned string to_string () { + switch (this) { + case SymbolType.CLASS: + return _("Class"); + case SymbolType.PROPERTY: + return _("Property"); + case SymbolType.SIGNAL: + return _("Signal"); + case SymbolType.METHOD: + return _("Method"); + case SymbolType.STRUCT: + return _("Struct"); + case SymbolType.ENUM: + return _("Enum"); + case SymbolType.CONSTANT: + return _("Constant"); + case SymbolType.CONSTRUCTOR: + return _("Constructor"); + case SymbolType.INTERFACE: + return _("Interface"); + case SymbolType.NAMESPACE: + return _("Namespace"); + case SymbolType.OTHER: + return _("Other"); + default: + assert_not_reached (); + } + } +} + +public interface Scratch.Services.SymbolItem : Code.Widgets.SourceList.ExpandableItem { + public abstract SymbolType symbol_type { get; set; default = SymbolType.OTHER;} +} + +public class Scratch.Services.SymbolOutline : Gtk.Box { + protected static SymbolType[] filters; //Initialized by derived classes + const string ACTION_GROUP = "symbol"; + const string ACTION_PREFIX = ACTION_GROUP + "."; + const string ACTION_SELECT = "action-select"; + const string ACTION_TOGGLE = "toggle-"; + SimpleActionGroup symbol_action_group; + public Scratch.Services.Document doc { get; construct; } + protected Gee.HashMap checks; + protected Gtk.SearchEntry search_entry; protected Code.Widgets.SourceList store; protected Code.Widgets.SourceList.ExpandableItem root; protected Gtk.CssProvider source_list_style_provider; - public Gtk.Widget get_widget () { return store; } - public abstract void parse_symbols (); + public Gtk.Widget get_widget () { return this; } + public virtual void parse_symbols () {} + + Gtk.MenuButton filter_button; construct { + symbol_action_group = new SimpleActionGroup (); + insert_action_group (ACTION_GROUP, symbol_action_group); + + checks = new Gee.HashMap (); store = new Code.Widgets.SourceList (); root = new Code.Widgets.SourceList.ExpandableItem (_("Symbols")); store.root.add (root); + search_entry = new Gtk.SearchEntry () { + placeholder_text = _("Find Symbol"), + hexpand = true + }; + + filter_button = new Gtk.MenuButton () { + image = new Gtk.Image.from_icon_name ( + "filter-symbolic", + Gtk.IconSize.SMALL_TOOLBAR + ), + tooltip_text = _("Filter symbol type"), + }; + + var select_section = new Menu (); + var top_model = new Menu (); + foreach (var filter in filters) { + add_filter_menuitem (top_model, filter); + } + + // Derived classes must not add SymbolType.OTHER + add_filter_menuitem (top_model, SymbolType.OTHER); + + var select_action = new SimpleAction ( + ACTION_SELECT, + new VariantType ("b") + ); + select_action.activate.connect (action_select_filters); + symbol_action_group.add_action (select_action); + + select_section.append ( + _("Select All"), + Action.print_detailed_name ( + ACTION_PREFIX + ACTION_SELECT, new Variant ("b", true) + ) + ); + select_section.append ( + _("Deselect All"), + Action.print_detailed_name ( + ACTION_PREFIX + ACTION_SELECT, new Variant ("b", false) + ) + ); + top_model.append_section ("", select_section); + + filter_button.menu_model = top_model; + + var tool_box = new Gtk.Box (HORIZONTAL, 3); + tool_box.add (search_entry); + tool_box.add (filter_button); + add (tool_box); + add (store); set_up_css (); + show_all (); + + realize.connect (() => { + store.set_filter_func (filter_func, false); + search_entry.changed.connect (schedule_refilter); + }); + } + + private void add_filter_menuitem (Menu menu, SymbolType filter) { + var filter_action = new SimpleAction.stateful ( + ACTION_TOGGLE + ((uint)filter).to_string (), + null, + new Variant.boolean (true) + ); + + checks[filter] = filter_action; + filter_action.activate.connect (action_toggle_filter); + symbol_action_group.add_action (filter_action); + + var filter_item = new MenuItem ( + filter.to_string (), + ACTION_PREFIX + filter_action.get_name () + ); + + menu.append_item (filter_item); + } + + protected bool filter_func (Object item) { + if (!(item is SymbolItem)) { + return true; + } + + var symbol_type = ((SymbolItem)item).symbol_type; + if (symbol_type == SymbolType.NAMESPACE) { + return true; + } + + if (checks[symbol_type] == null) { + symbol_type = SymbolType.OTHER; + } + + var filter_action = checks[symbol_type]; + + if (!filter_action.get_state ().get_boolean ()) { + return false; + } + + // Do not exclude text search misses on Item with children as may + // hide hits on its children + if (item is Code.Widgets.SourceList.ExpandableItem) { + var expandable = (Code.Widgets.SourceList.ExpandableItem)item; + if (expandable.n_children > 0) { + return true; + } + + return ((SymbolItem)item).name.contains (search_entry.text); + } + + return true; + } + + uint refilter_timeout_id = 0; + bool delay_refilter = false; + protected void schedule_refilter () { + // Ensure a refilter happens at least 500mS later if not already + // delayed. + if (refilter_timeout_id > 0) { + delay_refilter = true; + return; + } + + refilter_timeout_id = Timeout.add (500, () => { + if (delay_refilter) { + delay_refilter = false; + return Source.CONTINUE; + } else { + refilter_timeout_id = 0; + store.refilter (); + // Ensure new visible items shown when filter removed + store.root.expand_all (true, true); + return Source.REMOVE; + } + }); } protected void set_up_css () { @@ -41,8 +236,7 @@ public abstract class Scratch.Services.SymbolOutline : Object { Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION ); // Add a class to distinguish from foldermanager sourcelist - store.get_style_context ().add_class ("symbol-outline"); - + get_style_context ().add_class ("symbol-outline"); update_style_scheme (((Gtk.SourceBuffer)(doc.source_view.buffer)).style_scheme); doc.source_view.style_changed.connect (update_style_scheme); } @@ -56,7 +250,7 @@ public abstract class Scratch.Services.SymbolOutline : Object { color = text_color_data.background; } - var define = ".symbol-outline .sidebar {background-color: %s;}".printf (color); + var define = ".symbol-outline {background-color: %s;}".printf (color); try { source_list_style_provider.load_from_data (define); @@ -64,4 +258,22 @@ public abstract class Scratch.Services.SymbolOutline : Object { critical ("Unable to sourcelist styling, going back to classic styling"); } } + + private void action_select_filters (SimpleAction action, Variant? param) { + foreach (var filter_action in checks.values) { + filter_action.set_state (new Variant ("b", param.get_boolean ())); + } + schedule_refilter (); + // Keep menu open + Idle.add (() => { + filter_button.set_active (true); + return Source.REMOVE; + }); + } + + private void action_toggle_filter (SimpleAction action, Variant? param) { + var state = action.get_state ().get_boolean (); + action.set_state (new Variant ("b", !state)); + schedule_refilter (); + } } diff --git a/src/SymbolPane/Vala/ValaSymbolItem.vala b/src/SymbolPane/Vala/ValaSymbolItem.vala index f3d407c762..96b5613a7f 100644 --- a/src/SymbolPane/Vala/ValaSymbolItem.vala +++ b/src/SymbolPane/Vala/ValaSymbolItem.vala @@ -16,9 +16,9 @@ * */ -public class Scratch.Services.ValaSymbolItem : Code.Widgets.SourceList.ExpandableItem, Code.Widgets.SourceListSortable { +public class Scratch.Services.ValaSymbolItem : Code.Widgets.SourceList.ExpandableItem, Code.Widgets.SourceListSortable, Scratch.Services.SymbolItem { public Vala.Symbol symbol { get; set; } - + public SymbolType symbol_type { get; set; default = SymbolType.OTHER; } public ValaSymbolItem (Vala.Symbol symbol) { this.symbol = symbol; this.name = symbol.name; diff --git a/src/SymbolPane/Vala/ValaSymbolOutline.vala b/src/SymbolPane/Vala/ValaSymbolOutline.vala index c2f36f2ee9..f33520a509 100644 --- a/src/SymbolPane/Vala/ValaSymbolOutline.vala +++ b/src/SymbolPane/Vala/ValaSymbolOutline.vala @@ -22,10 +22,27 @@ public class Scratch.Services.ValaSymbolOutline : Scratch.Services.SymbolOutline private GLib.Cancellable cancellable; public ValaSymbolOutline (Scratch.Services.Document _doc) { Object ( + orientation: Gtk.Orientation.VERTICAL, + hexpand: true, doc: _doc ); } + static construct { + // Array of symbol types that could be assigned to a ValaSymbolItem + // by construct_child output () + filters = { + SymbolType.CLASS, + SymbolType.CONSTRUCTOR, + SymbolType.PROPERTY, + SymbolType.METHOD, + SymbolType.STRUCT, + SymbolType.ENUM, + SymbolType.CONSTANT, + SymbolType.INTERFACE + }; + } + construct { parser = new Vala.Parser (); resolver = new Code.Plugins.ValaSymbolResolver (); @@ -144,20 +161,27 @@ public class Scratch.Services.ValaSymbolOutline : Scratch.Services.SymbolOutline var tree_child = new ValaSymbolItem (symbol); if (symbol is Vala.Struct) { tree_child.icon = new ThemedIcon ("lang-struct"); + tree_child.symbol_type = SymbolType.STRUCT; } else if (symbol is Vala.Class) { if (((Vala.Class) symbol).is_abstract) { tree_child.icon = new ThemedIcon ("lang-class-abstract"); } else { tree_child.icon = new ThemedIcon ("lang-class"); } + + tree_child.symbol_type = SymbolType.CLASS; } else if (symbol is Vala.Constant) { tree_child.icon = new ThemedIcon ("lang-constant"); + tree_child.symbol_type = SymbolType.CONSTANT; } else if (symbol is Vala.Enum) { tree_child.icon = new ThemedIcon ("lang-enum"); + tree_child.symbol_type = SymbolType.ENUM; } else if (symbol is Vala.Field) { tree_child.icon = new ThemedIcon ("lang-property"); + tree_child.symbol_type = SymbolType.PROPERTY; } else if (symbol is Vala.Interface) { tree_child.icon = new ThemedIcon ("lang-interface"); + tree_child.symbol_type = SymbolType.INTERFACE; } else if (symbol is Vala.Property) { if (((Vala.Property) symbol).is_abstract) { tree_child.icon = new ThemedIcon ("lang-property-abstract"); @@ -166,10 +190,14 @@ public class Scratch.Services.ValaSymbolOutline : Scratch.Services.SymbolOutline } else { tree_child.icon = new ThemedIcon ("lang-property"); } + + tree_child.symbol_type = SymbolType.PROPERTY; } else if (symbol is Vala.Signal) { tree_child.icon = new ThemedIcon ("lang-signal"); + tree_child.symbol_type = SymbolType.SIGNAL; } else if (symbol is Vala.CreationMethod) { tree_child.icon = new ThemedIcon ("lang-constructor"); + tree_child.symbol_type = SymbolType.CONSTRUCTOR; } else if (symbol is Vala.Method) { if (((Vala.Method) symbol).is_abstract) { tree_child.icon = new ThemedIcon ("lang-method-abstract"); @@ -180,8 +208,11 @@ public class Scratch.Services.ValaSymbolOutline : Scratch.Services.SymbolOutline } else { tree_child.icon = new ThemedIcon ("lang-method"); } + + tree_child.symbol_type = SymbolType.METHOD; } else if (symbol is Vala.Namespace) { tree_child.icon = new ThemedIcon ("lang-namespace"); + tree_child.symbol_type = SymbolType.NAMESPACE; } else if (symbol is Vala.ErrorDomain) { tree_child.icon = new ThemedIcon ("lang-errordomain"); } else if (symbol is Vala.Delegate) {