diff --git a/meson.build b/meson.build index ae6bb0b225..7a365a7e3a 100644 --- a/meson.build +++ b/meson.build @@ -30,7 +30,7 @@ glib_dep = dependency('glib-2.0', version: '>=2.74.0') gio_unix_dep = dependency('gio-unix-2.0', version: '>=2.20') gee_dep = dependency('gee-0.8', version: '>=0.8.5') gtk_dep = dependency('gtk4', version: '>=4.14.2') -granite_dep = dependency('granite-7', version: '>=7.0.0') +granite_dep = dependency('granite-9', version: '>=9.0.0') adwaita_dep = dependency('libadwaita-1', version: '>=1.0.0') gtksourceview_dep = dependency('gtksourceview-5') peas_dep = dependency('libpeas-2') diff --git a/plugins/fuzzy-search/fuzzy-search.vala b/plugins/fuzzy-search/fuzzy-search.vala index 169fd5a801..5c5d1f6091 100644 --- a/plugins/fuzzy-search/fuzzy-search.vala +++ b/plugins/fuzzy-search/fuzzy-search.vala @@ -40,6 +40,11 @@ public class Scratch.Plugins.FuzzySearch: Peas.ExtensionBase, Scratch.Services.A plugins = (Scratch.Services.Interface) object; plugins.hook_window.connect ((w) => { + if (w.sidebar == null) { + warning ("sidebar is NULL"); + return; + } + if (window != null) { return; } @@ -89,6 +94,7 @@ public class Scratch.Plugins.FuzzySearch: Peas.ExtensionBase, Scratch.Services.A fuzzy_menuitem = new GLib.MenuItem (_("Find Project Files"), ACTION_PREFIX + ACTION_SHOW ); + warning ("get sidebar project_model.sidebar is %s null", window.sidebar != null ? "NOT" : ""); var menu = window.sidebar.project_menu_model as GLib.Menu; menu.append_item (fuzzy_menuitem); } @@ -126,10 +132,7 @@ public class Scratch.Plugins.FuzzySearch: Peas.ExtensionBase, Scratch.Services.A var popover = new Scratch.FuzzySearchPopover (indexer, window); popover.open_file.connect ((filepath) => { - var file = new Scratch.FolderManager.File (filepath); - var doc = new Scratch.Services.Document (window.actions, file.file); - - window.open_document.begin (doc); + window.document_view.open_document.begin (filepath); popover.popdown (); }); diff --git a/src/Application.vala b/src/Application.vala index ee7b260081..593da2eab0 100644 --- a/src/Application.vala +++ b/src/Application.vala @@ -178,16 +178,18 @@ namespace Scratch { var window = get_last_window (); foreach (var file in files) { bool is_folder; + var path = file.get_uri (); if (Scratch.Services.FileHandler.can_open_file (file, out is_folder)) { if (is_folder) { - window.open_folder (file); + window.open_folder_as_project (file); // Opens in sidebar not as documents } else { debug ("Files length: %d\n", files.length); - var doc = new Scratch.Services.Document (window.actions, file); + // var doc = new Scratch.Services.Document (window.actions, file); if (location_jump_manager.has_selection_range != null && files.length == 1) { - window.open_document_at_selected_range.begin (doc, true, location_jump_manager.range); + // We know file exists so we do not need to check again + window.document_view.open_document.begin (path, true, 0, location_jump_manager.range); } else { - window.open_document.begin (doc); + window.document_view.open_document.begin (path); } } } @@ -231,6 +233,24 @@ namespace Scratch { return true; } + public bool can_rename_file (Code.File file) { + // folder_manager_view.rename_request.connect ((file) => { + var allow = true; + foreach (var window in get_windows ()) { + var win = (MainWindow)window; + foreach (var doc in win.document_view.docs) { + if (doc.file.equal (file.file)) { + // Only allow sidebar to rename docs that are in sync with their file in + // all windows + allow = allow && !doc.locked && doc.saved; + } + } + } + + return allow; + // }); + } + public static int main (string[] args) { // By default, profile whole app when profiling is enabled in meson_options.txt // These conditional statements can be moved to profile sections of code diff --git a/src/Dialogs/BranchActions/BranchActionDialog.vala b/src/Dialogs/BranchActions/BranchActionDialog.vala index 4c1a55e80d..d29d20fda1 100644 --- a/src/Dialogs/BranchActions/BranchActionDialog.vala +++ b/src/Dialogs/BranchActions/BranchActionDialog.vala @@ -43,11 +43,11 @@ public class Scratch.Dialogs.BranchActionDialog : Granite.MessageDialog { } public bool can_apply { get; set; default = false; } - public FolderManager.ProjectFolderItem project { get; construct; } + public Code.ProjectFolderItem project { get; construct; } private Gtk.Stack stack; - public BranchActionDialog (FolderManager.ProjectFolderItem project) { + public BranchActionDialog (Code.ProjectFolderItem project) { Object ( project: project, modal: true diff --git a/src/FolderManager/File.vala b/src/FolderManager/File.vala index 8bef07dee7..6787ff50e1 100644 --- a/src/FolderManager/File.vala +++ b/src/FolderManager/File.vala @@ -1,216 +1,216 @@ /*- - * Copyright (c) 2017-2025 elementary LLC. (https://elementary.io), - * 2013 Julien Spautz - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License version 3 - * as published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranties of - * MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR - * PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - * - * Authored by: Julien Spautz , Andrei-Costin Zisu - */ - -namespace Scratch.FolderManager { - - /** - * Class for easily dealing with files. - */ - public class File : GLib.Object { - public GLib.File file { get; private set; } - private GLib.FileInfo? info = null; // Non-null after loading - - public File (string path) { - Object (path: path); - } +* Copyright (c) 2017-2025 elementary LLC. (https://elementary.io), +* 2013 Julien Spautz +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU Lesser General Public License version 3 +* as published by the Free Software Foundation, either version 3 of the +* License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranties of +* MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR +* PURPOSE. See the GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see . +* +* Authored by: Julien Spautz , Andrei-Costin Zisu +*/ + +public class Code.File : GLib.Object { + public GLib.File file { get; set construct; } + private GLib.FileInfo? info = null; // Non-null after loading + + public File (string path) { + Object (path: path); + } - // returns the path the file - public string path { - owned get { - return file.get_path (); - } - set construct { - load_file_for_path (value); - } + construct { + file = GLib.File.new_for_path (path); + info = new FileInfo (); + try { + var query_string = GLib.FileAttribute.STANDARD_CONTENT_TYPE + "," + + GLib.FileAttribute.STANDARD_IS_BACKUP + "," + + GLib.FileAttribute.STANDARD_IS_HIDDEN + "," + + GLib.FileAttribute.STANDARD_DISPLAY_NAME + "," + + GLib.FileAttribute.STANDARD_TYPE; + + info = file.query_info (query_string, FileQueryInfoFlags.NONE); + } catch (GLib.Error error) { + info = null; + warning (error.message); } - - // returns the basename of the file - private string _name; - public string name { - get { - if (_name != null) { - return _name; - } - - if (info == null) { - _name = file.get_basename (); - } else { - _name = info.get_display_name (); - } - + } + // returns the path the file + public string path { get; construct; } + // owned get { + // return file.get_path (); + // } + // set construct { + // load_file_for_path (value); + // } + // } + + // returns the basename of the file + private string _name; + public string name { + get { + if (_name != null) { return _name; } - } - - // returns the icon of the file's content type - private GLib.Icon? _icon = null; - public GLib.Icon icon { - get { - if (_icon != null) { - return _icon; - } - if (info != null) { - _icon = GLib.ContentType.get_icon (info.get_content_type ()); - } else { - _icon = new ThemedIcon ("missing-image"); - } - - return _icon; + if (info == null) { + _name = file.get_basename (); + } else { + _name = info.get_display_name (); } - } - // checks if file exists - public bool exists { - get { return file.query_exists (); } + return _name; } + } - // Checks if we're dealing with a non-backup directory - // If parent is hidden then inherit validity from parent - private bool? _is_valid_directory = null; - public bool is_valid_directory { - get { - if (_is_valid_directory == null) { - _is_valid_directory = info != null && - !info.get_is_backup () && - info.get_file_type () == FileType.DIRECTORY; - } - - return _is_valid_directory; + // returns the icon of the file's content type + private GLib.Icon? _icon = null; + public GLib.Icon icon { + get { + if (_icon != null) { + return _icon; } - } - // checks if we're dealing with a textfile - private bool? _is_valid_textfile = null; - public bool is_valid_textfile { - get { - if (_is_valid_textfile == null) { - _is_valid_textfile = !path.has_suffix ("~") && Utils.check_if_valid_text_file (path, info); - } - - return _is_valid_textfile; + if (info != null) { + _icon = GLib.ContentType.get_icon (info.get_content_type ()); + } else { + _icon = new ThemedIcon ("missing-image"); } - } - // Files can be executed and folders can be cd'd into - public bool is_executable { - get { - try { - return get_boolean_file_attribute (GLib.FileAttribute.ACCESS_CAN_EXECUTE); - } catch (GLib.Error error) { - return false; - } - } + return _icon; } + } - // returns a list of all children of a directory - private bool children_valid = false; - private Gee.ArrayList _children = new Gee.ArrayList (); - public Gee.Collection children { - owned get { - if (children_valid) { - return _children; - } - - _children.clear (); - - try { - var enumerator = file.enumerate_children ( - GLib.FileAttribute.STANDARD_NAME, - FileQueryInfoFlags.NONE - ); - - var file_info = new FileInfo (); - while ((file_info = enumerator.next_file ()) != null) { - var child = file.get_child (file_info.get_name ()); - var child_file = new File (child.get_path ()); - if (child_file.is_valid_directory || child_file.is_valid_textfile) { - _children.add (child_file); - } - } + //TODO Only keep icon name to match Granite.TreeListItem? + public string icon_name { + owned get { + return ((ThemedIcon)icon).names[0]; + } + } - children_valid = true; - } catch (GLib.Error error) { - warning (error.message); - } + // checks if file exists + public bool exists { + get { return file.query_exists (); } + } - return _children; + // Checks if we're dealing with a non-backup directory + // If parent is hidden then inherit validity from parent + private bool? _is_valid_directory = null; + public bool is_valid_directory { + get { + if (_is_valid_directory == null) { + _is_valid_directory = info != null && + !info.get_is_backup () && + info.get_file_type () == FileType.DIRECTORY; } + + return _is_valid_directory; } + } - private bool get_boolean_file_attribute (string attribute) throws GLib.Error { - var info = file.query_info (attribute, GLib.FileQueryInfoFlags.NONE); + // checks if we're dealing with a textfile + private bool? _is_valid_textfile = null; + public bool is_valid_textfile { + get { + if (_is_valid_textfile == null) { + _is_valid_textfile = !path.has_suffix ("~") && Scratch.Utils.check_if_valid_text_file (path, info); + } - return info.get_attribute_boolean (attribute); + return _is_valid_textfile; } + } - private void load_file_for_path (string path) { - file = GLib.File.new_for_path (path); - - info = new FileInfo (); + // Files can be executed and folders can be cd'd into + public bool is_executable { + get { try { - var query_string = GLib.FileAttribute.STANDARD_CONTENT_TYPE + "," + - GLib.FileAttribute.STANDARD_IS_BACKUP + "," + - GLib.FileAttribute.STANDARD_IS_HIDDEN + "," + - GLib.FileAttribute.STANDARD_DISPLAY_NAME + "," + - GLib.FileAttribute.STANDARD_TYPE; - - info = file.query_info (query_string, FileQueryInfoFlags.NONE); + return get_boolean_file_attribute (GLib.FileAttribute.ACCESS_CAN_EXECUTE); } catch (GLib.Error error) { - info = null; - warning (error.message); + return false; } } + } - public void rename (string name) { - try { - if (exists) { - file.set_display_name (name); - } - } catch (GLib.Error error) { - warning (error.message); + // returns a list of all children of a directory + private bool children_valid = false; + private Gee.ArrayList _children = new Gee.ArrayList (); + public Gee.Collection children { + owned get { + if (children_valid) { + return _children; } - } - public void trash () { + _children.clear (); + try { - file.trash (); + var enumerator = file.enumerate_children ( + GLib.FileAttribute.STANDARD_NAME, + FileQueryInfoFlags.NONE + ); + + var file_info = new FileInfo (); + while ((file_info = enumerator.next_file ()) != null) { + var child = file.get_child (file_info.get_name ()); + var child_file = new File (child.get_path ()); + if (child_file.is_valid_directory || child_file.is_valid_textfile) { + _children.add (child_file); + } + } + + children_valid = true; } catch (GLib.Error error) { warning (error.message); } + + return _children; } + } - public static int compare (File a, File b) { - if (a.is_valid_directory && b.is_valid_textfile) { - return -1; - } - if (a.is_valid_textfile && b.is_valid_directory) { - return 1; + private bool get_boolean_file_attribute (string attribute) throws GLib.Error { + var info = file.query_info (attribute, GLib.FileQueryInfoFlags.NONE); + + return info.get_attribute_boolean (attribute); + } + + public void rename (string name) { + try { + if (exists) { + file.set_display_name (name); } + } catch (GLib.Error error) { + warning (error.message); + } + } - return strcmp (a.path.collate_key_for_filename (), - b.path.collate_key_for_filename ()); + public void trash () { + try { + file.trash (); + } catch (GLib.Error error) { + warning (error.message); } + } - public void invalidate_cache () { - children_valid = false; + public static int compare (File a, File b) { + if (a.is_valid_directory && b.is_valid_textfile) { + return -1; + } + if (a.is_valid_textfile && b.is_valid_directory) { + return 1; } + + return strcmp (a.path.collate_key_for_filename (), + b.path.collate_key_for_filename ()); + } + + public void invalidate_cache () { + children_valid = false; } } + diff --git a/src/FolderManager/FileItem.vala b/src/FolderManager/FileItem.vala index d75ff5841b..dffdf57278 100644 --- a/src/FolderManager/FileItem.vala +++ b/src/FolderManager/FileItem.vala @@ -18,101 +18,104 @@ * Authored by: Julien Spautz , Andrei-Costin Zisu */ -namespace Scratch.FolderManager { - /** - * Normal item in the source list, represents a textfile. - */ - public class FileItem : Item { - public FileItem (File file, FileView view) { - Object (file: file, view: view); - } - - // The ListView has an activate signal - // public override void activated () { - // view.activate (file.path); - // } - - public override GLib.Menu? get_context_menu () { - GLib.FileInfo info = null; - - try { - info = file.file.query_info (GLib.FileAttribute.STANDARD_CONTENT_TYPE, 0); - } catch (Error e) { - warning (e.message); - } - - var file_type = info.get_attribute_string (GLib.FileAttribute.STANDARD_CONTENT_TYPE); - var contractor_items = Utils.create_contract_items_for_file (file.file); - var external_actions_section = new GLib.Menu (); - external_actions_section.append_item (create_submenu_for_open_in (file_type)); - if (contractor_items.get_n_items () > 0) { - external_actions_section.append_submenu ( - _("Other Actions"), - contractor_items - ); - } - - var rename_item = new GLib.MenuItem ( - _("Rename"), - GLib.Action.print_detailed_name ( - FileView.ACTION_PREFIX + FileView.ACTION_RENAME_FILE, - new Variant.string (file.path) - ) - ); - var rename_action = Utils.action_from_group (FileView.ACTION_RENAME_FILE, view.actions); - rename_action.set_enabled (view.rename_request (file)); +public class Code.FileItem : FolderManagerItem, Code.FolderManagerItemInterface { + public FileItem (File file, FolderTree view) { + Object (file: file, view: view); + } - var delete_item = new GLib.MenuItem ( - _("Move to Trash"), - GLib.Action.print_detailed_name ( - FileView.ACTION_PREFIX + FileView.ACTION_DELETE, - new Variant.string (file.path) - ) - ); + // The ListView has an activate signal + // public override void activated () { + // view.activate (file.path); + // } - var direct_actions_section = new GLib.Menu (); - direct_actions_section.append_item (rename_item); - direct_actions_section.append_item (delete_item); + // public override GLib.Menu? get_context_menu () { + // GLib.FileInfo info = null; - var menu_model = new GLib.Menu (); - menu_model.append_section (null, external_actions_section); - menu_model.append_section (null, direct_actions_section); + // try { + // info = file.file.query_info (GLib.FileAttribute.STANDARD_CONTENT_TYPE, 0); + // } catch (Error e) { + // warning (e.message); + // } - return menu_model; - } + // var file_type = info.get_attribute_string (GLib.FileAttribute.STANDARD_CONTENT_TYPE); + // var contractor_items = create_contract_items_for_file (file.file); // Interface method + // var external_actions_section = new GLib.Menu (); + // external_actions_section.append_item (create_submenu_for_open_item_in (file_type)); + // if (contractor_items.get_n_items () > 0) { + // external_actions_section.append_submenu ( + // _("Other Actions"), + // contractor_items + // ); + // } - private GLib.MenuItem create_submenu_for_open_in (string? file_type) { + // var rename_item = new GLib.MenuItem ( + // _("Rename"), + // GLib.Action.print_detailed_name ( + // ITEM_ACTION_PREFIX + ACTION_RENAME_FILE, + // new Variant.string (file.path) + // ) + // ); + // var rename_action = Scratch.Utils.action_from_group (ACTION_RENAME_FILE, view.actions); + // var app = (Scratch.Application) (Application.get_default ()); + // rename_action.set_enabled (app.can_rename_file (file)); + // rename_action.set_enabled (view.rename_request (file)); + + // var delete_item = new GLib.MenuItem ( + // _("Move to Trash"), + // GLib.Action.print_detailed_name ( + // ITEM_ACTION_PREFIX + ACTION_DELETE, + // new Variant.string (file.path) + // ) + // ); + + // var direct_actions_section = new GLib.Menu (); + // direct_actions_section.append_item (rename_item); + // direct_actions_section.append_item (delete_item); + + // var menu_model = new GLib.Menu (); + // menu_model.append_section (null, external_actions_section); + // menu_model.append_section (null, direct_actions_section); + + // return menu_model; + // } + + public override GLib.MenuItem create_submenu_for_open_item_in (GLib.File gfile, string? file_type) { + var menu_item = base.create_submenu_for_open_item_in (gfile, file_type); + var sub_menu = (GLib.Menu) (menu_item.get_link ("submenu")); + + if (file.is_valid_textfile) { + var top_section = new GLib.Menu (); var new_window_menu_item = new GLib.MenuItem ( _("New Window"), GLib.Action.print_detailed_name ( - MainWindow.ACTION_PREFIX + MainWindow.ACTION_OPEN_IN_NEW_WINDOW, - file.path + Scratch.MainWindow.ACTION_PREFIX + Scratch.MainWindow.ACTION_OPEN_IN_NEW_WINDOW, + path ) ); + top_section.append_item (new_window_menu_item); + sub_menu.prepend_section (null, top_section); + } - var top_section = new GLib.Menu (); - if (file.is_valid_textfile) { - top_section.append_item (new_window_menu_item); - } + //TODO Add section for regular file only actions - var other_menu_item = new GLib.MenuItem ( - _("Other Application…"), - GLib.Action.print_detailed_name ( - FileView.ACTION_PREFIX + FileView.ACTION_SHOW_APP_CHOOSER, - file.path - ) - ); + // var other_menu_item = new GLib.MenuItem ( + // _("Other Application…"), + // GLib.Action.print_detailed_name ( + // ITEM_ACTION_PREFIX + ACTION_SHOW_APP_CHOOSER, + // file.path + // ) + // ); - var extra_section = new GLib.Menu (); - extra_section.append_item (other_menu_item); + // var extra_section = new GLib.Menu (); + // extra_section.append_item (other_menu_item); - var open_in_menu = new GLib.Menu (); - open_in_menu.append_section (null, top_section); - open_in_menu.append_section (null, Utils.create_executable_app_items_for_file (file.file, file_type)); - open_in_menu.append_section (null, extra_section); + // var open_in_menu = new GLib.Menu (); + // open_in_menu.append_section (null, top_section); + // open_in_menu.append_section (null, create_executable_app_items_for_file (file.file, file_type)); + // open_in_menu.append_section (null, extra_section); - var open_in_menu_item = new GLib.MenuItem.submenu (_("Open In"), open_in_menu); - return open_in_menu_item; - } + // var open_in_menu_item = new GLib.MenuItem.submenu (_("Open In"), open_in_menu); + return menu_item; } } + diff --git a/src/FolderManager/FileView.vala b/src/FolderManager/FileView.vala deleted file mode 100644 index fb9a814d84..0000000000 --- a/src/FolderManager/FileView.vala +++ /dev/null @@ -1,768 +0,0 @@ -/*- - * Copyright (c) 2017-2026 elementary LLC. (https://elementary.io), - * 2013 Julien Spautz - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License version 3 - * as published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranties of - * MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR - * PURPOSE. See the GNU General Public License for more details. - - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - * - * Authored by: Julien Spautz , Andrei-Costin Zisu - */ - -/** - * SourceList that displays folders and their contents. - */ -public class Scratch.FolderManager.FileView : Code.TreeList, Code.PaneSwitcher { - public const string ACTION_GROUP = "file-view"; - public const string ACTION_PREFIX = ACTION_GROUP + "."; - public const string ACTION_LAUNCH_APP_WITH_FILE_PATH = "launch-app-with-file-path"; - public const string ACTION_SHOW_APP_CHOOSER = "show-app-chooser"; - public const string ACTION_EXECUTE_CONTRACT_WITH_FILE_PATH = "execute-contract-with-file-path"; - public const string ACTION_RENAME_FILE = "rename-file"; - public const string ACTION_RENAME_FOLDER = "rename-folder"; - public const string ACTION_DELETE = "delete"; - public const string ACTION_NEW_FILE = "new-file"; - public const string ACTION_NEW_FROM_TEMPLATE = "new-from-template"; - public const string ACTION_NEW_FOLDER = "new-folder"; - public const string ACTION_CLOSE_FOLDER = "close-folder"; - public const string ACTION_CLOSE_OTHER_FOLDERS = "close-other-folders"; - - private const ActionEntry[] ACTION_ENTRIES = { - { ACTION_LAUNCH_APP_WITH_FILE_PATH, action_launch_app_with_file_path, "as" }, - { ACTION_SHOW_APP_CHOOSER, action_show_app_chooser, "s" }, - { ACTION_EXECUTE_CONTRACT_WITH_FILE_PATH, action_execute_contract_with_file_path, "as" }, - { ACTION_RENAME_FILE, action_rename_file, "s" }, - { ACTION_RENAME_FOLDER, action_rename_folder, "s" }, - { ACTION_DELETE, action_delete, "s" }, - { ACTION_NEW_FILE, add_new_file, "s" }, - { ACTION_NEW_FROM_TEMPLATE, add_new_from_template, "(ss)" }, - { ACTION_NEW_FOLDER, add_new_folder, "s"}, - { ACTION_CLOSE_FOLDER, action_close_folder, "s"}, - { ACTION_CLOSE_OTHER_FOLDERS, action_close_other_folders, "s"} - }; - - private GLib.Settings settings; - private Scratch.Services.GitManager git_manager; - private Scratch.Services.PluginsManager plugins; - - public signal void file_activate (File file); - public signal bool rename_request (File file); - - public SimpleActionGroup actions { get; private set; } - public string icon_name { get; set; } - public string title { get; set; } - - public FileView (Scratch.Services.PluginsManager plugins_manager) { - plugins = plugins_manager; - } - - construct { - icon_name = "folder-symbolic"; - title = _("Folders"); - - settings = new GLib.Settings ("io.elementary.code.folder-manager"); - - git_manager = Scratch.Services.GitManager.get_instance (); - - actions = new SimpleActionGroup (); - actions.add_action_entries (ACTION_ENTRIES, this); - insert_action_group (ACTION_GROUP, actions); - - Scratch.saved_state.changed["order-folders"].connect (() => { - order_folders (); - }); - - // Convert ListView signal into file_activate - item_activated.connect ((item) => { - if (item is FileItem) { - file_activate (((FileItem) item).file); - } - }); - } - - private void action_close_folder (SimpleAction action, GLib.Variant? parameter) { - var path = parameter.get_string (); - if (path == null || path == "") { - return; - } - - var project_item = find_path (null, path) as ProjectFolderItem; - if (project_item == null) { - return; - } - - project_item.closed (); - } - - private void action_close_other_folders (SimpleAction action, GLib.Variant? parameter) { - var path = parameter.get_string (); - if (path == null || path == "") { - return; - } - - var folder_root = find_path (null, path) as ProjectFolderItem; - if (folder_root == null) { - return; - } - - List to_remove = null; - iterate_children (null, (child) => { - var project_folder_item = (ProjectFolderItem) child; - if (project_folder_item != folder_root) { - activate_action ( - MainWindow.ACTION_PREFIX + MainWindow.ACTION_CLOSE_PROJECT_DOCS, - "s", - project_folder_item.path - ); - to_remove.prepend (project_folder_item); - git_manager.remove_project (project_folder_item); - } - - return Code.TreeList.ITERATE_CONTINUE; - }); - - remove_root_children (to_remove); - //Make remaining project the active one - set_project_active (path); - } - - private void action_set_active_project (SimpleAction action, GLib.Variant? parameter) { - var path = parameter.get_string (); - if (path == null || path == "") { - return; - } - - set_active_project (path); - } - - private ProjectFolderItem? set_active_project (string path) { - var folder_root = find_path (null, path) as ProjectFolderItem; - if (folder_root == null) { - return null; - } - - git_manager.active_project_path = path; - - write_settings (); - - return folder_root; - } - - private void set_project_active (string path) { - activate_action ( - MainWindow.ACTION_PREFIX + MainWindow.ACTION_SET_ACTIVE_PROJECT, - "s", - path - ); - } - - public async void restore_saved_state () { - foreach (unowned string path in settings.get_strv ("opened-folders")) { - yield add_folder (new File (path), false, true); - } - } - - public void open_folder (File folder) { - if (is_open (folder)) { - var existing = find_path (null, folder.path); - if (existing is Code.Widgets.SourceList.ExpandableItem) { - ((Code.Widgets.SourceList.ExpandableItem)existing).expanded = true; - } - - return; - } - - add_folder.begin (folder, true); - } - - public void collapse_all () { - iterate_children (null, (child) => { - child.collapse_all (true, true); - return Code.TreeList.ITERATE_CONTINUE; - }); - } - - public void order_folders () { - if (!Scratch.saved_state.get_boolean ("order-folders")) { - return; - } - - sort_root_children ((a, b) => { - return strcmp ( - ((ProjectFolderItem)a).name.down (), - ((ProjectFolderItem)b).name.down () - ); - }); - } - - public void select_path (string path) { - find_path (null, path); - } - - public void unselect_file (GLib.File file) { - //TODO Complete this - } - - public void unselect_all () { - selection_model.unselect_all (); - } - - public void collapse_other_projects () { - unowned string path; - path = git_manager.active_project_path; - - iterate_children (null, (child) => { - var project_folder = ((ProjectFolderItem) child); - if (project_folder.path != path) { - project_folder.is_expanded = false; - activate_action ( - MainWindow.ACTION_PREFIX + MainWindow.ACTION_HIDE_PROJECT_DOCS, - "s", - project_folder.path - ); - } else if (project_folder.path == path) { - project_folder.is_expanded = true; - activate_action ( - MainWindow.ACTION_PREFIX + MainWindow.ACTION_RESTORE_PROJECT_DOCS, - "s", - project_folder.path - ); - } - - return Code.TreeList.ITERATE_CONTINUE; - }); - } - - public void branch_actions (string path) { - // Must only carry out branch actions on active project so switch if necessary. - //TODO Warn before switching active project? - var active_project = set_active_project (path); - if (active_project == null || !active_project.is_git_repo) { - Gdk.Display.get_default ().beep (); - return; - } - - var dialog = new Dialogs.BranchActionDialog (active_project); - dialog.response.connect ((res) => { - if (res == Gtk.ResponseType.APPLY) { - perform_branch_action (dialog); - } - - dialog.destroy (); - }); - - dialog.present (); - } - - private void perform_branch_action ( - Scratch.Dialogs.BranchActionDialog dialog - ) { - switch (dialog.action) { - case CHECKOUT: - dialog.project.checkout_branch_ref (dialog.branch_ref); - break; - case COMMIT: - break; - case PUSH: - break; - case PULL: - break; - case MERGE: - break; - case DELETE: - break; - case CREATE: - dialog.project.new_branch (dialog.new_branch_name); - break; - default: - assert_not_reached (); - } - } - - private Code.TreeListItem? find_path ( - Code.TreeListItem? list, // Starting point for search - string path, // File path to search fod - bool expand = false, // Whether to expsnd to show found item - GLib.File? target_file = null // Alternatively find this file - ) { - - var model = list != null ? list.child_model : root_model; - var target = target_file ?? GLib.File.new_for_path (path); - Code.TreeListItem? matched_item = null; - - iterate_children (null, (item) => { - if ((item is Item) && item.path == path) { - matched_item = item; - return Code.TreeList.ITERATE_STOP; - } - - if (item is FolderItem) { - var folder = item as FolderItem; - var folder_root = folder.file.file; - if (folder_root.get_relative_path (target) == null) { - return Code.TreeList.ITERATE_CONTINUE; - } - - if (!folder.is_expanded) { - if (expand) { - folder.load_children (); //Synchronous - folder.is_expanded = true; - } else { - return Code.TreeList.ITERATE_CONTINUE; - } - } - - var recurse_item = find_path (folder, path, expand, target); - if (recurse_item != null) { - matched_item = recurse_item; - return Code.TreeList.ITERATE_STOP; - } - } - - return Code.TreeList.ITERATE_CONTINUE; - }); - - return matched_item; - } - - public bool project_is_open (string project_path) { - return get_project_for_file (GLib.File.new_for_path (project_path)) != null; - } - - public ProjectFolderItem? get_project_for_file (GLib.File file) { - ProjectFolderItem? matched_project = null; - iterate_children (null, (item) => { - if (item is ProjectFolderItem) { - var folder = (ProjectFolderItem) item; - if (folder.file.file.equal (file) || folder.contains_file (file)) { - matched_project = folder; - return Code.TreeList.ITERATE_STOP; - } - } - - return Code.TreeList.ITERATE_CONTINUE; - }); - - return matched_project; - } - - public Code.TreeListItem? expand_to_path (string path) { - return find_path (null, path, true); - } - - /* Do global search on project containing the file path supplied in parameter */ - public void search_global (string path, string? term = null) { - var item_for_path = (Item?)(expand_to_path (path)); - if (item_for_path != null) { - var search_root = item_for_path.get_root_folder (); - if (search_root is ProjectFolderItem) { - GLib.File start_folder = (item_for_path is FolderItem) - ? item_for_path.file.file - : search_root.file.file; - - bool is_explicit = !(item_for_path is ProjectFolderItem); - search_root.global_search.begin (start_folder, term, is_explicit); - } - } - } - - public void clear_badges () { - iterate_children (null, (child) => { - if (child is ProjectFolderItem) { - ((FolderItem)child).remove_all_badges (); - } - - return Code.TreeList.ITERATE_CONTINUE; - }); - } - - public void folder_item_update_hook (GLib.File source, GLib.File? dest, GLib.FileMonitorEvent event) { - plugins.hook_folder_item_change (source, dest, event); - } - - // This only works when the list is stable (nothing being added, expanded etc) - private void rename_file (string path) { - // this.select_path (path); - // if (this.start_editing_item (selected)) { - // ulong once = 0; - // once = selected.edited.connect ((new_name) => { - // selected.disconnect (once); - // var new_path = Path.get_dirname (path) + Path.DIR_SEPARATOR_S + new_name; - // activate_action ( - // MainWindow.ACTION_PREFIX + MainWindow.ACTION_CLOSE_TAB, - // "s", - // path - // ); - - // // RecentManager requires valid URI - // var new_uri = "file://" + new_path; // Code only edits local files - // Gtk.RecentManager.get_default ().add_item (new_uri); - - // activate (new_path); - // }); - // } - - // // Handle cancelled rename (which does not produce signal) - // Timeout.add (200, () => { - // if (this.editing) { - // return Source.CONTINUE; - // } else { - // // Avoid selected but unopened item if rename cancelled (they would not open if clicked on) - // this.unselect_all (); - // return Source.REMOVE; - // } - // }); - } - - private void rename_folder (string path) { - // var folder_to_rename = find_path (null, path) as FolderItem; - // if (folder_to_rename == null) { - // critical ("Could not find folder from given path to rename: %s", path); - // return; - // } - - // folder_to_rename.selectable = true; - // if (start_editing_item (folder_to_rename)) { - // // Need to poll view as no signal emited when editing cancelled and need to set - // // selectable to false anyway. - // Timeout.add (200, () => { - // if (editing) { - // return Source.CONTINUE; - // } else { - // unselect_all (); - // // Must do this *after* unselecting all else sourcelist breaks - // folder_to_rename.selectable = false; - // } - - // return Source.REMOVE; - // }); - // } else { - // critical ("Could not rename %s", path); - // folder_to_rename.selectable = false; - // } - } - - private void rename_items_with_same_name (Item item) { - // string item_name = item.file.name; - // iterate_children (null, (child) => { - // string new_other_item_name, new_item_name; - // var other_item = (ProjectFolderItem) child; - - // if (Utils.find_unique_path ( - // item.file.file, - // other_item.file.file, - // out new_item_name, - // out new_other_item_name - // ) - // ) { - // if (item_name.length < new_item_name.length) { - // item_name = new_item_name; - // } - - // if (other_item.name.length < new_other_item_name.length) { - // other_item.name = new_other_item_name; - // } - // } - - // return Code.TreeList.ITERATE_CONTINUE; - // }); - - // item.name = item_name; - } - - private void add_new_folder (SimpleAction action, Variant? param) { - // Using "path" of parent folder from params, call `on_add_new (true)` on `FolderItem` - var path = param.get_string (); - - if (path == null || path == "") { - return; - } - - var folder = find_path (null, path) as FolderItem; - if (folder == null) { - return; - } - - folder.on_add_new (true); - } - - private void add_new_file (SimpleAction action, Variant? param) { - // Using "path" of parent folder from params, call `on_add_new (false)` on `FolderItem` - var path = param != null ? param.get_string () : null; - - if (path == null || path == "") { - critical ("No path"); - return; - } - - var folder = find_path (null, path) as FolderItem; - if (folder == null) { - return; - } - - folder.on_add_new (false); - } - - private void add_new_from_template (SimpleAction action, Variant? param) { - // Using "path" of parent folder from params, call `on_add_new (false)` on `FolderItem` - // var path = param.get_string (); - string? parent_path = null, template_path = null; - param.@get ("(ss)", out parent_path, out template_path); - - //Do we need this check? - if (parent_path == null || parent_path == "") { - return; - } - - var folder = find_path (null, parent_path) as FolderItem; - if (folder == null) { - return; - } - - folder.on_add_template (template_path); - } - - private void action_launch_app_with_file_path (SimpleAction action, Variant? param) { - var params = param.get_strv (); - Utils.launch_app_with_file (params[1], params[0]); - } - - private void action_show_app_chooser (SimpleAction action, Variant? param) { - var path = param.get_string (); - - if (path == null || path == "") { - return; - } - - var file = GLib.File.new_for_path (path); - var dialog = new Gtk.AppChooserDialog (new Gtk.Window (), Gtk.DialogFlags.MODAL, file); - dialog.deletable = false; - - dialog.response.connect ((res) => { - if (res == Gtk.ResponseType.OK) { - var app_info = dialog.get_app_info (); - if (app_info != null) { - Utils.launch_app_with_file (app_info.get_id (), path); - } - } - - dialog.destroy (); - }); - - dialog.show (); - } - - private void action_execute_contract_with_file_path (SimpleAction action, Variant? param) { - var params = param.get_strv (); - var path = params[0]; - if (path == null || path == "") { - return; - } - - var contract_name = params[1]; - if (contract_name == null || contract_name == "") { - return; - } - - Utils.execute_contract_with_file_path (path, contract_name); - } - - private void action_rename_file (SimpleAction action, Variant? param) { - var path = param.get_string (); - - if (path == null || path == "") { - return; - } - - rename_file (path); - } - - private void action_rename_folder (SimpleAction action, Variant? param) { - var path = param.get_string (); - - if (path == null || path == "") { - return; - } - - rename_folder (path); - } - - - private void action_delete (SimpleAction action, Variant? param) { - var path = param.get_string (); - - if (path == null || path == "") { - return; - } - - var item = find_path (null, path); - if (item != null) { - var item_to_delete = item as Scratch.FolderManager.Item; - - // Wait for ProjectFolderItem closed signal handle logic to run before moving item to trash - if (item_to_delete is Scratch.FolderManager.ProjectFolderItem) { - item_to_delete.closed.connect_after (() => { - item_to_delete.trash (); - }); - item_to_delete.closed (); - return; - } - - item_to_delete.trash (); - } - } - - private async void add_folder (File folder, bool expand, bool restoring = false) { - if (is_open (folder)) { - warning ("Folder '%s' is already open.", folder.path); - return; - } else if (!folder.is_valid_directory) { - warning ("Cannot open invalid directory."); - return; - } - - var add_file = folder.file; - // Need to deal with case where folder is parent or child of an existing project - var parents = new List (); - var children = new List (); - - iterate_children (null, (child) => { - var item = (ProjectFolderItem) child; - if (add_file.get_relative_path (item.file.file) != null) { - debug ("Trying to add parent of existing project"); - children.append (item); - } else if (item.file.file.get_relative_path (add_file) != null) { - debug ("Trying to add child of existing project"); - parents.append (item); - } - - return Code.TreeList.ITERATE_CONTINUE; - }); - - if (parents.length () > 0 || children.length () > 0) { - assert (parents.length () <= 1); - assert (parents.length () == 0 || children.length () == 0); - var dialog = new Scratch.Dialogs.CloseProjectsConfirmationDialog ( - (MainWindow) get_root (), - parents.length (), - children.length () - ); - - var close_projects = false; - dialog.response.connect ((res) => { - if (res == Gtk.ResponseType.ACCEPT) { - close_projects = true; - } - - dialog.destroy (); - add_folder.callback (); - }); - - dialog.show (); - yield; - - if (close_projects) { - foreach (var item in parents) { - item.closed (); - } - - foreach (var item in children) { - item.closed (); - } - } else { - return; - } - } - - // Process any closed signals emitted before proceeding - Idle.add (() => { - var folder_root = new ProjectFolderItem (folder, this); // Constructor adds project to GitManager - add_root_item (folder_root); - rename_items_with_same_name (folder_root); - - folder_root.is_expanded = expand; - folder_root.closed.connect (() => { - activate_action ( - MainWindow.ACTION_PREFIX + MainWindow.ACTION_CLOSE_PROJECT_DOCS, - "s", - folder_root.path - ); - - remove_root_item (folder_root); - - iterate_children (null, (child) => { - var child_folder = (ProjectFolderItem) child; - if (child_folder.name != child_folder.file.name) { - rename_items_with_same_name (child_folder); - } - - return Code.TreeList.ITERATE_CONTINUE; - }); - - Scratch.Services.GitManager.get_instance ().remove_project (folder_root); - write_settings (); - }); - - // We do not want to rewrite settings while restoring from settings - // This interferes with fuzzy-finder plugins_manager - // See https://github.com/elementary/code/issues/1533 - if (!restoring) { - write_settings (); - } - - add_folder.callback (); - return Source.REMOVE; - }); - - yield; - - order_folders (); - } - - private bool is_open (File folder) { - bool open = false; - iterate_children (null, (child) => { - if (folder.path == ((Item) child).path) { - open = true; - return Code.TreeList.ITERATE_STOP; - } - - return Code.TreeList.ITERATE_CONTINUE; - }); - - return open; - } - - private void write_settings () { - string[] to_save = {}; - iterate_children (null, (item) => { - var saved = false; - var folder_path = ((Item) item).path; - - //Do we need to de-duplicate? Not possible to open a project twice? - foreach (var saved_folder in to_save) { - if (folder_path == saved_folder) { - saved = true; - break; - } - } - - if (!saved) { - to_save += folder_path; - } - - return Code.TreeList.ITERATE_CONTINUE; - }); - - settings.set_strv ("opened-folders", to_save); - } -} diff --git a/src/FolderManager/FolderInterface.vala b/src/FolderManager/FolderInterface.vala new file mode 100644 index 0000000000..a7173aeedc --- /dev/null +++ b/src/FolderManager/FolderInterface.vala @@ -0,0 +1,156 @@ +// Folder interface shared with Project folder items used in ListBox +public interface Code.FolderInterface : Object { + public abstract bool is_expanded { get; set; } + public abstract ListStore? child_model { get; set; } + public abstract FileMonitor monitor { get; set; } + public virtual void on_changed ( + GLib.File source, + GLib.File? dest, + GLib.FileMonitorEvent event + ) {} + + public virtual void init_monitor_directory (GLib.File gfile) { + try { + monitor = gfile.monitor_directory (GLib.FileMonitorFlags.NONE); + monitor.changed.connect (on_changed); + } catch (GLib.Error e) { + warning (e.message); + } + } + + // Used by both ProjectListItem and FolderItem + public virtual GLib.MenuItem create_submenu_for_new (string file_path) { + var new_folder_item = new GLib.MenuItem ( + _("Folder"), + GLib.Action.print_detailed_name ( + ITEM_ACTION_PREFIX + ACTION_NEW_FOLDER, + new Variant.string (file_path) + ) + ); + + var new_file_item = new GLib.MenuItem ( + _("Empty File"), + GLib.Action.print_detailed_name ( + ITEM_ACTION_PREFIX + ACTION_NEW_FILE, + new Variant.string (file_path) + ) + ); + + var new_menu = new GLib.Menu (); + new_menu.append_item (new_folder_item); + new_menu.append_item (new_file_item); + + // Append any templates/template folders. + unowned string? template_path = GLib.Environment.get_user_special_dir (GLib.UserDirectory.TEMPLATES); + if (template_path != null) { + var template_submenu = new Menu (); + uint template_count = load_templates_from_folder ( + file_path, + GLib.File.new_for_path (template_path), + template_submenu + ); + if (template_count > 0) { + if (template_count > MAX_TEMPLATES) { + template_submenu.append_item (new MenuItem (_("…too many templates"), null)); + } + + new_menu.append_submenu (_("Templates"), template_submenu); + } + } + + var new_item = new GLib.MenuItem.submenu (_("Add New"), new_menu); + new_item.set_submenu (new_menu); + + return new_item; + } + + + public delegate bool IterateChildrenCallback (Object obj); + public virtual void iterate_children (IterateChildrenCallback cb) {} + + // Recursively load templates from folder and subfolders keeping count of total menuitems + const int MAX_TEMPLATES = 2048; + private uint load_templates_from_folder ( + string target_path, + GLib.File template_folder, + Menu template_submenu + ) { + GLib.List template_list = null; + GLib.List folder_list = null; + + GLib.FileEnumerator enumerator; + var flags = GLib.FileQueryInfoFlags.NOFOLLOW_SYMLINKS; + uint count = 0; + try { + enumerator = template_folder.enumerate_children ("standard::*", flags, null); + GLib.File location; + GLib.FileInfo? info = enumerator.next_file (null); + + while (count < MAX_TEMPLATES && (info != null)) { + if (!info.get_attribute_boolean (GLib.FileAttribute.STANDARD_IS_BACKUP)) { + location = template_folder.get_child (info.get_name ()); + if (info.get_file_type () == GLib.FileType.DIRECTORY) { + folder_list.prepend (location); + } else { + template_list.prepend (location); + } + + count ++; + } + + info = enumerator.next_file (null); + } + } catch (GLib.Error error) { + return 0; + } + + folder_list.sort ((a, b) => { + return strcmp (a.get_basename ().down (), b.get_basename ().down ()); + }); + + unowned List fl = folder_list; + while (fl != null && count < MAX_TEMPLATES) { + var folder = fl.data; + var folder_submenu = new Menu (); + var folder_submenuitem = new MenuItem.submenu ( + folder.get_basename (), + folder_submenu + ); + + var sub_count = load_templates_from_folder (target_path, folder, folder_submenu); + if (sub_count > 0) { + template_submenu.append_item (folder_submenuitem); + count += sub_count; + } else { + count -= 1; // Adjust count for ignored folder + } + + fl = fl.next; + } + + if (count > MAX_TEMPLATES) { + warning ("too many templates! %u", count); + return count; + } + + template_list.sort ((a, b) => { + return strcmp (a.get_basename ().down (), b.get_basename ().down ()); + }); + + template_list.@foreach ((template) => { + var template_menuitem = new MenuItem ( + template.get_basename (), + GLib.Action.print_detailed_name ( + ITEM_ACTION_PREFIX + ACTION_NEW_FROM_TEMPLATE, + new Variant ("(ss)", target_path, template.get_path ()) + ) + ); + + template_submenu.append_item (template_menuitem); + }); + + return count; + } +} + + diff --git a/src/FolderManager/FolderItem.vala b/src/FolderManager/FolderItem.vala index bcd0e81fd2..bb595f4e13 100644 --- a/src/FolderManager/FolderItem.vala +++ b/src/FolderManager/FolderItem.vala @@ -18,644 +18,457 @@ * Authored by: Julien Spautz , Andrei-Costin Zisu */ -namespace Scratch.FolderManager { - /** - * Expandable item in the source list, represents a folder. - * Monitored for changes inside the directory. - */ - public class FolderItem : Item { - private const uint RENAME_AFTER_NEW_DELAY_MSEC = 500; - private GLib.FileMonitor monitor; - private bool children_loaded = false; - private bool has_dummy; - private Code.TreeListItem dummy; /* Blank item for expanded empty folders */ - - public bool loading_required { - get { - return !children_loaded && file.children.size > 0; - } - } - - public signal void children_finished_loading (); +public class Code.FolderItem : FolderManagerItem, Code.FolderInterface, Code.FolderManagerItemInterface { // Ultimately a Granite.TreeListItem + public signal void children_finished_loading (); - public FolderItem (File file, FileView view) { - Object ( - file: file, - view: view - ); + public bool loading_required { + get { + return !children_loaded && file.children.size > 0; } + } - ~FolderItem () { - monitor.cancel (); - } + // Code.FolderInterface + protected FileMonitor monitor { get; set; } - construct { - is_selectable = false; - is_expandable = true; + private const uint RENAME_AFTER_NEW_DELAY_MSEC = 500; + private bool children_loaded = false; + private bool has_dummy; - dummy = new Code.TreeListItem.dummy (); - // Must add dummy on unexpanded folders else expander will not show - add_child (dummy); - has_dummy = true; + public FolderItem (File file, FolderTree view) { + Object ( + file: file, + view: view, + is_expandable: true + ); + } - // Signal when expanded - notify["is-expanded"].connect (on_toggled); + ~FolderItem () { + monitor.cancel (); + } - try { - monitor = file.file.monitor_directory (GLib.FileMonitorFlags.NONE); - monitor.changed.connect (on_changed); - } catch (GLib.Error e) { - warning (e.message); - } - } + construct { + is_selectable = false; - private void create_child_items (File file) { - foreach (var child_file in file.children) { - if (child_file.is_valid_directory) { - add_child (new FolderItem (child_file, view)); - } else if (child_file.is_valid_textfile) { - add_child (new FileItem (child_file, view)); - } - } - } + // dummy = new Granite.TreeListItem.dummy (); + // // Must add dummy on unexpanded folders else expander will not show + // add_child (dummy); + // has_dummy = true; - public void load_children () { - if (loading_required) { - create_child_items (file); - after_children_loaded (); - } - } + init_monitor_directory (file.file); + + // Signal when expanded + notify["is-expanded"].connect (on_toggled); + } - private async void load_children_async () { - if (loading_required) { - // Do we need to yield after each child with dynamic view?? - Idle.add (() => { - create_child_items (file); - load_children_async.callback (); - return Source.REMOVE; - }); + public override void iterate_children (IterateChildrenCallback cb) { + uint pos = 0; + Object? child = null; + do { + child = child_model.get_object (pos++); + } while (child != null && cb (child)); + } - yield; + private void create_child_items (File file) { + foreach (var child_file in file.children) { + if (child_file.is_valid_directory) { + add_child (new FolderItem (child_file, view)); + } else if (child_file.is_valid_textfile) { + add_child (new FileItem (child_file, view)); } + } + } + public void load_children () { + if (loading_required) { + create_child_items (file); after_children_loaded (); } + } - private void add_file (File child) { - Code.TreeListItem item = null; - if (child.is_valid_directory) { - item = new FolderItem (child, view); - } else if (child.is_valid_textfile) { - item = new FileItem (child, view); - } + private async void load_children_async () { + if (loading_required) { + // Do we need to yield after each child with dynamic view?? + Idle.add (() => { + create_child_items (file); + load_children_async.callback (); + return Source.REMOVE; + }); - if (item != null) { - add_child (item); - } + yield; } - private void after_children_loaded () { - children_loaded = true; - var root = get_root_folder (); - if (root != null) { - root.after_child_folder_loaded (this); //Updates child status emblens - } + after_children_loaded (); + } - children_finished_loading (); + private void add_file (File child) { + Granite.TreeListItem item = null; + if (child.is_valid_directory) { + item = new FolderItem (child, view); + } else if (child.is_valid_textfile) { + item = new FileItem (child, view); } - private void on_toggled () { - if (is_expanded) { - load_children_async.begin (); - } else { - var root = get_root_folder (); - if (root != null && - root.monitored_repo != null) { - //When toggled closed, update status to reflect hidden contents - root.update_item_status (this); - } - } + if (item != null) { + add_child (item); } + } - public override GLib.Menu? get_context_menu () { - GLib.FileInfo info = null; - try { - info = file.file.query_info (GLib.FileAttribute.STANDARD_CONTENT_TYPE, GLib.FileQueryInfoFlags.NONE); - } catch (Error e) { - warning (e.message); - } - - var file_type = info.get_content_type (); - - var contractor_items = Utils.create_contract_items_for_file (file.file); - - var rename_menu_item = new GLib.MenuItem ( - _("Rename"), - GLib.Action.print_detailed_name ( - FileView.ACTION_PREFIX + FileView.ACTION_RENAME_FOLDER, - new Variant.string (file.path) - ) - ); - - var delete_item = new GLib.MenuItem ( - _("Move to Trash"), - GLib.Action.print_detailed_name ( - FileView.ACTION_PREFIX + FileView.ACTION_DELETE, - new Variant.string (file.path) - ) - ); - - var search_item = new GLib.MenuItem ( - _("Find in Folder…"), - GLib.Action.print_detailed_name ( - MainWindow.ACTION_PREFIX + MainWindow.ACTION_FIND_GLOBAL, - new Variant.string (file.file.get_path ()) - ) - ); - - var external_actions_section = new GLib.Menu (); - external_actions_section.append_item (create_submenu_for_open_in (file_type)); - if (contractor_items.get_n_items () > 0) { - external_actions_section.append_submenu (_("Other Actions"), contractor_items); - } - - var direct_actions_section = new GLib.Menu (); - direct_actions_section.append_item (create_submenu_for_new ()); - direct_actions_section.append_item (rename_menu_item); - direct_actions_section.append_item (delete_item); - - var search_section = new GLib.Menu (); - search_section.append_item (search_item); + private void after_children_loaded () { + children_loaded = true; + // var root = get_root_folder (); + // if (root != null) { + // root.after_child_folder_loaded (this); //Updates child status emblens + // } - var menu_model = new GLib.Menu (); - menu_model.append_section (null, external_actions_section); - menu_model.append_section (null, direct_actions_section); - menu_model.append_section (null, search_section); + children_finished_loading (); + } - return menu_model; + private void on_toggled () { + if (is_expanded) { + load_children_async.begin (); } + else { + update_item_status (); + } + } - protected GLib.MenuItem create_submenu_for_open_in (string? file_type) { - var open_in_terminal_pane_item = new GLib.MenuItem ( - (_("Terminal Pane")), - GLib.Action.print_detailed_name ( - MainWindow.ACTION_PREFIX + MainWindow.ACTION_OPEN_IN_TERMINAL, - new Variant.string (file.path) - ) - ); - open_in_terminal_pane_item.set_icon (new ThemedIcon ("panel-bottom-symbolic")); - - var other_menu_item = new GLib.MenuItem ( - _("Other Application…"), - GLib.Action.print_detailed_name ( - FileView.ACTION_PREFIX + FileView.ACTION_SHOW_APP_CHOOSER, - file.path - ) - ); - - var extra_section = new GLib.Menu (); - extra_section.append_item (other_menu_item); - - var terminal_pane_section = new Menu (); - terminal_pane_section.append_item (open_in_terminal_pane_item); + private void update_item_status () { + //TODO Update status icon according to hidden files + } - file_type = file_type ?? "inode/directory"; + public override GLib.Menu? get_context_menu () { + warning ("Folder item get context menu"); + var menu_model = base.get_context_menu (); + + // Add items related only to folders + var search_item = new GLib.MenuItem ( + _("Find in Folder…"), + GLib.Action.print_detailed_name ( + Scratch.MainWindow.ACTION_PREFIX + Scratch.MainWindow.ACTION_FIND_GLOBAL, + new Variant.string (file.file.get_path ()) + ) + ); + var search_section = new GLib.Menu (); + search_section.append_item (search_item); + menu_model.append_section (null, search_section); + // var external_actions_section = new GLib.Menu (); + // external_actions_section.append_item (create_submenu_for_open_item_in (file.path, file_type)); + + var direct_actions_section = new GLib.Menu (); + direct_actions_section.append_item (create_submenu_for_new (file.path)); + menu_model.prepend_section (null, direct_actions_section); + // direct_actions_section.append_item (rename_menu_item); + // direct_actions_section.append_item (delete_item); + + return menu_model; + } - var open_in_menu = new GLib.Menu (); - open_in_menu.append_section (null, terminal_pane_section); - open_in_menu.append_section (null, Utils.create_executable_app_items_for_file (file.file, file_type)); - open_in_menu.append_section (null, extra_section); - var open_in_menu_item = new GLib.MenuItem.submenu (_("Open In"), open_in_menu); - return open_in_menu_item; + // public void remove_all_badges () { + // view.iterate_children (this, (child) => { + // remove_badge (child); + // return Granite.TreeList.ITERATE_CONTINUE; + // }); + // } + + // private void remove_badge (Granite.TreeListItem item) { + + // if (item is FolderItem) { + // ((FolderItem) item).remove_all_badges (); + // } + + // item.badge = ""; + // } + + // public override void add_child (Granite.TreeListItem item) { + // if (has_dummy) { + // base.remove_child (dummy); + // has_dummy = false; + // } + + // base.add_child (item); + // } + + // // Only for removing "real" items; dummy is added/removed directly + // public override void remove_child (Granite.TreeListItem item) requires (has_dummy == false) { + // if (item is FolderItem) { + // var folder = (FolderItem) item; + // folder.remove_all_children (); + // } + + // base.remove_child (item); + + // // Add back dummy if empty + // if (has_no_children ()) { + // base.add_child (dummy); + // has_dummy = true; + // } + // } + + // public new void clear () { + // ((Code.Widgets.SourceList.ExpandableItem)this).clear (); + // has_dummy = false; + // } + + protected virtual void on_changed ( + GLib.File source, + GLib.File? dest, + GLib.FileMonitorEvent event + ) { + if (source.get_basename ().has_prefix (".goutputstream")) { + return; // Ignore changes due to temp files and streams } - protected GLib.MenuItem create_submenu_for_new () { - var new_folder_item = new GLib.MenuItem ( - _("Folder"), - GLib.Action.print_detailed_name ( - FileView.ACTION_PREFIX + FileView.ACTION_NEW_FOLDER, - new Variant.string (file.path) - ) - ); + view.folder_item_update_hook (source, dest, event); - var new_file_item = new GLib.MenuItem ( - _("Empty File"), - GLib.Action.print_detailed_name ( - FileView.ACTION_PREFIX + FileView.ACTION_NEW_FILE, - new Variant.string (file.path) - ) - ); - - var new_menu = new GLib.Menu (); - new_menu.append_item (new_folder_item); - new_menu.append_item (new_file_item); - - //Append any templates/template folders. - unowned string? template_path = GLib.Environment.get_user_special_dir (GLib.UserDirectory.TEMPLATES); - if (template_path != null) { - var template_submenu = new Menu (); - uint template_count = load_templates_from_folder (GLib.File.new_for_path (template_path), template_submenu); - if (template_count > 0) { - if (template_count > MAX_TEMPLATES) { - template_submenu.append_item (new MenuItem (_("…too many templates"), null)); + if (!children_loaded) { // No child items except dummy, child never expanded + /* Empty folder with dummy item will come here even if expanded */ + switch (event) { + case GLib.FileMonitorEvent.DELETED: + file.invalidate_cache (); //TODO Throttle if required + if (is_expanded) { + // Force refresh of child model? + notify_property ("is-expanded"); } - - new_menu.append_submenu (_("Templates"), template_submenu); - } + break; + case GLib.FileMonitorEvent.CREATED: + file.invalidate_cache (); //TODO Throttle if required + if (is_expanded) { + notify_property ("is-expanded"); + } + break; + case FileMonitorEvent.RENAMED: + case FileMonitorEvent.PRE_UNMOUNT: + case FileMonitorEvent.UNMOUNTED: + case FileMonitorEvent.CHANGED: + case FileMonitorEvent.CHANGES_DONE_HINT: + case FileMonitorEvent.MOVED: + case FileMonitorEvent.MOVED_IN: + case FileMonitorEvent.MOVED_OUT: + case FileMonitorEvent.ATTRIBUTE_CHANGED: + + break; } + } else { + // Child has been expanded ( but could be closed now) and + // items loaded (or dummy). + // No cache invalidation is needed here because the entire state + // is kept in the tree + //TODO Test for TreeList + switch (event) { + case GLib.FileMonitorEvent.DELETED: + // Find item corresponding to deleted file + // Note may not be found if deleted file is not valid for display + var path_item = get_child_for_path (source.get_path ()); + if (path_item != null) { + remove_child (path_item); + } - var new_item = new GLib.MenuItem.submenu (_("New"), new_menu); - new_item.set_submenu (new_menu); - - return new_item; - } + break; + case GLib.FileMonitorEvent.CREATED: + if (source.query_exists () == false) { + return; + } - // Recursively load templates from folder and subfolders keeping count of total menuitems - const int MAX_TEMPLATES = 2048; - private uint load_templates_from_folder ( - GLib.File template_folder, - Menu template_submenu - ) { - GLib.List template_list = null; - GLib.List folder_list = null; - - GLib.FileEnumerator enumerator; - var flags = GLib.FileQueryInfoFlags.NOFOLLOW_SYMLINKS; - uint count = 0; - try { - enumerator = template_folder.enumerate_children ("standard::*", flags, null); - GLib.File location; - GLib.FileInfo? info = enumerator.next_file (null); - - while (count < MAX_TEMPLATES && (info != null)) { - if (!info.get_attribute_boolean (GLib.FileAttribute.STANDARD_IS_BACKUP)) { - location = template_folder.get_child (info.get_name ()); - if (info.get_file_type () == GLib.FileType.DIRECTORY) { - folder_list.prepend (location); - } else { - template_list.prepend (location); + var path_item = get_child_for_path (source.get_path ()); + if (path_item == null) { + var file = new File (source.get_path ()); + if (file.is_valid_directory) { + path_item = new FolderItem (file, view); + } else if (file.is_valid_textfile) { + path_item = new FileItem (file, view); } - count ++; + add_child (path_item); // null parameter is silently ignored } - info = enumerator.next_file (null); - } - } catch (GLib.Error error) { - return 0; + break; + case FileMonitorEvent.RENAMED: + case FileMonitorEvent.PRE_UNMOUNT: + case FileMonitorEvent.UNMOUNTED: + case FileMonitorEvent.CHANGED: + case FileMonitorEvent.CHANGES_DONE_HINT: + case FileMonitorEvent.MOVED: + case FileMonitorEvent.MOVED_IN: + case FileMonitorEvent.MOVED_OUT: + case FileMonitorEvent.ATTRIBUTE_CHANGED: + break; } + } - folder_list.sort ((a, b) => { - return strcmp (a.get_basename ().down (), b.get_basename ().down ()); - }); - - unowned List fl = folder_list; - while (fl != null && count < MAX_TEMPLATES) { - var folder = fl.data; - var folder_submenu = new Menu (); - var folder_submenuitem = new MenuItem.submenu ( - folder.get_basename (), - folder_submenu - ); - - var sub_count = load_templates_from_folder (folder, folder_submenu); - if (sub_count > 0) { - template_submenu.append_item (folder_submenuitem); - count += sub_count; - } else { - count -= 1; // Adjust count for ignored folder - } - - fl = fl.next; + // Reduce spamming of root (still results in multiple signals per change in file being edited + //TODO Throttle this signal? + if (event == FileMonitorEvent.CHANGES_DONE_HINT) { + //TODO Get root folder once as it will not change for the life of this folder + var root = get_root_folder (this); + if (root != null) { + root.child_folder_changed (this); } + } + } - if (count > MAX_TEMPLATES) { - warning ("too many templates! %u", count); - return count; + // Called on MonitorEvent so only concerns direct children + private Code.FolderManagerItem? get_child_for_path (string path) { + Code.FolderManagerItem? found_item = null; + iterate_children ((obj) => { + // Item could be dummy + if (obj is Code.FolderManagerItem) { + var child = (Code.FolderManagerItem) obj; + if (child.path == path) { + found_item = child; + return Granite.TreeList.ITERATE_STOP; + } } - template_list.sort ((a, b) => { - return strcmp (a.get_basename ().down (), b.get_basename ().down ()); - }); + return Granite.TreeList.ITERATE_CONTINUE; + }); - template_list.@foreach ((template) => { - var template_menuitem = new MenuItem ( - template.get_basename (), - GLib.Action.print_detailed_name ( - FileView.ACTION_PREFIX + FileView.ACTION_NEW_FROM_TEMPLATE, - new Variant ("(ss)", this.path, template.get_path ()) - ) - ); - - template_submenu.append_item (template_menuitem); - }); - - return count; - } + return found_item; + } - public void remove_all_badges () { - view.iterate_children (this, (child) => { - remove_badge (child); - return Code.TreeList.ITERATE_CONTINUE; + public void on_add_template (string template_path) { + warning ("add template"); + // Expand folder before trying to copy template so that child appears for renaming + if (!is_expanded) { + is_expanded = true; // causes async loading of children + ulong once = 0; + once = children_finished_loading.connect (() => { + this.disconnect (once); + copy_template (template_path); }); + } else { + copy_template (template_path); } + } - private void remove_badge (Code.TreeListItem item) { - - if (item is FolderItem) { - ((FolderItem) item).remove_all_badges (); - } - - item.badge = ""; - } - - public override void add_child (Code.TreeListItem item) { - if (has_dummy) { - base.remove_child (dummy); - has_dummy = false; - } - - base.add_child (item); + private void copy_template (string template_path) { + if (!file.is_executable) { + // This is necessary to avoid infinite loop below + warning ("Unable to open parent folder"); + return; } - // Only for removing "real" items; dummy is added/removed directly - public override void remove_child (Code.TreeListItem item) requires (has_dummy == false) { - if (item is FolderItem) { - var folder = (FolderItem) item; - folder.remove_all_children (); - } - - base.remove_child (item); - - // Add back dummy if empty - if (has_no_children ()) { - base.add_child (dummy); - has_dummy = true; - } + var template_file = GLib.File.new_for_path (template_path); + name = template_file.get_basename (); + var new_file = file.file.get_child (name); + var n = 1; + while (new_file.query_exists ()) { + new_file = file.file.get_child (("%s %d").printf (name, n)); + n++; } - // public new void clear () { - // ((Code.Widgets.SourceList.ExpandableItem)this).clear (); - // has_dummy = false; - // } - - protected virtual void on_changed ( - GLib.File source, - GLib.File? dest, - GLib.FileMonitorEvent event - ) { - if (source.get_basename ().has_prefix (".goutputstream")) { - return; // Ignore changes due to temp files and streams - } - - view.folder_item_update_hook (source, dest, event); - - if (!children_loaded) { // No child items except dummy, child never expanded - /* Empty folder with dummy item will come here even if expanded */ - switch (event) { - case GLib.FileMonitorEvent.DELETED: - file.invalidate_cache (); //TODO Throttle if required - if (is_expanded) { - // Force refresh of child model? - notify_property ("is-expanded"); - } - break; - case GLib.FileMonitorEvent.CREATED: - file.invalidate_cache (); //TODO Throttle if required - if (is_expanded) { - notify_property ("is-expanded"); - } - break; - case FileMonitorEvent.RENAMED: - case FileMonitorEvent.PRE_UNMOUNT: - case FileMonitorEvent.UNMOUNTED: - case FileMonitorEvent.CHANGED: - case FileMonitorEvent.CHANGES_DONE_HINT: - case FileMonitorEvent.MOVED: - case FileMonitorEvent.MOVED_IN: - case FileMonitorEvent.MOVED_OUT: - case FileMonitorEvent.ATTRIBUTE_CHANGED: - - break; - } - } else { - // Child has been expanded ( but could be closed now) and - // items loaded (or dummy). - // No cache invalidation is needed here because the entire state - // is kept in the tree - //TODO Test for TreeList - switch (event) { - case GLib.FileMonitorEvent.DELETED: - // Find item corresponding to deleted file - // Note may not be found if deleted file is not valid for display - var path_item = find_item_for_path (source.get_path ()); - if (path_item != null) { - remove_child (path_item); - } - - break; - case GLib.FileMonitorEvent.CREATED: - if (source.query_exists () == false) { - return; - } - - var path_item = find_item_for_path (source.get_path ()); - if (path_item == null) { - var file = new File (source.get_path ()); - if (file.is_valid_directory) { - path_item = new FolderItem (file, view); - } else if (file.is_valid_textfile) { - path_item = new FileItem (file, view); - } - - add_child (path_item); // null parameter is silently ignored - } + name = new_file.get_basename (); - break; - case FileMonitorEvent.RENAMED: - case FileMonitorEvent.PRE_UNMOUNT: - case FileMonitorEvent.UNMOUNTED: - case FileMonitorEvent.CHANGED: - case FileMonitorEvent.CHANGES_DONE_HINT: - case FileMonitorEvent.MOVED: - case FileMonitorEvent.MOVED_IN: - case FileMonitorEvent.MOVED_OUT: - case FileMonitorEvent.ATTRIBUTE_CHANGED: - break; - } - } - - // Reduce spamming of root (still results in multiple signals per change in file being edited - //TODO Throttle this signal? - if (event == FileMonitorEvent.CHANGES_DONE_HINT) { - //TODO Get root folder once as it will not change for the life of this folder - var root = get_root_folder (this); - if (root != null) { - root.child_folder_changed (this); - } - } + //Assume templates are small and can be copied without problems + try { + template_file.copy ( + new_file, + TARGET_DEFAULT_MODIFIED_TIME | TARGET_DEFAULT_PERMS | NOFOLLOW_SYMLINKS, + null, //No cancellable + null //No progress + ); + } catch (Error e) { + warning ("Error copying template %s", e.message); + return; } - private FolderManager.Item? find_item_for_path (string path) { - FolderManager.Item? found_item = null; - view.iterate_children (this, (item) => { - // Item could be dummy - if (item is FolderManager.Item) { - var child = (FolderManager.Item) item; - if (child.path == path) { - found_item = child; - return Code.TreeList.ITERATE_STOP; - } + // Wait for monitor to pickup file creation and add new item + ulong once = 0; + once = child_added.connect (() => { + this.disconnect (once); + var path = new_file.get_path (); + // Still need to wait for sourcelist to become stable and editable + //TODO Find a better way + Timeout.add (RENAME_AFTER_NEW_DELAY_MSEC, () => { + var rename_action = Scratch.Utils.action_from_group (ACTION_RENAME_FILE, view.actions); + if (rename_action != null && rename_action.enabled) { + rename_action.activate (path); + } else { + critical ("Rename action not available"); } - return Code.TreeList.ITERATE_CONTINUE; + return Source.REMOVE; }); + }); + } - return found_item; + public void on_add_new (bool is_folder) { + warning ("add new"); + if (!file.is_executable) { + // This is necessary to avoid infinite loop below + warning ("Unable to open parent folder"); + return; } - public void on_add_template (string template_path) { - // Expand folder before trying to copy template so that child appears for renaming - if (!is_expanded) { - is_expanded = true; // causes async loading of children - ulong once = 0; - once = children_finished_loading.connect (() => { - this.disconnect (once); - copy_template (template_path); - }); - } else { - copy_template (template_path); - } - } - - private void copy_template (string template_path) { - if (!file.is_executable) { - // This is necessary to avoid infinite loop below - warning ("Unable to open parent folder"); - return; - } - var template_file = GLib.File.new_for_path (template_path); - name = template_file.get_basename (); - var new_file = file.file.get_child (name); - var n = 1; - while (new_file.query_exists ()) { - new_file = file.file.get_child (("%s %d").printf (name, n)); - n++; - } + var name = is_folder ? _("untitled folder") : _("new file"); + var new_file = file.file.get_child (name); + var n = 1; + while (new_file.query_exists ()) { + new_file = file.file.get_child (("%s %d").printf (name, n)); + n++; + } - name = new_file.get_basename (); - - //Assume templates are small and can be copied without problems - try { - template_file.copy ( - new_file, - TARGET_DEFAULT_MODIFIED_TIME | TARGET_DEFAULT_PERMS | NOFOLLOW_SYMLINKS, - null, //No cancellable - null //No progress - ); - } catch (Error e) { - warning ("Error copying template %s", e.message); - return; - } + name = new_file.get_basename (); - // Wait for monitor to pickup file creation and add new item + // Expand folder before trying to rename + if (!is_expanded) { ulong once = 0; - once = child_added.connect (() => { + once = children_finished_loading.connect (() => { this.disconnect (once); - var path = new_file.get_path (); - // Still need to wait for sourcelist to become stable and editable - //TODO Find a better way - Timeout.add (RENAME_AFTER_NEW_DELAY_MSEC, () => { - var rename_action = Utils.action_from_group (FileView.ACTION_RENAME_FILE, view.actions); - if (rename_action != null && rename_action.enabled) { - rename_action.activate (path); - } else { - critical ("Rename action not available"); - } - - return Source.REMOVE; - }); + rename_new (name, is_folder); }); - } - - public void on_add_new (bool is_folder) { - if (!file.is_executable) { - // This is necessary to avoid infinite loop below - warning ("Unable to open parent folder"); - return; - } - - var name = is_folder ? _("untitled folder") : _("new file"); - var new_file = file.file.get_child (name); - var n = 1; - while (new_file.query_exists ()) { - new_file = file.file.get_child (("%s %d").printf (name, n)); - n++; - } - - name = new_file.get_basename (); - - // Expand folder before trying to rename - if (!is_expanded) { - ulong once = 0; - once = children_finished_loading.connect (() => { - this.disconnect (once); - rename_new (name, is_folder); - }); - - is_expanded = true; // causes async loading of children - } else { - rename_new (name, is_folder); - } + is_expanded = true; // causes async loading of children + } else { + rename_new (name, is_folder); } + } - private void rename_new (string name, bool is_folder) { - // assert (!view.editing); - // var rename_item = new RenameItem (name, is_folder); - // add_child (rename_item); - - // // Wait until can start editing - // // For some reason using an Idle does not work properly here - the editable gets drawn in the wrong place - // //TODO Find a way to detect when the sourcelist is stable and can be edited - // Timeout.add (RENAME_AFTER_NEW_DELAY_MSEC, () => { - // if (view.start_editing_item (rename_item)) { - // ulong once = 0; - // once = rename_item.edited.connect (() => { - // rename_item.disconnect (once); - // // A name was accepted so create the corresponding file - // var new_name = rename_item.name; - // try { - // var gfile = file.file.get_child_for_display_name (new_name); - // if (rename_item.is_folder) { - // gfile.make_directory (); - // } else { - // gfile.create (FileCreateFlags.NONE); - // view.activate (gfile.get_path ()); - // } - // } catch (Error e) { - // warning (e.message); - // } - // }); - - // /* Need to remove rename item even when editing cancelled so cannot use "edited" signal */ - // Timeout.add (200, () => { - // if (view.editing) { - // return Source.CONTINUE; - // } else { - // remove_child (rename_item); - // return Source.REMOVE; - // } - // }); - // } else { - // critical ("Failed to rename new item"); - // remove_child (rename_item); - // } - - // return Source.REMOVE; - // }); - } + private void rename_new (string name, bool is_folder) { + // assert (!view.editing); + // var rename_item = new RenameItem (name, is_folder); + // add_child (rename_item); + + // // Wait until can start editing + // // For some reason using an Idle does not work properly here - the editable gets drawn in the wrong place + // //TODO Find a way to detect when the sourcelist is stable and can be edited + // Timeout.add (RENAME_AFTER_NEW_DELAY_MSEC, () => { + // if (view.start_editing_item (rename_item)) { + // ulong once = 0; + // once = rename_item.edited.connect (() => { + // rename_item.disconnect (once); + // // A name was accepted so create the corresponding file + // var new_name = rename_item.name; + // try { + // var gfile = file.file.get_child_for_display_name (new_name); + // if (rename_item.is_folder) { + // gfile.make_directory (); + // } else { + // gfile.create (FileCreateFlags.NONE); + // view.activate (gfile.get_path ()); + // } + // } catch (Error e) { + // warning (e.message); + // } + // }); + + // /* Need to remove rename item even when editing cancelled so cannot use "edited" signal */ + // Timeout.add (200, () => { + // if (view.editing) { + // return Source.CONTINUE; + // } else { + // remove_child (rename_item); + // return Source.REMOVE; + // } + // }); + // } else { + // critical ("Failed to rename new item"); + // remove_child (rename_item); + // } + + // return Source.REMOVE; + // }); } } + diff --git a/src/FolderManager/FolderManagerItemInterface.vala b/src/FolderManager/FolderManagerItemInterface.vala new file mode 100644 index 0000000000..27dbc52f4a --- /dev/null +++ b/src/FolderManager/FolderManagerItemInterface.vala @@ -0,0 +1,173 @@ + +public interface Code.FolderManagerItemInterface : Object { + public abstract string path { get; set; } + public abstract string name { get; set; } + public abstract string badge { get; set; } + public bool equal (FolderManagerItemInterface b) { + return path == b.path; + } + + public virtual GLib.Menu? get_context_menu () { + GLib.FileInfo info = null; + var gfile = GLib.File.new_for_path (path); + try { + info = gfile.query_info (GLib.FileAttribute.STANDARD_CONTENT_TYPE, GLib.FileQueryInfoFlags.NONE); + } catch (Error e) { + warning (e.message); + } + + var file_type = info.get_content_type (); + + // external_actions_section holds submenus for "Open In" and "Other Actions" + // which involve other apps + var external_actions_section = new GLib.Menu (); + external_actions_section.append_item (create_submenu_for_open_item_in (gfile, file_type)); + var contractor_items = create_contract_items_for_file (gfile); + if (contractor_items.get_n_items () > 0) { + external_actions_section.append_submenu (_("Other Actions"), contractor_items); + } + + var menu_model = new GLib.Menu (); + menu_model.append_section (null, external_actions_section); + var rename_menu_item = new GLib.MenuItem ( + _("Rename"), + GLib.Action.print_detailed_name ( + ITEM_ACTION_PREFIX + ACTION_RENAME_FOLDER, + new Variant.string (path) + ) + ); + + var delete_item = new GLib.MenuItem ( + _("Move to Trash"), + GLib.Action.print_detailed_name ( + ITEM_ACTION_PREFIX + ACTION_DELETE, + new Variant.string (path) + ) + ); + + return menu_model; + } + + public virtual GLib.MenuItem create_submenu_for_open_item_in (GLib.File gfile, string? file_type) { + //TODO Open in Terminal Pane only for projects + // var open_in_terminal_pane_item = new GLib.MenuItem ( + // (_("Terminal Pane")), + // GLib.Action.print_detailed_name ( + // Scratch.MainWindow.ACTION_PREFIX + Scratch.MainWindow.ACTION_OPEN_IN_TERMINAL, + // new Variant.string (file_path) + // ) + // ); + // open_in_terminal_pane_item.set_icon (new ThemedIcon ("panel-bottom-symbolic")); + + var file_path = gfile.get_path (); + var other_menu_item = new GLib.MenuItem ( + _("Other Application…"), + GLib.Action.print_detailed_name ( + ITEM_ACTION_PREFIX + ACTION_SHOW_APP_CHOOSER, + file_path + ) + ); + + var extra_section = new GLib.Menu (); + extra_section.append_item (other_menu_item); + + // var terminal_pane_section = new Menu (); + // terminal_pane_section.append_item (open_in_terminal_pane_item); + + file_type = file_type ?? "inode/directory"; + + var open_in_menu = new GLib.Menu (); + // open_in_menu.append_section (null, terminal_pane_section); + open_in_menu.append_section (null, create_executable_app_items_for_file ( + gfile, + file_type + )); + open_in_menu.append_section (null, extra_section); + + var open_in_menu_item = new GLib.MenuItem.submenu (_("Open In"), open_in_menu); + open_in_menu_item.set_link ("submenu", open_in_menu); // So overriding methods can modify + return open_in_menu_item; + } + + public GLib.Menu? create_executable_app_items_for_file (GLib.File file, string file_type) { + var scratch_app = (Scratch.Application) (GLib.Application.get_default ()); + var this_id = scratch_app.application_id + ".desktop"; + var menu = new GLib.Menu (); + + if (scratch_app.is_running_in_flatpak) { + var menu_item = new MenuItem ( + ///TRANSLATORS '%s' represents the quoted basename of a uri to be opened with the default app + _("Show '%s' with default app").printf (file.get_basename ()), + GLib.Action.print_detailed_name ( + ITEM_ACTION_PREFIX + ACTION_LAUNCH_APP_WITH_FILE_PATH, + new GLib.Variant.array ( + GLib.VariantType.STRING, + { file.get_path (), "" } + ) + ) + ); + menu.append_item (menu_item); + } else { + List external_apps = null; + if (file_type == "") { + var files_appinfo = AppInfo.get_default_for_type ("inode/directory", true); + external_apps.prepend (files_appinfo); + } else { + external_apps = GLib.AppInfo.get_all_for_type (file_type); + external_apps.sort ((a, b) => { + return a.get_name ().collate (b.get_name ()); + }); + } + + foreach (AppInfo app_info in external_apps) { + string app_id = app_info.get_id (); + if (app_id == this_id) { + continue; + } + + var menu_item = new MenuItem ( + app_info.get_name (), + GLib.Action.print_detailed_name ( + ITEM_ACTION_PREFIX + ACTION_LAUNCH_APP_WITH_FILE_PATH, + new GLib.Variant.array ( + GLib.VariantType.STRING, + { file.get_path (), app_id } + ) + ) + ); + menu_item.set_icon (app_info.get_icon ()); + menu.append_item (menu_item); + } + } + + return menu; + } + + public GLib.Menu create_contract_items_for_file (GLib.File file) { + var menu = new GLib.Menu (); + + try { + var contracts = Granite.Services.ContractorProxy.get_contracts_for_file (file); + foreach (var contract in contracts) { + string contract_name = contract.get_display_name (); + var menu_item = new GLib.MenuItem ( + contract_name, + GLib.Action.print_detailed_name ( + ITEM_ACTION_PREFIX + ACTION_EXECUTE_CONTRACT_WITH_FILE_PATH, + new GLib.Variant.array ( + GLib.VariantType.STRING, + { file.get_path (), contract_name } + ) + ) + ); + + menu.append_item (menu_item); + } + } catch (Error e) { + warning (e.message); + } + + return menu; + } +} + diff --git a/src/FolderManager/FolderTree.vala b/src/FolderManager/FolderTree.vala new file mode 100644 index 0000000000..4a90ee23fc --- /dev/null +++ b/src/FolderManager/FolderTree.vala @@ -0,0 +1,771 @@ +/* + * SPDX-License-Identifier: GPL-3.0-or-later + * SPDX-FileCopyrightText: 2026 elementary, Inc. + */ + +/* FolderTree shows a single folder and its children as a tree */ +public class Code.FolderTreeItem : Granite.TreeListItem { + public FolderManagerItem? item { get; construct; } //Either File or Folder Item + + public FolderTreeItem (FolderManagerItem item) { + Object ( item: item ); + } + + construct { + item.file.bind_property ("name", this, text); + } +} + +public class Code.FolderTree : Gtk.Box, Code.PaneSwitcher { + public signal void file_activate (File file); + + private static string ADDED_ICON_NAME = "emblem-git-new-symbolic"; + private static string MODIFIED_ICON_NAME = "emblem-git-modified-symbolic"; + + //PaneSwitcher properties (not currently used) + public string icon_name { get; set; } + public string title { get; set; } + + public SimpleActionGroup actions { get; private set; } + public string root_path { get { return project.path; } } + public bool is_empty { get { return tree_list.n_root_items () == 0; } } + public ProjectFolderItem project { get; construct; } + private Scratch.Services.MonitoredRepository monitored_repo { + get { + return project.monitored_repo; + } + } + + private const ActionEntry[] ACTION_ENTRIES = { + { ACTION_RENAME_FILE, action_rename_file, "s" }, + { ACTION_RENAME_FOLDER, action_rename_folder, "s" }, + { ACTION_LAUNCH_APP_WITH_FILE_PATH, action_launch_app_with_file_path, "as" }, + { ACTION_SHOW_APP_CHOOSER, action_show_app_chooser, "s" }, + { ACTION_DELETE, action_delete, "s" }, + { ACTION_EXECUTE_CONTRACT_WITH_FILE_PATH, action_execute_contract_with_file_path, "as" }, + { ACTION_NEW_FILE, add_new_file, "s" }, + { ACTION_NEW_FROM_TEMPLATE, add_new_from_template, "(ss)" }, + { ACTION_NEW_FOLDER, add_new_folder, "s"}, + + }; + + private Granite.TreeList tree_list; + private Gtk.PopoverMenu context_menu; + public FolderTree (ProjectFolderItem project) { + Object ( + project: project + ); + } + + construct { + actions = new SimpleActionGroup (); + actions.add_action_entries (ACTION_ENTRIES, this); + insert_action_group (ITEM_ACTION_GROUP, actions); + + tree_list = new Granite.TreeList (); + actions = new SimpleActionGroup (); + actions.add_action_entries (ACTION_ENTRIES, this); + insert_action_group (ITEM_ACTION_PREFIX, actions); + + // Convert ListView signal into file_activate + tree_list.item_activated.connect ((item) => { + if (item is FileItem) { + tree_list.activate_action ( + "win.show-document", + "s", + item.path + ); + file_activate (((FileItem) item).file); + } else if (item.is_expandable) { + item.is_expanded = !item.is_expanded; + } + }); + + context_menu = new Gtk.PopoverMenu.from_model (null) { + position = BOTTOM + }; + this.append (context_menu); + tree_list.popup_context_menu.connect (on_popup_context_menu); + + append (tree_list); + + var cfile = new Code.File (root_path); + foreach (var child_file in cfile.children) { + if (child_file.is_valid_directory) { + tree_list.add_root_item (new FolderItem (child_file, this)); + } else if (child_file.is_valid_textfile) { + tree_list.add_root_item (new FileItem (child_file, this)); + } + } + + if (monitored_repo != null) { + monitored_repo.ignored_changed.connect (() => update_item_status (null)); + monitored_repo.file_status_change.connect (() => update_item_status (null)); + } + } + + //TODO Move to FolderTree + public void after_child_folder_loaded (FolderItem folder) { + warning ("after child folder loaded"); + //TODO Do we need to maintain list if visible children? + // iterate_children (folder, (child) => { + // if (child is FolderManagerItem) { + // var item = (FolderManagerItem)child; + // var rel_path = this.file.file.get_relative_path (item.file.file); + // if (rel_path != null && rel_path != "") { + // visible_item_list.prepend ({rel_path, item}); + // } + // } + + // return Granite.TreeList.ITERATE_CONTINUE; + // }); + + if (project.monitored_repo != null) { + project.monitored_repo.update_status_map (); + update_item_status (folder); + // deprioritize_git_ignored (); + } + } + + //TODO For now just get status of non-folder items not closed folders + private void update_item_status (FolderItem? start_folder, uint level = 0) + requires (project.monitored_repo != null && start_folder.is_expanded) + { +warning ("update status level %u", level); + tree_list.iterate_children (start_folder, (item) => { + var child_item = (FolderManagerItem) item; + if (child_item is FolderItem && child_item.is_expanded) { + update_item_status ((FolderItem) child_item, ++level); + } else if (!(child_item is FolderItem)) { + check_child_file_status (child_item); + } + + deprioritize_git_ignored (child_item); + return Granite.TreeList.ITERATE_CONTINUE; + }); + } + + private void check_child_file_status (FolderManagerItem child_item) requires (!(child_item is FolderItem)) { + project.monitored_repo.non_current_entries.@foreach ((entry) => { + // Match non_current_path with parent folder as well as itself + var match = entry.key == child_item.path; + if (match) { + var is_new = ( + entry.@value & (Ggit.StatusFlags.WORKING_TREE_NEW | Ggit.StatusFlags.INDEX_NEW)) > 0 + ; + child_item.secondary_icon_name = is_new ? ADDED_ICON_NAME : MODIFIED_ICON_NAME; + child_item.secondary_icon_tooltip = is_new ? _("New") : _("Modified"); + return false; + } else { + return true; + } + }); + } + + //TODO Move to folder tree + private void deprioritize_git_ignored (FolderManagerItem child_item) + requires (project.monitored_repo != null) + { + + try { + child_item.deprioritize = project.monitored_repo.path_is_ignored (child_item.path); + if (child_item.deprioritize) { + warning ("%s deprioritised", child_item.text); + } + } catch (Error e) { + warning ("An error occurred while checking if item '%s' is git-ignored: %s", child_item.name, e.message); + } + } + + public bool contains_file (GLib.File gfile) { + //TODO Fill out + return false; + } + + private void action_close_folder (SimpleAction action, GLib.Variant? parameter) { + } + + private void action_close_other_folders (SimpleAction action, GLib.Variant? parameter) { + } + + private void action_set_active_project (SimpleAction action, GLib.Variant? parameter) { + } + + private ProjectFolderItem? set_active_project (string path) { + //TODO complete + return null; + } + + private void set_project_active (string path) { + } + + public async void restore_saved_state () { + } + + public void open_folder (File folder) { + } + + public void collapse_all () { + tree_list.iterate_children (null, (child) => { + child.collapse_all (true, true); + return Granite.TreeList.ITERATE_CONTINUE; + }); + } + + public void select_path (string path) { + // find_path (null, path); + } + + public void unselect_file (GLib.File file) { + //TODO Complete this + } + + public void unselect_all () { + tree_list.unselect_all (); + } + + public void remove_all () { + tree_list.remove_all (); + } + + private void on_popup_context_menu (Graphene.Point vp, Granite.TreeListItem treelistitem) { + var foldermanageritem = (Code.FolderManagerItem) treelistitem; + var model = foldermanageritem.get_context_menu (); + context_menu.menu_model = model; + context_menu.pointing_to = Gdk.Rectangle () {x = (int) vp.x, y = (int) vp.y, height = 1, width = 1}; + context_menu.popup (); + } + + public void collapse_other_projects () { + } + + // Move to MainWindow?? + // public void branch_actions (string path) { + // // Must only carry out branch actions on active project so switch if necessary. + // //TODO Warn before switching active project? + // var active_project = set_active_project (path); + // if (active_project == null || !active_project.is_git_repo) { + // Gdk.Display.get_default ().beep (); + // return; + // } + + // var dialog = new Dialogs.BranchActionDialog (active_project); + // dialog.response.connect ((res) => { + // if (res == Gtk.ResponseType.APPLY) { + // perform_branch_action (dialog); + // } + + // dialog.destroy (); + // }); + + // dialog.present (); + // } + + //TODO Move to Dialog or MainWindow + // private void perform_branch_action ( + // Scratch.Dialogs.BranchActionDialog dialog + // ) { + // switch (dialog.action) { + // case CHECKOUT: + // dialog.project.checkout_branch_ref (dialog.branch_ref); + // break; + // case COMMIT: + // break; + // case PUSH: + // break; + // case PULL: + // break; + // case MERGE: + // break; + // case DELETE: + // break; + // case CREATE: + // dialog.project.new_branch (dialog.new_branch_name); + // break; + // default: + // assert_not_reached (); + // } + // } + + public FolderManagerItem? find_path ( + Granite.TreeListItem? start, + string path, // File path to search fod + bool expand = false // Whether to expsnd to show found item + ) { + +warning ("Folder tree find path"); + FolderManagerItem? matched_item = null; + var target = GLib.File.new_for_path (path); + + tree_list.iterate_children (start, (item) => { + if ((item is FolderManagerItem) && item.path == path) { + matched_item = (FolderManagerItem) item; + return Granite.TreeList.ITERATE_STOP; + } + + if (item is FolderItem) { + var folder = item as FolderItem; + var folder_root = folder.file.file; + if (folder_root.get_relative_path (target) == null) { + return Granite.TreeList.ITERATE_CONTINUE; + } + + if (!folder.is_expanded) { + if (expand) { + folder.load_children (); //Synchronous + folder.is_expanded = true; + } else { + return Granite.TreeList.ITERATE_CONTINUE; + } + } + + var recurse_item = find_path (folder, path, expand); + if (recurse_item != null) { + matched_item = recurse_item; + return Granite.TreeList.ITERATE_STOP; + } + } + + return Granite.TreeList.ITERATE_CONTINUE; + }); + + return matched_item; + } + + // public bool project_is_open (string project_path) { + // return get_project_for_file (GLib.File.new_for_path (project_path)) != null; + // } + + // public ProjectFolderItem? get_project_for_file (GLib.File file) { + // ProjectFolderItem? matched_project = null; + // tree_list.iterate_children (null, (item) => { + // if (item is ProjectFolderItem) { + // var folder = (ProjectFolderItem) item; + // if (folder.file.file.equal (file) || folder.contains_file (file)) { + // matched_project = folder; + // return Granite.TreeList.ITERATE_STOP; + // } + // } + + // return Granite.TreeList.ITERATE_CONTINUE; + // }); + + // return matched_project; + // } + + public Granite.TreeListItem? expand_to_path (string path) { + return find_path (null, path, true); + } + + // /* Do global search on project containing the file path supplied in parameter */ + // public void search_global (string path, string? term = null) { + // var item_for_path = (FolderManagerItem?)(expand_to_path (path)); + // if (item_for_path != null) { + // var search_root = item_for_path.get_root_folder (); + // if (search_root is ProjectFolderItem) { + // GLib.File start_folder = (item_for_path is FolderItem) + // ? item_for_path.file.file + // : search_root.file.file; + + // bool is_explicit = !(item_for_path is ProjectFolderItem); + // search_root.global_search.begin (start_folder, term, is_explicit); + // } + // } + // } + + public void clear_badges () { + // tree_list.iterate_children (null, (child) => { + // if (child is ProjectFolderItem) { + // ((FolderItem)child).remove_all_badges (); + // } + + // return Granite.TreeList.ITERATE_CONTINUE; + // }); + } + + public void folder_item_update_hook (GLib.File source, GLib.File? dest, GLib.FileMonitorEvent event) { + // plugins.hook_folder_item_change (source, dest, event); + //TODO Make plugins a singleton + } + + private void iterate_children (Granite.TreeListItem? start, Granite.TreeList.ListIteratorCallback cb) { + tree_list.iterate_children (start, cb); + } + + // This only works when the list is stable (nothing being added, expanded etc) + private void rename_file (string path) { + // this.select_path (path); + // if (this.start_editing_item (selected)) { + // ulong once = 0; + // once = selected.edited.connect ((new_name) => { + // selected.disconnect (once); + // var new_path = Path.get_dirname (path) + Path.DIR_SEPARATOR_S + new_name; + // activate_action ( + // Scratch.MainWindow.ACTION_PREFIX + Scratch.MainWindow.ACTION_CLOSE_TAB, + // "s", + // path + // ); + + // // RecentManager requires valid URI + // var new_uri = "file://" + new_path; // Code only edits local files + // Gtk.RecentManager.get_default ().add_item (new_uri); + + // activate (new_path); + // }); + // } + + // // Handle cancelled rename (which does not produce signal) + // Timeout.add (200, () => { + // if (this.editing) { + // return Source.CONTINUE; + // } else { + // // Avoid selected but unopened item if rename cancelled (they would not open if clicked on) + // this.unselect_all (); + // return Source.REMOVE; + // } + // }); + } + + private void rename_folder (string path) { + // var folder_to_rename = find_path (null, path) as FolderItem; + // if (folder_to_rename == null) { + // critical ("Could not find folder from given path to rename: %s", path); + // return; + // } + + // folder_to_rename.selectable = true; + // if (start_editing_item (folder_to_rename)) { + // // Need to poll view as no signal emited when editing cancelled and need to set + // // selectable to false anyway. + // Timeout.add (200, () => { + // if (editing) { + // return Source.CONTINUE; + // } else { + // unselect_all (); + // // Must do this *after* unselecting all else sourcelist breaks + // folder_to_rename.selectable = false; + // } + + // return Source.REMOVE; + // }); + // } else { + // critical ("Could not rename %s", path); + // folder_to_rename.selectable = false; + // } + } + + private void rename_items_with_same_name (FolderManagerItem item) { + // string item_name = item.file.name; + // tree_list.iterate_children (null, (child) => { + // string new_other_item_name, new_item_name; + // var other_item = (ProjectFolderItem) child; + + // if (Utils.find_unique_path ( + // item.file.file, + // other_item.file.file, + // out new_item_name, + // out new_other_item_name + // ) + // ) { + // if (item_name.length < new_item_name.length) { + // item_name = new_item_name; + // } + + // if (other_item.name.length < new_other_item_name.length) { + // other_item.name = new_other_item_name; + // } + // } + + // return Granite.TreeList.ITERATE_CONTINUE; + // }); + + // item.name = item_name; + } + + private void add_new_folder (SimpleAction action, Variant? param) { + // // Using "path" of parent folder from params, call `on_add_new (true)` on `FolderItem` + // var path = param.get_string (); + + // if (path == null || path == "") { + // return; + // } + + // var folder = find_path (null, path) as FolderItem; + // if (folder == null) { + // return; + // } + + // folder.on_add_new (true); + } + + private void add_new_file (SimpleAction action, Variant? param) { + // // Using "path" of parent folder from params, call `on_add_new (false)` on `FolderItem` + // var path = param != null ? param.get_string () : null; + + // if (path == null || path == "") { + // critical ("No path"); + // return; + // } + + // var folder = find_path (null, path) as FolderItem; + // if (folder == null) { + // return; + // } + + // folder.on_add_new (false); + } + + private void add_new_from_template (SimpleAction action, Variant? param) { + // // Using "path" of parent folder from params, call `on_add_new (false)` on `FolderItem` + // // var path = param.get_string (); + // string? parent_path = null, template_path = null; + // param.@get ("(ss)", out parent_path, out template_path); + + // //Do we need this check? + // if (parent_path == null || parent_path == "") { + // return; + // } + + // var folder = find_path (null, parent_path) as FolderItem; + // if (folder == null) { + // return; + // } + + // folder.on_add_template (template_path); + } + + private void action_launch_app_with_file_path (SimpleAction action, Variant? param) { + // var params = param.get_strv (); + // Utils.launch_app_with_file (params[1], params[0]); + } + + private void action_show_app_chooser (SimpleAction action, Variant? param) { + // var path = param.get_string (); + + // if (path == null || path == "") { + // return; + // } + + // var file = GLib.File.new_for_path (path); + // var dialog = new Gtk.AppChooserDialog (new Gtk.Window (), Gtk.DialogFlags.MODAL, file); + // dialog.deletable = false; + + // dialog.response.connect ((res) => { + // if (res == Gtk.ResponseType.OK) { + // var app_info = dialog.get_app_info (); + // if (app_info != null) { + // Utils.launch_app_with_file (app_info.get_id (), path); + // } + // } + + // dialog.destroy (); + // }); + + // dialog.show (); + } + + private void action_execute_contract_with_file_path (SimpleAction action, Variant? param) { + // var params = param.get_strv (); + // var path = params[0]; + // if (path == null || path == "") { + // return; + // } + + // var contract_name = params[1]; + // if (contract_name == null || contract_name == "") { + // return; + // } + + // Utils.execute_contract_with_file_path (path, contract_name); + } + + private void action_rename_file (SimpleAction action, Variant? param) { + // var path = param.get_string (); + + // if (path == null || path == "") { + // return; + // } + + // rename_file (path); + } + + private void action_rename_folder (SimpleAction action, Variant? param) { + // var path = param.get_string (); + + // if (path == null || path == "") { + // return; + // } + + // rename_folder (path); + } + + + private void action_delete (SimpleAction action, Variant? param) { + // var path = param.get_string (); + + // if (path == null || path == "") { + // return; + // } + + // var item = find_path (null, path); + // if (item != null) { + // var item_to_delete = item as Code.FolderManagerItem; + + // // Wait for ProjectFolderItem closed signal handle logic to run before moving item to trash + // if (item_to_delete is Code.ProjectFolderItem) { + // item_to_delete.closed.connect_after (() => { + // item_to_delete.trash (); + // }); + // item_to_delete.closed (); + // return; + // } + + // item_to_delete.trash (); + // } + } + + // private async void add_folder (File folder, bool expand, bool restoring = false) { + // if (is_open (folder)) { + // warning ("Folder '%s' is already open.", folder.path); + // return; + // } else if (!folder.is_valid_directory) { + // warning ("Cannot open invalid directory."); + // return; + // } + + // var add_file = folder.file; + // // Need to deal with case where folder is parent or child of an existing project + // var parents = new List (); + // var children = new List (); + + // tree_list.iterate_children (null, (child) => { + // var item = (ProjectFolderItem) child; + // if (add_file.get_relative_path (item.file.file) != null) { + // debug ("Trying to add parent of existing project"); + // children.append (item); + // } else if (item.file.file.get_relative_path (add_file) != null) { + // debug ("Trying to add child of existing project"); + // parents.append (item); + // } + + // return Granite.TreeList.ITERATE_CONTINUE; + // }); + + // if (parents.length () > 0 || children.length () > 0) { + // assert (parents.length () <= 1); + // assert (parents.length () == 0 || children.length () == 0); + // var dialog = new Scratch.Dialogs.CloseProjectsConfirmationDialog ( + // (Scratch.MainWindow) get_root (), + // parents.length (), + // children.length () + // ); + + // var close_projects = false; + // dialog.response.connect ((res) => { + // if (res == Gtk.ResponseType.ACCEPT) { + // close_projects = true; + // } + + // dialog.destroy (); + // add_folder.callback (); + // }); + + // dialog.show (); + // yield; + + // if (close_projects) { + // foreach (var item in parents) { + // item.closed (); + // } + + // foreach (var item in children) { + // item.closed (); + // } + // } else { + // return; + // } + // } + + // // Process any closed signals emitted before proceeding + // Idle.add (() => { + // var folder_root = new ProjectFolderItem (folder, this); // Constructor adds project to GitManager + // tree_list.add_root_item (folder_root); + // rename_items_with_same_name (folder_root); + + // folder_root.is_expanded = expand; + // folder_root.closed.connect (() => { + // activate_action ( + // MainWindow.ACTION_PREFIX + MainWindow.ACTION_CLOSE_PROJECT_DOCS, + // "s", + // folder_root.path + // ); + + // tree_list.remove_root_item (folder_root); + + // tree_list.iterate_children (null, (child) => { + // var child_folder = (ProjectFolderItem) child; + // if (child_folder.name != child_folder.file.name) { + // rename_items_with_same_name (child_folder); + // } + + // return Granite.TreeList.ITERATE_CONTINUE; + // }); + + // Scratch.Services.GitManager.get_instance ().remove_project (folder_root); + // write_settings (); + // }); + + // // We do not want to rewrite settings while restoring from settings + // // This interferes with fuzzy-finder plugins_manager + // // See https://github.com/elementary/code/issues/1533 + // if (!restoring) { + // write_settings (); + // } + + // add_folder.callback (); + // return Source.REMOVE; + // }); + + // yield; + + // order_folders (); + // } + + // private bool is_open (File folder) { + // bool open = false; + // tree_list.iterate_children (null, (child) => { + // if (folder.path == ((FolderManagerItem) child).path) { + // open = true; + // return Granite.TreeList.ITERATE_STOP; + // } + + // return Granite.TreeList.ITERATE_CONTINUE; + // }); + + // return open; + // } + + // private void write_settings () { + // string[] to_save = {}; + // tree_list.iterate_children (null, (item) => { + // var saved = false; + // var folder_path = ((FolderManagerItem) item).path; + + // //Do we need to de-duplicate? Not possible to open a project twice? + // foreach (var saved_folder in to_save) { + // if (folder_path == saved_folder) { + // saved = true; + // break; + // } + // } + + // if (!saved) { + // to_save += folder_path; + // } + + // return Granite.TreeList.ITERATE_CONTINUE; + // }); + + // settings.set_strv ("opened-folders", to_save); + // } +} diff --git a/src/FolderManager/Item.vala b/src/FolderManager/Item.vala index f4621a1bf2..49466f2957 100644 --- a/src/FolderManager/Item.vala +++ b/src/FolderManager/Item.vala @@ -18,128 +18,124 @@ * Authored by: Julien Spautz , Andrei-Costin Zisu */ -namespace Scratch.FolderManager { - /** - * Common abstract class for file and folder items. - */ - public abstract class Item: Code.TreeListItem { - public File file { get; construct; } - public FileView view { get; construct; } - - public string name { - get { - return text; - } - - set { - text = value; - } - } +public abstract class Code.FolderManagerItem: Granite.TreeListItem, Code.FolderManagerItemInterface { + public signal void edited (string new_name); - public string path { - owned get { return file.path; } - set { file.path = value; } - } + public Code.File file { get; construct; } + public Code.FolderTree view { get; construct; } - public signal void edited (string new_name); - - construct { - is_selectable = true; - is_editable = true; - text = file.name; - icon = file.icon; - edited.connect (rename); - tooltip = Scratch.Utils.replace_home_with_tilde (file.path); - - notify["activatable-tooltip"].connect (() => { - tooltip = ("%s\n" + Granite.TOOLTIP_SECONDARY_TEXT_MARKUP).printf ( - Scratch.Utils.replace_home_with_tilde (file.path), - secondary_icon_tooltip - ); - }); + // interface Code.FolderManagerItemInterface + public string name { + get { + return text; } - protected void rename (string new_name) { - file.rename (new_name); + set { + text = value; } + } - public bool equal (Item b) { - return path == b.path; - } + public string path { get; set; } + public string badge { get; set; } - public void trash () { - file.trash (); - } - public int compare (Code.TreeListItem a, Code.TreeListItem b) { - // if (a is RenameItem) { - // return -1; - // } else - // if (b is RenameItem) { - // return 1; - // } + construct { + is_selectable = true; + is_editable = true; + text = file.name; + icon_name = file.icon_name; + edited.connect (rename); + tooltip = Scratch.Utils.replace_home_with_tilde (file.path); - if (a is FolderItem && b is FileItem) { - return -1; - } else if (a is FileItem && b is FolderItem) { - return 1; - } + notify["activatable-tooltip"].connect (() => { + tooltip = ("%s\n" + Granite.TOOLTIP_SECONDARY_TEXT_MARKUP).printf ( + Scratch.Utils.replace_home_with_tilde (file.path), + secondary_icon_tooltip + ); + }); - assert (a is Item && b is Item); //Ensure more informative error message + file.bind_property ("path", this, "path", SYNC_CREATE); + } - return File.compare (((Item)a).file, ((Item)b).file); - } + protected void rename (string new_name) { + file.rename (new_name); + } - public bool allow_dnd_sorting () { - return false; - } + public bool equal (FolderManagerItem b) { + return path == b.path; + } - public ProjectFolderItem? get_root_folder (Code.TreeListItem start = null) { - if (start == null) { - start = this; - } - - if (start is ProjectFolderItem) { - return start as ProjectFolderItem; - } else if (start.parent is ProjectFolderItem) { - return start.parent as ProjectFolderItem; - } else if (start.parent != null) { - return get_root_folder (start.parent); - } else { - return null; - } - } + public void trash () { + file.trash (); + } - // protected class RenameItem : Code.TreeListItem { - // public bool is_folder { get; construct; } - // public string name { get; set construct; } - - // public RenameItem (string name, bool is_folder) { - // Object ( - // name: name - // is_folder: is_folder - // ); - // } - - // construct { - // is_editable = true; - // is_selectable = true; - // edited.connect (on_edited); - - // if (is_folder) { - // icon = GLib.ContentType.get_icon ("inode/directory"); - // } else { - // icon = GLib.ContentType.get_icon ("text"); - // } - // } - - // private void on_edited (string new_name) { - // if (new_name != "") { - // name = new_name; - // } - // } + public int compare (Granite.TreeListItem a, Granite.TreeListItem b) { + // if (a is RenameItem) { + // return -1; + // } else + // if (b is RenameItem) { + // return 1; // } + warning ("compare item"); + if (a is FolderItem && b is FileItem) { + return -1; + } else if (a is FileItem && b is FolderItem) { + return 1; + } + + assert (a is FolderManagerItem && b is FolderManagerItem); //Ensure more informative error message + + return File.compare (((FolderManagerItem)a).file, ((FolderManagerItem)b).file); + } - public virtual GLib.Menu? get_context_menu () { return null; } + public bool allow_dnd_sorting () { + return false; } + + public ProjectFolderItem? get_root_folder (Granite.TreeListItem start = null) { + if (start == null) { + start = this; + } + + if (start is ProjectFolderItem) { + return start as ProjectFolderItem; + } else if (start.parent is ProjectFolderItem) { + return start.parent as ProjectFolderItem; + } else if (start.parent != null) { + return get_root_folder (start.parent); + } else { + return null; + } + } + + // protected class RenameItem : Granite.TreeListItem { + // public bool is_folder { get; construct; } + // public string name { get; set construct; } + + // public RenameItem (string name, bool is_folder) { + // Object ( + // name: name + // is_folder: is_folder + // ); + // } + + // construct { + // is_editable = true; + // is_selectable = true; + // edited.connect (on_edited); + + // if (is_folder) { + // icon = GLib.ContentType.get_icon ("inode/directory"); + // } else { + // icon = GLib.ContentType.get_icon ("text"); + // } + // } + + // private void on_edited (string new_name) { + // if (new_name != "") { + // name = new_name; + // } + // } + // } } + diff --git a/src/FolderManager/ProjectFolderItem.vala b/src/FolderManager/ProjectFolderItem.vala index 3f9437c06c..5e936c2d23 100644 --- a/src/FolderManager/ProjectFolderItem.vala +++ b/src/FolderManager/ProjectFolderItem.vala @@ -1,688 +1,664 @@ /*- - * Copyright (c) 2018-2026 elementary LLC. (https://elementary.io), - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License version 3 - * as published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranties of - * MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR - * PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - * - * Authored by: David Hewitt - */ - -namespace Scratch.FolderManager { - public class ProjectFolderItem : FolderItem { - struct VisibleItem { - public string rel_path; - public Item item; +* Copyright (c) 2018-2026 elementary LLC. (https://elementary.io), +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU Lesser General Public License version 3 +* as published by the Free Software Foundation, either version 3 of the +* License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranties of +* MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR +* PURPOSE. See the GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see . +* +* Authored by: David Hewitt +*/ + +// Top level items for ProjectList not derived from Granite.TreeListItem but sharing some interfaces +// public class Code.ProjectFolderItem : Object, Code.FolderInterface, Code.FolderManagerItemInterface { +public class Code.ProjectFolderItem : Object, Code.FolderInterface, Code.FolderManagerItemInterface { + public signal void deleted (); + public Scratch.Services.MonitoredRepository? monitored_repo { get; private set; default = null; } + public bool is_git_repo { + get { + return monitored_repo != null; } + } - private static Icon added_icon; - private static Icon modified_icon; - - - - public signal void closed (); + // Code.FolderManagerItemInterface + public string path { get; set; } + public string name { get; set; } + public string badge { get; set; } + + // Code.FolderItemInterface + public bool is_expanded { get; set; } + public FileMonitor monitor { get; set; } + public ListStore? child_model { get; set; default = null;} + + public Code.FolderTree folder_tree; + public Code.File file { get; construct; } + public Code.ProjectList view { get; construct; } + // For convenience + public GLib.File gfile { get { return file.file; }} + public string secondary_text { get; private set; } + + private struct VisibleItem { + public string rel_path; + public FolderManagerItem item; + } - public Scratch.Services.MonitoredRepository? monitored_repo { get; private set; default = null; } - // Cache the visible item in the project. - private List visible_item_list = null; + private Scratch.Services.GitManager git_manager; - public bool is_git_repo { - get { - return monitored_repo != null; - } - } + // Cache the visible item in the project. (Excludes items loaded but in collapsed folder) + private List visible_item_list = null; + // Child widget shows tree of project files and folders - private Ggit.Repository? git_repo { - get { - return (is_git_repo ? monitored_repo.git_repo : null); - } + private Ggit.Repository? git_repo { + get { + return (is_git_repo ? monitored_repo.git_repo : null); } + } - public ProjectFolderItem (File file, FileView view) requires (file.is_valid_directory) { - Object (file: file, view: view); - } + public ProjectFolderItem (File file, Code.ProjectList view) requires (file.is_valid_directory) { + Object (file: file, view: view); + } - static construct { - added_icon = new ThemedIcon ("emblem-git-new-symbolic"); - modified_icon = new ThemedIcon ("emblem-git-modified-symbolic"); - } - - private void branch_or_name_changed () { - if (monitored_repo != null) { - //As SourceList items are not widgets we have to use markup to change appearance of text. - if (monitored_repo.head_is_branch) { - text = "%s\n%s".printf ( - name, monitored_repo.branch_name - ); - } else { //Distinguish detached heads visually - text = "%s\n %s".printf ( - name, monitored_repo.branch_name - ); - } - } + construct { + path = file.path; + name = file.name; + monitored_repo = Scratch.Services.GitManager.get_instance ().add_project (this); + git_manager = Scratch.Services.GitManager.get_instance (); + folder_tree = new Code.FolderTree (this); + file.notify["name"].connect (branch_or_name_changed); + if (monitored_repo != null) { + monitored_repo.branch_changed.connect (branch_or_name_changed); + monitored_repo.update_status_map (); + monitored_repo.branch_changed (); } + } - construct { - monitored_repo = Scratch.Services.GitManager.get_instance ().add_project (this); - notify["name"].connect (branch_or_name_changed); - if (monitored_repo != null) { - monitored_repo.branch_changed.connect (branch_or_name_changed); - monitored_repo.ignored_changed.connect ((deprioritize_git_ignored)); - monitored_repo.file_status_change.connect (() => update_item_status (null)); - monitored_repo.update_status_map (); - monitored_repo.branch_changed (); - } - } + public void refresh_diff ( + ref Gee.HashMap line_status_map, + string doc_path + ) { + monitored_repo.refresh_diff (doc_path, ref line_status_map); + } - protected override void on_changed (GLib.File source, GLib.File? dest, GLib.FileMonitorEvent event) { - if (source.equal (file.file) && event == DELETED) { - closed (); - } else { - base.on_changed (source, dest, event); - } - } - public void child_folder_changed (FolderItem folder) { - if (monitored_repo != null) { - monitored_repo.update_status_map (); - } + public void child_folder_changed (FolderItem folder) { + if (monitored_repo != null) { + monitored_repo.update_status_map (); } + } - public void after_child_folder_loaded (FolderItem folder) { - view.iterate_children (folder, (child) => { - if (child is Item) { - var item = (Item)child; - var rel_path = this.file.file.get_relative_path (item.file.file); - if (rel_path != null && rel_path != "") { - visible_item_list.prepend ({rel_path, item}); - } - } - - return Code.TreeList.ITERATE_CONTINUE; - }); - - - if (monitored_repo != null) { - monitored_repo.update_status_map (); - update_item_status (folder); - deprioritize_git_ignored (); + // warning ("returns a menu model"); + public override Menu? get_context_menu () { + string file_type = ""; + try { + var info = gfile.query_info (GLib.FileAttribute.STANDARD_CONTENT_TYPE, GLib.FileQueryInfoFlags.NONE); + if (info.has_attribute (GLib.FileAttribute.STANDARD_CONTENT_TYPE)) { + file_type = info.get_content_type (); } + } catch (Error e) { + warning (e.message); } - public override GLib.Menu? get_context_menu () { - string file_type = ""; - try { - var info = file.file.query_info (GLib.FileAttribute.STANDARD_CONTENT_TYPE, GLib.FileQueryInfoFlags.NONE); - if (info.has_attribute (GLib.FileAttribute.STANDARD_CONTENT_TYPE)) { - file_type = info.get_content_type (); - } - } catch (Error e) { - warning (e.message); - } - - MenuItem set_active_folder_item; - if (is_git_repo) { - set_active_folder_item = new GLib.MenuItem ( - _("Set as Active Project"), - GLib.Action.print_detailed_name ( - MainWindow.ACTION_PREFIX + MainWindow.ACTION_SET_ACTIVE_PROJECT, - new Variant.string (file.path) - ) - ); - } else { - set_active_folder_item = new GLib.MenuItem ( - _("Open in Terminal Pane"), - GLib.Action.print_detailed_name ( - MainWindow.ACTION_PREFIX + MainWindow.ACTION_OPEN_IN_TERMINAL, - new Variant.string ( - Services.GitManager.get_instance ().get_default_build_dir (path) - ) - ) - ); - } - - set_active_folder_item.set_attribute_value ( - "accel", - Utils.get_accel_for_action ( - GLib.Action.print_detailed_name ( - MainWindow.ACTION_PREFIX + MainWindow.ACTION_OPEN_IN_TERMINAL, - "" - ) - ) - ); - - var external_actions_section = new GLib.Menu (); - external_actions_section.append_item (set_active_folder_item); - external_actions_section.append_item (create_submenu_for_open_in (file_type)); - - var folder_actions_section = new GLib.Menu (); - folder_actions_section.append_item (create_submenu_for_new ()); - if (monitored_repo != null) { - var branch_action_item = new MenuItem ( - _("Branch Actions…"), - GLib.Action.print_detailed_name ( - MainWindow.ACTION_PREFIX + MainWindow.ACTION_BRANCH_ACTIONS, - new Variant.string (file.path) - ) - ); - folder_actions_section.append_item (branch_action_item); - } - - var close_folder_item = new GLib.MenuItem ( - _("Close Folder"), - GLib.Action.print_detailed_name ( - FileView.ACTION_PREFIX + FileView.ACTION_CLOSE_FOLDER, - new Variant.string (file.path) - ) - ); - - var close_all_except_item = new GLib.MenuItem ( - _("Close Other Folders"), + MenuItem set_active_folder_item; + if (is_git_repo) { + set_active_folder_item = new GLib.MenuItem ( + _("Set as Active Project"), GLib.Action.print_detailed_name ( - FileView.ACTION_PREFIX + FileView.ACTION_CLOSE_OTHER_FOLDERS, + Scratch.MainWindow.ACTION_PREFIX + Scratch.MainWindow.ACTION_SET_ACTIVE_PROJECT, new Variant.string (file.path) ) ); - var close_other_folders_action = Utils.action_from_group ( - FileView.ACTION_CLOSE_OTHER_FOLDERS, - view.actions - ); - close_other_folders_action.set_enabled (view.n_root_items () > 1); - - var close_actions_section = new GLib.Menu (); - close_actions_section.append_item (close_folder_item); - close_actions_section.append_item (close_all_except_item); - - var n_open = Scratch.Services.DocumentManager.get_instance ().open_for_project (path); - var open_text = ngettext ("Close %u Open Document", - "Close %u Open Documents", - n_open).printf (n_open); - - var close_item = new GLib.MenuItem ( - open_text, - GLib.Action.print_detailed_name ( - MainWindow.ACTION_PREFIX + MainWindow.ACTION_CLOSE_PROJECT_DOCS, - new Variant.string (file.file.get_path ()) - ) - ); - - var hide_text = ngettext ("Hide %u Open Document", - "Hide %u Open Documents", - n_open).printf (n_open); - - var hide_item = new GLib.MenuItem ( - hide_text, + } else { + set_active_folder_item = new GLib.MenuItem ( + _("Open in Terminal Pane"), GLib.Action.print_detailed_name ( - MainWindow.ACTION_PREFIX + MainWindow.ACTION_HIDE_PROJECT_DOCS, - new Variant.string (file.file.get_path ()) - ) - ); - - hide_item.set_attribute_value ( - "accel", - Utils.get_accel_for_action ( - GLib.Action.print_detailed_name ( - MainWindow.ACTION_PREFIX + MainWindow.ACTION_HIDE_PROJECT_DOCS, - "" + Scratch.MainWindow.ACTION_PREFIX + Scratch.MainWindow.ACTION_OPEN_IN_TERMINAL, + new Variant.string ( + Scratch.Services.GitManager.get_instance ().get_default_build_dir (path) ) ) ); + } - var n_restorable = Scratch.Services.DocumentManager.get_instance ().restorable_for_project (path); - var restore_text = ngettext ("Restore %u Hidden Document", - "Restore %u Hidden Documents", - n_restorable).printf (n_restorable); - - var restore_item = new GLib.MenuItem ( - restore_text, - GLib.Action.print_detailed_name ( - MainWindow.ACTION_PREFIX + MainWindow.ACTION_RESTORE_PROJECT_DOCS, - new Variant.string (file.file.get_path ()) - ) - ); - - restore_item.set_attribute_value ( - "accel", - Utils.get_accel_for_action ( - GLib.Action.print_detailed_name ( - MainWindow.ACTION_PREFIX + MainWindow.ACTION_RESTORE_PROJECT_DOCS, - "" - ) - ) - ); - - var direct_actions_section = new GLib.Menu (); - if (n_restorable > 0) { - direct_actions_section.append_item (restore_item); - } - - if (n_open > 0) { - direct_actions_section.append_item (hide_item); - direct_actions_section.append_item (close_item); - } - - var search_item = new GLib.MenuItem ( - _("Find in Project…"), + set_active_folder_item.set_attribute_value ( + "accel", + Scratch.Utils.get_accel_for_action ( GLib.Action.print_detailed_name ( - MainWindow.ACTION_PREFIX + MainWindow.ACTION_FIND_GLOBAL, - new Variant.string (file.file.get_path ()) + Scratch.MainWindow.ACTION_PREFIX + Scratch.MainWindow.ACTION_OPEN_IN_TERMINAL, + "" ) - ); - - search_item.set_attribute_value ( - "accel", - Utils.get_accel_for_action ( - GLib.Action.print_detailed_name ( - MainWindow.ACTION_PREFIX + MainWindow.ACTION_FIND_GLOBAL, - "" - ) - ) - ); - - var search_actions_section = new GLib.Menu (); - search_actions_section.append_item (search_item); - - var menu_model = new GLib.Menu (); - menu_model.append_section (null, external_actions_section); - menu_model.append_section (null, folder_actions_section); - menu_model.append_section (null, close_actions_section); - menu_model.append_section (null, direct_actions_section); - menu_model.append_section (null, search_actions_section); - - return menu_model; - } - - public void update_item_status (FolderItem? start_folder) { - if (monitored_repo == null) { - debug ("Ignore non-git folders"); - return; - } - bool is_new = false; - string start_path = start_folder != null ? start_folder.path : ""; - visible_item_list.@foreach ((visible_item) => { - if (start_path.has_prefix (visible_item.rel_path)) { - return; //Only need to update status for start_folder and its children - } + ) + ); + + var new_window_menu_item = new GLib.MenuItem ( + _("Open in New Window"), + GLib.Action.print_detailed_name ( + Scratch.MainWindow.ACTION_PREFIX + Scratch.MainWindow.ACTION_OPEN_IN_NEW_WINDOW, + path + ) + ); + + var external_actions_section = new GLib.Menu (); + external_actions_section.append_item (set_active_folder_item); + external_actions_section.append_item (new_window_menu_item); + + var folder_actions_section = new GLib.Menu (); + folder_actions_section.append_item (create_submenu_for_new (path)); + + // if (monitored_repo != null) { + // var branch_action_item = new MenuItem ( + // _("Branch Actions…"), + // GLib.Action.print_detailed_name ( + // Scratch.MainWindow.ACTION_PREFIX + Scratch.MainWindow.ACTION_BRANCH_ACTIONS, + // new Variant.string (file.path) + // ) + // ); + // folder_actions_section.append_item (branch_action_item); + // } + + var close_folder_item = new GLib.MenuItem ( + _("Close Folder"), + GLib.Action.print_detailed_name ( + PROJECT_ACTION_PREFIX + ACTION_CLOSE_PROJECT_FOLDER, + new Variant.string (file.path) + ) + ); + + var close_all_except_item = new GLib.MenuItem ( + _("Close Other Folders"), + GLib.Action.print_detailed_name ( + PROJECT_ACTION_PREFIX + ACTION_CLOSE_OTHER_PROJECT_FOLDERS, + new Variant.string (file.path) + ) + ); + var close_other_folders_action = Scratch.Utils.action_from_group ( + ACTION_CLOSE_OTHER_PROJECT_FOLDERS, + view.actions + ); + close_other_folders_action.set_enabled (!view.is_empty); + + var close_actions_section = new GLib.Menu (); + close_actions_section.append_item (close_folder_item); + close_actions_section.append_item (close_all_except_item); + + // var n_open = Scratch.Services.DocumentManager.get_instance ().open_for_project (path); + // var open_text = ngettext ("Close %u Open Document", + // "Close %u Open Documents", + // n_open).printf (n_open); + + // var close_item = new GLib.MenuItem ( + // open_text, + // GLib.Action.print_detailed_name ( + // Scratch.MainWindow.ACTION_PREFIX + Scratch.MainWindow.ACTION_CLOSE_PROJECT_DOCS, + // new Variant.string (file.file.get_path ()) + // ) + // ); + + // var hide_text = ngettext ("Hide %u Open Document", + // "Hide %u Open Documents", + // n_open).printf (n_open); + + // var hide_item = new GLib.MenuItem ( + // hide_text, + // GLib.Action.print_detailed_name ( + // Scratch.MainWindow.ACTION_PREFIX + Scratch.MainWindow.ACTION_HIDE_PROJECT_DOCS, + // new Variant.string (file.file.get_path ()) + // ) + // ); + + // hide_item.set_attribute_value ( + // "accel", + // Scratch.Utils.get_accel_for_action ( + // GLib.Action.print_detailed_name ( + // Scratch.MainWindow.ACTION_PREFIX + Scratch.MainWindow.ACTION_HIDE_PROJECT_DOCS, + // "" + // ) + // ) + // ); + + // var n_restorable = Scratch.Services.DocumentManager.get_instance ().restorable_for_project (path); + // var restore_text = ngettext ("Restore %u Hidden Document", + // "Restore %u Hidden Documents", + // n_restorable).printf (n_restorable); + + // var restore_item = new GLib.MenuItem ( + // restore_text, + // GLib.Action.print_detailed_name ( + // Scratch.MainWindow.ACTION_PREFIX + Scratch.MainWindow.ACTION_RESTORE_PROJECT_DOCS, + // new Variant.string (file.file.get_path ()) + // ) + // ); + + // restore_item.set_attribute_value ( + // "accel", + // Scratch.Utils.get_accel_for_action ( + // GLib.Action.print_detailed_name ( + // Scratch.MainWindow.ACTION_PREFIX + Scratch.MainWindow.ACTION_RESTORE_PROJECT_DOCS, + // "" + // ) + // ) + // ); + + // var direct_actions_section = new GLib.Menu (); + // if (n_restorable > 0) { + // direct_actions_section.append_item (restore_item); + // } + + // if (n_open > 0) { + // direct_actions_section.append_item (hide_item); + // direct_actions_section.append_item (close_item); + // } + + // var search_item = new GLib.MenuItem ( + // _("Find in Project…"), + // GLib.Action.print_detailed_name ( + // Scratch.MainWindow.ACTION_PREFIX + Scratch.MainWindow.ACTION_FIND_GLOBAL, + // new Variant.string (file.file.get_path ()) + // ) + // ); + + // search_item.set_attribute_value ( + // "accel", + // Scratch.Utils.get_accel_for_action ( + // GLib.Action.print_detailed_name ( + // Scratch.MainWindow.ACTION_PREFIX + Scratch.MainWindow.ACTION_FIND_GLOBAL, + // "" + // ) + // ) + // ); + + // var search_actions_section = new GLib.Menu (); + // search_actions_section.append_item (search_item); + + var menu_model = new GLib.Menu (); + menu_model.append_section (null, external_actions_section); + menu_model.append_section (null, folder_actions_section); + menu_model.append_section (null, close_actions_section); + // menu_model.append_section (null, direct_actions_section); + // menu_model.append_section (null, search_actions_section); + + return menu_model; + + // return null; + } - var item = visible_item.item; - item.secondary_icon = null; - monitored_repo.non_current_entries.@foreach ((entry) => { - // Match non_current_path with parent folder as well as itself - var match = entry.key.has_prefix (visible_item.rel_path); - if (match) { - is_new = (entry.@value & (Ggit.StatusFlags.WORKING_TREE_NEW | Ggit.StatusFlags.INDEX_NEW)) > 0; - // Only mark folders new if only contains new items otherwise mark modified - if (item is FolderItem && - is_new && item.secondary_icon == null) { - - item.secondary_icon = added_icon; - item.secondary_icon_tooltip = _("New"); - return true; // scan all children - } + // public void update_item_status () { + // // var status = folder_tree.update_status (); + // //TODO update appearance according to global status + // } - if (!(item is FolderItem) || !item.is_expanded) { //No need to show status when children shown - item.secondary_icon = is_new ? added_icon : modified_icon; - item.secondary_icon_tooltip = is_new ? _("New") : _("Modified"); - } - return false; - } else { - return true; - } - }); - }); - } - public bool contains_file (GLib.File descendant) { - return file.file.get_relative_path (descendant) != null; - } - private void deprioritize_git_ignored () requires (monitored_repo != null) { - visible_item_list.@foreach ((visible_item) => { - var item = visible_item.item; - try { - if (monitored_repo.path_is_ignored (visible_item.rel_path)) { - item.text = Markup.printf_escaped ("%s", item.name); - } else { - item.text = item.name; - } - } catch (Error e) { - warning ("An error occurred while checking if item '%s' is git-ignored: %s", item.name, e.message); - } - }); - } + // This assumes the descendant exists + public bool contains_file (GLib.File descendant) { + return file.file.get_relative_path (descendant) != null; + } - public bool checkout_branch_ref (Ggit.Ref branch_ref) { - if (branch_ref.is_branch ()) { - try { - var branch_name = ((Ggit.Branch)branch_ref).get_name (); - if (branch_name != null) { - return monitored_repo.change_local_branch (branch_name); - } else { - return false; - } - } catch (Error e) { - warning ("Failed to get branch name from ref. %s", e.message); - return false; - } - } else { - var target_shorthand = branch_ref.get_shorthand (); - if (target_shorthand != null) { - return monitored_repo.checkout_remote_branch (target_shorthand); + public bool checkout_branch_ref (Ggit.Ref branch_ref) { + if (branch_ref.is_branch ()) { + try { + var branch_name = ((Ggit.Branch)branch_ref).get_name (); + if (branch_name != null) { + return monitored_repo.change_local_branch (branch_name); } else { return false; } + } catch (Error e) { + warning ("Failed to get branch name from ref. %s", e.message); + return false; + } + } else { + var target_shorthand = branch_ref.get_shorthand (); + if (target_shorthand != null) { + return monitored_repo.checkout_remote_branch (target_shorthand); + } else { + return false; } } + } - public void new_branch (string branch_name) { - try { - if (monitored_repo.head_is_branch) { - monitored_repo.create_new_branch (branch_name); - } else { - throw new IOError.NOT_FOUND ("Cannot create a new branch when head is detached"); - } - } catch (Error e) { - var dialog = new Granite.MessageDialog ( - _("Error while creating new branch: “%s”").printf (branch_name), - e.message, - new ThemedIcon ("git"), - Gtk.ButtonsType.CLOSE - ) { - badge_icon = new ThemedIcon ("dialog-error"), - modal = true - }; - dialog.transient_for = (Gtk.Window)(view.get_root ()); - dialog.response.connect (() => { - dialog.destroy (); - }); - dialog.show (); + public void new_branch (string branch_name) { + try { + if (monitored_repo.head_is_branch) { + monitored_repo.create_new_branch (branch_name); + } else { + throw new IOError.NOT_FOUND ("Cannot create a new branch when head is detached"); } + } catch (Error e) { + var dialog = new Granite.MessageDialog ( + _("Error while creating new branch: “%s”").printf (branch_name), + e.message, + new ThemedIcon ("git"), + Gtk.ButtonsType.CLOSE + ) { + badge_icon = new ThemedIcon ("dialog-error"), + modal = true + }; + dialog.transient_for = (Gtk.Window)(view.get_root ()); + dialog.response.connect (() => { + dialog.destroy (); + }); + dialog.show (); } + } - public bool is_recent_ref (Ggit.Ref bref) { - return monitored_repo.is_recent_ref (bref); - } + public bool is_recent_ref (Ggit.Ref bref) { + return monitored_repo.is_recent_ref (bref); + } - public Gee.ArrayList get_all_branch_refs () requires (is_git_repo) { - return monitored_repo.get_all_branch_refs (); - } + public Gee.ArrayList get_all_branch_refs () requires (is_git_repo) { + return monitored_repo.get_all_branch_refs (); + } - public bool has_local_branch_name (string name) { - return is_git_repo ? monitored_repo.has_local_branch_name (name) : false; - } + public bool has_local_branch_name (string name) { + return is_git_repo ? monitored_repo.has_local_branch_name (name) : false; + } + + public bool has_remote_branch_name (string name) { + return is_git_repo ? monitored_repo.has_remote_branch_name (name) : false; + } - public bool has_remote_branch_name (string name) { - return is_git_repo ? monitored_repo.has_remote_branch_name (name) : false; + public bool has_branch_name (string name, out bool found_is_remote) { + found_is_remote = false; + // var is_remote = false; + var found = false; + if (!has_local_branch_name (name)) { + found = has_remote_branch_name (name); + found_is_remote = found; + } else { + found = true; } - public bool has_branch_name (string name, out bool found_is_remote) { - found_is_remote = false; - // var is_remote = false; - var found = false; - if (!has_local_branch_name (name)) { - found = has_remote_branch_name (name); - found_is_remote = found; - } else { - found = true; - } + return found; + } - return found; - } + public string get_current_branch_name () { + return is_git_repo ? monitored_repo.branch_name : ""; + } + + public bool is_valid_new_branch_name (string new_name) { + return is_git_repo ? monitored_repo.is_valid_new_local_branch_name (new_name) : false; + } - public string get_current_branch_name () { - return is_git_repo ? monitored_repo.branch_name : ""; + // The parameter "is_explicit" indicates whether a global search was requested + // via a context menu on an explicitly chosen folder, in which case everything in that + // folder will be searched, or whether the hot-key was used in which case the search will + // take place on the active project and will omit certain folders + public async void global_search ( + GLib.File start_folder = this.file.file, + string? term = null, + bool is_explicit = false + ) { + /* For now set all options to the most inclusive (except case). + * The ability to set these in the dialog (or by parameter) may be added later. */ + string? search_term = null; + bool search_tracked_only = false; + bool recurse_subfolders = true; + bool check_is_text = true; + string[] path_spec = {"*.*"}; + bool modified_only = false; + bool case_sensitive = false; + Regex? pattern = null; + + var wholeword_search = Scratch.settings.get_boolean ("wholeword-search"); + var case_mode = (Scratch.CaseSensitiveMode)(Scratch.settings.get_enum ("case-sensitive-search")); + var use_regex = Scratch.settings.get_boolean ("regex-search"); + + switch (case_mode) { + case NEVER: + case_sensitive = false; + break; + case MIXED: + case_sensitive = (term != term.ascii_up () && term != term.ascii_down ()); + break; + case ALWAYS: + case_sensitive = true; + break; + default: + assert_not_reached (); } - public bool is_valid_new_branch_name (string new_name) { - return is_git_repo ? monitored_repo.is_valid_new_local_branch_name (new_name) : false; + var folder_name = start_folder.get_basename (); + if (this.file.file.equal (start_folder)) { + folder_name = name; } - // The parameter "is_explicit" indicates whether a global search was requested - // via a context menu on an explicitly chosen folder, in which case everything in that - // folder will be searched, or whether the hot-key was used in which case the search will - // take place on the active project and will omit certain folders - public async void global_search ( - GLib.File start_folder = this.file.file, - string? term = null, - bool is_explicit = false + var dialog = new Scratch.Dialogs.GlobalSearchDialog ( + folder_name, + monitored_repo != null && monitored_repo.git_repo != null, + case_sensitive, + wholeword_search, + use_regex ) { - /* For now set all options to the most inclusive (except case). - * The ability to set these in the dialog (or by parameter) may be added later. */ - string? search_term = null; - bool search_tracked_only = false; - bool recurse_subfolders = true; - bool check_is_text = true; - string[] path_spec = {"*.*"}; - bool modified_only = false; - bool case_sensitive = false; - Regex? pattern = null; - - var wholeword_search = Scratch.settings.get_boolean ("wholeword-search"); - var case_mode = (CaseSensitiveMode)(Scratch.settings.get_enum ("case-sensitive-search")); - var use_regex = Scratch.settings.get_boolean ("regex-search"); - - switch (case_mode) { - case NEVER: - case_sensitive = false; - break; - case MIXED: - case_sensitive = (term != term.ascii_up () && term != term.ascii_down ()); - break; - case ALWAYS: - case_sensitive = true; + search_term = term + }; + + dialog.response.connect ((response) => { + switch (response) { + case Gtk.ResponseType.ACCEPT: + search_term = dialog.search_term; break; - default: - assert_not_reached (); - } - var folder_name = start_folder.get_basename (); - if (this.file.file.equal (start_folder)) { - folder_name = name; + default: + term = null; + break; } - var dialog = new Scratch.Dialogs.GlobalSearchDialog ( - folder_name, - monitored_repo != null && monitored_repo.git_repo != null, - case_sensitive, - wholeword_search, - use_regex - ) { - search_term = term - }; - - dialog.response.connect ((response) => { - switch (response) { - case Gtk.ResponseType.ACCEPT: - search_term = dialog.search_term; - break; + dialog.destroy (); + global_search.callback (); + }); + + dialog.show (); + yield; + + if (search_term != null) { + // Remove results of previous search before attempting a new one + folder_tree.clear_badges (); + // Collapse everything + collapse (); + + /* Put search term in search bar to help user locate the position of the matches in each doc */ + var search_variant = new Variant.string (search_term); + var app = (Gtk.Application)GLib.Application.get_default (); + var win = (Scratch.MainWindow)(app.get_active_window ()); + win.actions.lookup_action ("action-find").activate (search_variant); + + if (!use_regex) { + search_term = Regex.escape_string (search_term); + if (wholeword_search) { + search_term = "\\b%s\\b".printf (search_term); + } + } // else use search_term as is - TODO do we need to escape it? - default: - term = null; - break; + try { + var flags = RegexCompileFlags.MULTILINE; + if (!case_sensitive) { + flags |= RegexCompileFlags.CASELESS; } - dialog.destroy (); - global_search.callback (); - }); + pattern = new Regex (search_term, flags); + } catch (Error e) { + critical ("Error creating regex from '%s': %s", search_term, e.message); + return; + } + } else { + return; + } - dialog.show (); - yield; - - if (search_term != null) { - // Remove results of previous search before attempting a new one - remove_all_badges (); - // Collapse everything - collapse_all (true, true); - - /* Put search term in search bar to help user locate the position of the matches in each doc */ - var search_variant = new Variant.string (search_term); - var app = (Gtk.Application)GLib.Application.get_default (); - var win = (Scratch.MainWindow)(app.get_active_window ()); - win.actions.lookup_action ("action-find").activate (search_variant); - - if (!use_regex) { - search_term = Regex.escape_string (search_term); - if (wholeword_search) { - search_term = "\\b%s\\b".printf (search_term); - } - } // else use search_term as is - TODO do we need to escape it? + var status_scope = Ggit.StatusOption.DEFAULT; + if (!modified_only) { + status_scope |= Ggit.StatusOption.INCLUDE_UNMODIFIED; + } + if (!search_tracked_only) { + status_scope |= Ggit.StatusOption.INCLUDE_UNTRACKED; + } + var status_options = new Ggit.StatusOptions ( + status_scope, + Ggit.StatusShow.WORKDIR_ONLY, + path_spec + ); - try { - var flags = RegexCompileFlags.MULTILINE; - if (!case_sensitive) { - flags |= RegexCompileFlags.CASELESS; + if (monitored_repo != null && !is_explicit) { + try { + monitored_repo.git_repo.file_status_foreach (status_options, (rel_path, status) => { + var target = file.file.resolve_relative_path (rel_path); + if (check_is_text && rel_path.has_prefix ("po/")) { // Ignore translation files + return 0; } - pattern = new Regex (search_term, flags); - } catch (Error e) { - critical ("Error creating regex from '%s': %s", search_term, e.message); - return; - } - } else { - return; - } + if ((recurse_subfolders && start_folder.get_relative_path (target) != null) || + start_folder.equal (target.get_parent ())) { - var status_scope = Ggit.StatusOption.DEFAULT; - if (!modified_only) { - status_scope |= Ggit.StatusOption.INCLUDE_UNMODIFIED; - } - if (!search_tracked_only) { - status_scope |= Ggit.StatusOption.INCLUDE_UNTRACKED; + perform_match (target, pattern, check_is_text); + } + + return 0; //TODO Allow cancelling? + }); + } catch (Error err) { + warning ("Error getting file status: %s", err.message); } - var status_options = new Ggit.StatusOptions ( - status_scope, - Ggit.StatusShow.WORKDIR_ONLY, - path_spec - ); + } else { + search_folder_children (start_folder, pattern, recurse_subfolders); + } - if (monitored_repo != null && !is_explicit) { - try { - monitored_repo.git_repo.file_status_foreach (status_options, (rel_path, status) => { - var target = file.file.resolve_relative_path (rel_path); - if (check_is_text && rel_path.has_prefix ("po/")) { // Ignore translation files - return 0; - } + return; + } - if ((recurse_subfolders && start_folder.get_relative_path (target) != null) || - start_folder.equal (target.get_parent ())) { + public void set_as_active_project () { + git_manager.active_project_path = path; + } - perform_match (target, pattern, check_is_text); - } + public void collapse () { + is_expanded = false; + } - return 0; //TODO Allow cancelling? - }); - } catch (Error err) { - warning ("Error getting file status: %s", err.message); - } - } else { - search_folder_children (start_folder, pattern, recurse_subfolders); - } + public void expand () { + is_expanded = true; + } - return; + public FolderManagerItem? find_path (string path, bool expand) { + return folder_tree.find_path (null, path, expand); + } + // Does not remove from liststore - that is up to ProjectList + public void close () { + folder_tree.remove_all (); + git_manager.remove_project (this); // Takes care of active_project? + } + protected override void on_changed (GLib.File source, GLib.File? dest, GLib.FileMonitorEvent event) { + if (source.equal (file.file) && event == DELETED) { + deleted (); } + } - private void search_folder_children (GLib.File start_folder, Regex pattern, bool recurse_subfolders) { - try { - var enumerator = start_folder.enumerate_children ( - FileAttribute.STANDARD_CONTENT_TYPE + "," + FileAttribute.STANDARD_TYPE, - FileQueryInfoFlags.NOFOLLOW_SYMLINKS, - null - ); - - unowned FileInfo info = null; - unowned GLib.File child = null; - while (enumerator.iterate (out info, out child, null) && info != null) { - if (info != null && info.has_attribute (FileAttribute.STANDARD_TYPE)) { - if (info.get_file_type () == FileType.DIRECTORY) { - if (recurse_subfolders) { - search_folder_children (child, pattern, recurse_subfolders); - } - } else { - perform_match (child, pattern, true, info); + private void search_folder_children (GLib.File start_folder, Regex pattern, bool recurse_subfolders) { + try { + var enumerator = start_folder.enumerate_children ( + FileAttribute.STANDARD_CONTENT_TYPE + "," + FileAttribute.STANDARD_TYPE, + FileQueryInfoFlags.NOFOLLOW_SYMLINKS, + null + ); + + unowned FileInfo info = null; + unowned GLib.File child = null; + while (enumerator.iterate (out info, out child, null) && info != null) { + if (info != null && info.has_attribute (FileAttribute.STANDARD_TYPE)) { + if (info.get_file_type () == FileType.DIRECTORY) { + if (recurse_subfolders) { + search_folder_children (child, pattern, recurse_subfolders); } + } else { + perform_match (child, pattern, true, info); } } - } catch (Error enumerate_error) { - warning ("Error enumerating children of %s: %s", start_folder.get_path (), enumerate_error.message); } + } catch (Error enumerate_error) { + warning ("Error enumerating children of %s: %s", start_folder.get_path (), enumerate_error.message); } + } - private void perform_match (GLib.File target, - Regex search_regex, - bool check_is_text = false, - FileInfo? target_info = null) { - string contents; - string target_path = target.get_path (); - if (check_is_text) { - FileInfo? info = null; - if (target_info == null) { - try { - info = target.query_info ( - FileAttribute.STANDARD_CONTENT_TYPE, - FileQueryInfoFlags.NOFOLLOW_SYMLINKS, - null - ); - } catch (Error query_error) { - warning ( - "Error getting file info for %s: %s. Ignoring.", target.get_path (), query_error.message - ); - } - } else { - info = target_info; - } - - if (info == null) { - return; + private void perform_match (GLib.File target, + Regex search_regex, + bool check_is_text = false, + FileInfo? target_info = null) { + string contents; + string target_path = target.get_path (); + if (check_is_text) { + FileInfo? info = null; + if (target_info == null) { + try { + info = target.query_info ( + FileAttribute.STANDARD_CONTENT_TYPE, + FileQueryInfoFlags.NOFOLLOW_SYMLINKS, + null + ); + } catch (Error query_error) { + warning ( + "Error getting file info for %s: %s. Ignoring.", target.get_path (), query_error.message + ); } + } else { + info = target_info; + } - var type = info.get_content_type (); - if (!ContentType.is_mime_type (type, "text/*") || - ContentType.is_mime_type (type, "image/*")) { //Do not search svg images - return; - } + if (info == null) { + return; } - try { - FileUtils.get_contents (target_path, out contents); - } catch (Error e) { - warning ("error getting contents: %s", e.message); + var type = info.get_content_type (); + if (!ContentType.is_mime_type (type, "text/*") || + ContentType.is_mime_type (type, "image/*")) { //Do not search svg images return; } + } - MatchInfo? match_info = null; - int match_count = 0; - try { - for (search_regex.match (contents, RegexMatchFlags.NOTEMPTY, out match_info); - match_info.matches (); - match_info.next ()) { + try { + FileUtils.get_contents (target_path, out contents); + } catch (Error e) { + warning ("error getting contents: %s", e.message); + return; + } - match_count++; - } - } catch (RegexError next_error) { - critical ("Error getting next match: %s", next_error.message); - } + MatchInfo? match_info = null; + int match_count = 0; + try { + for (search_regex.match (contents, RegexMatchFlags.NOTEMPTY, out match_info); + match_info.matches (); + match_info.next ()) { - if (match_count > 0) { - var item = view.expand_to_path (target_path); - if (item != null) { - item.badge = match_count.to_string (); - } + match_count++; } + } catch (RegexError next_error) { + critical ("Error getting next match: %s", next_error.message); + } - return; + if (match_count > 0) { + var item = view.expand_to_path (target_path); + if (item != null) { + item.badge = match_count.to_string (); + } } - public void refresh_diff (ref Gee.HashMap line_status_map, string doc_path) { - monitored_repo.refresh_diff (doc_path, ref line_status_map); + return; + } + + private void branch_or_name_changed () { + name = file.name; + if (monitored_repo != null) { + //As SourceList items are not widgets we have to use markup to change appearance of text. + if (monitored_repo.head_is_branch) { + secondary_text = monitored_repo.branch_name; + } else { //Distinguish detached heads visually + secondary_text = _("%s (detached)").printf (monitored_repo.branch_name); + } } } } + diff --git a/src/FolderManager/ProjectList.vala b/src/FolderManager/ProjectList.vala new file mode 100644 index 0000000000..dc51fcc562 --- /dev/null +++ b/src/FolderManager/ProjectList.vala @@ -0,0 +1,784 @@ +/* + * SPDX-License-Identifier: GPL-3.0-or-later + * SPDX-FileCopyrightText: 2026 elementary, Inc. + */ + +namespace Code { + // All project related actions handled in ProjectFolderItem + public const string PROJECT_ACTION_GROUP = "project"; + public const string PROJECT_ACTION_PREFIX = "project."; + public const string ACTION_CLOSE_PROJECT_FOLDER = "close-project-folder"; + public const string ACTION_CLOSE_OTHER_PROJECT_FOLDERS = "close-other-project-folders"; + public const string ACTION_EXECUTE_CONTRACT_WITH_FILE_PATH = "execute-contract-with-file-path"; + public const string ACTION_NEW_FILE = "new-file"; + public const string ACTION_NEW_FROM_TEMPLATE = "new-from-template"; + public const string ACTION_NEW_FOLDER = "new-folder"; + + // All fileitem related actions handled in FolderTree + public const string ITEM_ACTION_GROUP = "item"; + public const string ITEM_ACTION_PREFIX = "item."; + public const string ACTION_RENAME_FILE = "rename-file"; + public const string ACTION_RENAME_FOLDER = "rename-folder"; + public const string ACTION_LAUNCH_APP_WITH_FILE_PATH = "launch-app-with-file-path"; + public const string ACTION_SHOW_APP_CHOOSER = "show-app-chooser"; + public const string ACTION_DELETE = "delete"; +} +/* ProjectList is a flat list of FolderTrees each representing a single project */ +public class Code.ProjectList : Granite.Bin, Code.PaneSwitcher { + private const ActionEntry[] ACTION_ENTRIES = { + { ACTION_CLOSE_PROJECT_FOLDER, action_close_project_folder, "s"}, + { ACTION_CLOSE_OTHER_PROJECT_FOLDERS, action_close_other_project_folders, "s"} + }; + + public const string CLOSE_PROJECT_DOCS_ACTION_NAME = + Scratch.MainWindow.ACTION_PREFIX + + Scratch.MainWindow.ACTION_CLOSE_PROJECT_DOCS + ; + + public SimpleActionGroup actions { get; private set; } + public bool is_empty { get { return list_store.n_items == 0; } } + + // PaneSwitcher interface + public string icon_name { get; set; } + public string title { get; set; } + + private Gtk.ScrolledWindow scrolled_window; + private Gtk.ListBox list_box; + private GLib.ListStore list_store; + private Gtk.NoSelection selection_model; + + private GLib.Settings settings; + private Scratch.Services.PluginsManager plugins; + + // public ProjectList (Scratch.Services.PluginsManager plugins_manager) { + // plugins = plugins_manager; + // } + + construct { + actions = new SimpleActionGroup (); + actions.add_action_entries (ACTION_ENTRIES, this); + insert_action_group (PROJECT_ACTION_GROUP, actions); + + //For Code.PaneSwitcher iterface + icon_name = "not-exist"; + title = "Projects"; + settings = new GLib.Settings ("io.elementary.code.folder-manager"); + list_store = new ListStore (typeof (ProjectFolderItem)); + selection_model = new Gtk.NoSelection (list_store); + list_box = new Gtk.ListBox (); + list_box.add_css_class (Granite.STYLE_CLASS_SIDEBAR); + list_box.bind_model ( + selection_model, + (obj) => { + var project_item = (ProjectFolderItem) obj; + return new ProjectListRowWidget (project_item); + } + ); + + Scratch.saved_state.changed["order-folders"].connect (() => { + order_folders (); + }); + + scrolled_window = new Gtk.ScrolledWindow () { + child = list_box, + }; + child = scrolled_window; + + list_box.activate_on_single_click = true; + list_box.row_activated.connect ((row) => { + var project_widget = (ProjectListRowWidget) (row.child); + project_widget.toggle_expanded (); + }); + } + + public async void restore_saved_state () { + foreach (unowned string path in settings.get_strv ("opened-folders")) { + yield add_new_project_folder (path, false, true); + } + } + + public void open_project_folder (File folder) { + warning ("open project folder"); + ProjectFolderItem? listitem; + if (is_existing_project_path (folder.path, out listitem)) { + warning ("exists - expanding"); + listitem.expand (); + return; //TODO Should we expand here? + } + + add_new_project_folder.begin (folder.path, true, false); + } + + public void collapse_all () { + iterate_children ((listitem) => { + // For now just collapse the top level + listitem.is_expanded = false; + return Granite.TreeList.ITERATE_CONTINUE; + }); + } + + public void order_folders () { + if (!Scratch.saved_state.get_boolean ("order-folders")) { + return; + } + + //TODO Sorting + } + + public void select_path (string path) { + //TODO write find_path + // find_path (null, path); + } + + public void unselect_file (GLib.File file) { // Needed? + //TODO Complete this + } + + public void unselect_all () { // Needed?? + //TODO Call unselect all on all children. + // tree_list.unselect_all (); + } + + public void collapse_other_projects (string active_project_path) { + iterate_children ((listitem) => { + if (listitem.path != active_project_path) { + listitem.collapse (); + activate_action ( + Scratch.MainWindow.ACTION_PREFIX + Scratch.MainWindow.ACTION_HIDE_PROJECT_DOCS, + "s", + listitem.path + ); + } else { + listitem.expand (); + activate_action ( + Scratch.MainWindow.ACTION_PREFIX + Scratch.MainWindow.ACTION_RESTORE_PROJECT_DOCS, + "s", + listitem.path + ); + } + + return Granite.TreeList.ITERATE_CONTINUE; + }); + } + + public void branch_actions (string path) { + // // Must only carry out branch actions on active project so switch if necessary. + // //TODO Warn before switching active project? + // var active_project = set_active_project (path); + // if (active_project == null || !active_project.is_git_repo) { + // Gdk.Display.get_default ().beep (); + // return; + // } + + // var dialog = new Dialogs.BranchActionDialog (active_project); + // dialog.response.connect ((res) => { + // if (res == Gtk.ResponseType.APPLY) { + // perform_branch_action (dialog); + // } + + // dialog.destroy (); + // }); + + // dialog.present (); + } + + private void perform_branch_action ( + Scratch.Dialogs.BranchActionDialog dialog + ) { + // switch (dialog.action) { + // case CHECKOUT: + // dialog.project.checkout_branch_ref (dialog.branch_ref); + // break; + // case COMMIT: + // break; + // case PUSH: + // break; + // case PULL: + // break; + // case MERGE: + // break; + // case DELETE: + // break; + // case CREATE: + // dialog.project.new_branch (dialog.new_branch_name); + // break; + // default: + // assert_not_reached (); + // } + } + + //TODO do we need to return anything? + // Call to find top level foldertree and then call find path on that. + private Object? find_path ( + string path, // File path to search fod + bool expand = false // Whether to expsnd to show found item + // GLib.File? target_file = null // Alternatively find this file + ) { +warning ("Project list find path %s", path); + // var target = target_file ?? GLib.File.new_for_path (path); + ProjectFolderItem? matched_project = null; + iterate_children ((listitem) => { + if (listitem.path == path) { + matched_project = listitem; + return Granite.TreeList.ITERATE_STOP; + } + + return Granite.TreeList.ITERATE_CONTINUE; + }); + + if (matched_project != null) { + return matched_project; + } + + FolderManagerItem? matched_item = null; + iterate_children ((listitem) => { + if (path.has_prefix (listitem.path)) { //TODO Ensure paths are compatible + matched_item = listitem.find_path (path, expand); + return Granite.TreeList.ITERATE_STOP; + } + + return Granite.TreeList.ITERATE_CONTINUE; + }); + + + return matched_item; + // if (listitem is FolderItem) { + // var folder = item as FolderItem; + // var folder_root = folder.file.file; + // if (folder_root.get_relative_path (target) == null) { + // return Granite.TreeList.ITERATE_CONTINUE; + // } + + // if (!folder.is_expanded) { + // if (expand) { + // folder.load_children (); //Synchronous + // folder.is_expanded = true; + // } else { + // return Granite.TreeList.ITERATE_CONTINUE; + // } + // } + + // var recurse_item = find_path (folder, path, expand, target); + // if (recurse_item != null) { + // matched_item = recurse_item; + // return Granite.TreeList.ITERATE_STOP; + // } + // } + + // return Granite.TreeList.ITERATE_CONTINUE; + // }); + + } + + //TODO Is this needed? + // // Which project any file is in + // public ProjectFolderItem? get_project_for_file (GLib.File file) { + // ProjectFolderItem? matched_project_item = null; + // iterate_children ((listitem) => { + // if (listitem.is_or_contains_file (file)) { + // matched_project_item = listitem; + // return Granite.TreeList.ITERATE_STOP; + // } + + // return Granite.TreeList.ITERATE_CONTINUE; + // }); + + // return matched_project_item; + // } + + public FolderManagerItem? expand_to_path (string path) { + var object = find_path (path, true); + if (object is FolderManagerItem) { + return (FolderManagerItem) object; + } + + return null; + } + + /* Do global search on project containing the file path supplied in parameter */ + public void search_global (string path, string? term = null) { + var item_for_path = expand_to_path (path); + if (item_for_path != null) { + var search_root = item_for_path.get_root_folder (); + if (search_root is ProjectFolderItem) { + GLib.File start_folder = (item_for_path is FolderItem) + ? item_for_path.file.file + : search_root.file.file; + + bool is_explicit = !(item_for_path is ProjectFolderItem); + search_root.global_search.begin (start_folder, term, is_explicit); + } + } + } + + // public void clear_badges () { + // tree_list.iterate_children (null, (child) => { + // if (child is ProjectFolderItem) { + // ((FolderItem)child).remove_all_badges (); + // } + + // return Granite.TreeList.ITERATE_CONTINUE; + // }); + // } + + // public void folder_item_update_hook (GLib.File source, GLib.File? dest, GLib.FileMonitorEvent event) { + // plugins.hook_folder_item_change (source, dest, event); + // } + + + public void item_deleted (ProjectFolderItem listitem) { + // Just remove it for now + remove_project_item (listitem); + } + + + // private void add_new_folder (SimpleAction action, Variant? param) { + // // Using "path" of parent folder from params, call `on_add_new (true)` on `FolderItem` + // var path = param.get_string (); + + // if (path == null || path == "") { + // return; + // } + + // var folder = find_path (null, path); + // if (folder == null) { + // return; + // } + + + // folder.on_add_new (true); + // } + + // private void add_new_file (SimpleAction action, Variant? param) { + // // Using "path" of parent folder from params, call `on_add_new (false)` on `FolderItem` + // var path = param != null ? param.get_string () : null; + + // if (path == null || path == "") { + // critical ("No path"); + // return; + // } + + // var folder = find_path (null, path) as FolderItem; + // if (folder == null) { + // return; + // } + + // folder.on_add_new (false); + // } + + // private void add_new_from_template (SimpleAction action, Variant? param) { + // // Using "path" of parent folder from params, call `on_add_new (false)` on `FolderItem` + // // var path = param.get_string (); + // string? parent_path = null, template_path = null; + // param.@get ("(ss)", out parent_path, out template_path); + + // //Do we need this check? + // if (parent_path == null || parent_path == "") { + // return; + // } + + // var folder = find_path (null, parent_path) as FolderItem; + // if (folder == null) { + // return; + // } + + // folder.on_add_template (template_path); + // } + + // private void action_launch_app_with_file_path (SimpleAction action, Variant? param) { + // var params = param.get_strv (); + // Utils.launch_app_with_file (params[1], params[0]); + // } + + // private void action_show_app_chooser (SimpleAction action, Variant? param) { + // var path = param.get_string (); + + // if (path == null || path == "") { + // return; + // } + + // var file = GLib.File.new_for_path (path); + // var dialog = new Gtk.AppChooserDialog (new Gtk.Window (), Gtk.DialogFlags.MODAL, file); + // dialog.deletable = false; + + // dialog.response.connect ((res) => { + // if (res == Gtk.ResponseType.OK) { + // var app_info = dialog.get_app_info (); + // if (app_info != null) { + // Utils.launch_app_with_file (app_info.get_id (), path); + // } + // } + + // dialog.destroy (); + // }); + + // dialog.show (); + // } + + // private void action_execute_contract_with_file_path (SimpleAction action, Variant? param) { + // var params = param.get_strv (); + // var path = params[0]; + // if (path == null || path == "") { + // return; + // } + + // var contract_name = params[1]; + // if (contract_name == null || contract_name == "") { + // return; + // } + + // Utils.execute_contract_with_file_path (path, contract_name); + // } + + + + + // private void action_delete (SimpleAction action, Variant? param) { + // var path = param.get_string (); + + // if (path == null || path == "") { + // return; + // } + + // var item = find_path (null, path); + // if (item != null) { + // var item_to_delete = item as Code.FolderManagerItem; + + // // Wait for ProjectFolderItem closed signal handle logic to run before moving item to trash + // if (item_to_delete is Code.ProjectFolderItem) { + // item_to_delete.closed.connect_after (() => { + // item_to_delete.trash (); + // }); + // item_to_delete.closed (); + // return; + // } + + // item_to_delete.trash (); + // } + // } + + // Only call when path is known to be a new project + private async void add_new_project_folder (string path, bool expand, bool restoring = false) { + var folder = new File (path); + if (!folder.is_valid_directory) { + warning ("Cannot open invalid directory."); + return; + } + + var add_file = folder.file; + // Need to deal with case where folder is parent or child of an existing project + var parents = new List (); + var children = new List (); + + iterate_children ((listitem) => { + // var item = (ProjectFolderItem) child; + if (add_file.get_relative_path (listitem.gfile) != null) { + debug ("Trying to add parent of existing project"); + children.append (listitem); + } else if (listitem.gfile.get_relative_path (add_file) != null) { + debug ("Trying to add child of existing project"); + parents.append (listitem); + } + + return Granite.TreeList.ITERATE_CONTINUE; + }); + + if (parents.length () > 0 || children.length () > 0) { + assert (parents.length () <= 1); + assert (parents.length () == 0 || children.length () == 0); + var dialog = new Scratch.Dialogs.CloseProjectsConfirmationDialog ( + (Scratch.MainWindow) get_root (), + parents.length (), + children.length () + ); + + var close_projects = false; + dialog.response.connect ((res) => { + if (res == Gtk.ResponseType.ACCEPT) { + close_projects = true; + } + + dialog.destroy (); + add_new_project_folder.callback (); + }); + + dialog.show (); + yield; + + if (close_projects) { + foreach (var listitem in parents) { + listitem.close (); + remove_project_item (listitem); + } + + foreach (var listitem in children) { + listitem.close (); + remove_project_item (listitem); + } + } else { + return; + } + } + + // Process any closed signals emitted before proceeding + Idle.add (() => { + var new_item = new ProjectFolderItem (folder, this); + list_store.append (new_item); + if (expand) { + new_item.expand (); + } + // rename_items_with_same_name (new_project); //TODO do this later + + // We do not want to rewrite settings while restoring from settings + // This interferes with fuzzy-finder plugins_manager + // See https://github.com/elementary/code/issues/1533 + if (!restoring) { + write_open_folders_setting (); + } + + add_new_project_folder.callback (); + return Source.REMOVE; + }); + + yield; + + // order_folders (); //TODO do later + } + + public bool is_existing_project_path (string path, out ProjectFolderItem? list_item) { + bool open = false; + list_item = null; + ProjectFolderItem? matched_item = null; + // Only iterate this model + iterate_children ((listitem) => { + if (path == listitem.path) { + open = true; + matched_item = listitem; + return Granite.TreeList.ITERATE_STOP; + } + + return Granite.TreeList.ITERATE_CONTINUE; + }); + + list_item = matched_item; + return open; + } + + private void write_open_folders_setting () { + string[] to_save = {}; + iterate_children ((listitem) => { + var saved = false; + //Do we need to de-duplicate? Not possible to open a project twice? + foreach (var saved_folder in to_save) { + if (listitem.path == saved_folder) { + saved = true; + break; + } + } + + if (!saved) { + to_save += listitem.path; + } + + return Granite.TreeList.ITERATE_CONTINUE; + }); + + settings.set_strv ("opened-folders", to_save); + } + + private void remove_project_item (ProjectFolderItem listitem) { + activate_action ( + CLOSE_PROJECT_DOCS_ACTION_NAME, + "s", + listitem.path + ); + + uint pos; + list_store.find (listitem, out pos); + list_store.remove (pos); + + } + private void action_close_project_folder (SimpleAction action, GLib.Variant? parameter) { + var path = parameter.get_string (); + if (path == null || path == "") { + return; + } + + iterate_children ((listitem) => { + if (listitem.path == path) { + listitem.close (); + remove_project_item (listitem); // OK to remove as we stop iterating + return Granite.TreeList.ITERATE_STOP; + } + + return Granite.TreeList.ITERATE_CONTINUE; + }); + } + + private void action_close_other_project_folders (SimpleAction action, GLib.Variant? parameter) { + var path = parameter.get_string (); + if (path == null || path == "") { + return; + } + + List to_remove = null; + iterate_children ((listitem) => { + if (listitem.path != path) { + listitem.close (); + to_remove.prepend (listitem); // Delay removal during iteration + } + + return Granite.TreeList.ITERATE_CONTINUE; + }); + + // List to_remove = null; + // tree_list.iterate_children (null, (child) => { + // var project_folder_item = (ProjectFolderItem) child; + // if (project_folder_item != folder_root) { + // activate_action ( + // MainWindow.ACTION_PREFIX + MainWindow.ACTION_CLOSE_PROJECT_DOCS, + // "s", + // project_folder_item.path + // ); + // to_remove.prepend (project_folder_item); + // git_manager.remove_project (project_folder_item); + // } + + // return Granite.TreeList.ITERATE_CONTINUE; + // }); + + foreach (ProjectFolderItem listitem in to_remove) { + remove_project_item (listitem); + } + } + + private void action_set_active_project (SimpleAction action, GLib.Variant? parameter) { + var path = parameter.get_string (); + if (path == null || path == "") { + return; + } + + set_active_project (path); + } + + //TODO Do we need both these functions??? + private ProjectFolderItem? set_active_project (string path) { + ProjectFolderItem? project_item; + is_existing_project_path (path, out project_item); + project_item.set_as_active_project (); + return project_item; + } + + // private void set_project_active (string path) { + // activate_action ( + // MainWindow.ACTION_PREFIX + MainWindow.ACTION_SET_ACTIVE_PROJECT, + // "s", + // path + // ); + // } + + delegate bool ProjectListIteratorCallback (ProjectFolderItem item); + // This iterates the model objects not the listrows + private void iterate_children (ProjectListIteratorCallback cb) { + ProjectFolderItem? item = null; + uint pos = 0; + do { + item = (ProjectFolderItem?) (list_store.get_object (pos++)); + } while (item != null && cb (item)); + } + + private void execute_contract_with_file_path (string path, string contract_name) { + var file = GLib.File.new_for_path (path); + + try { + var contracts = Granite.Services.ContractorProxy.get_contracts_for_file (file); + int length = contracts.size; + for (int i = 0; i < length; i++) { + var contract = contracts[i]; + if (contract.get_display_name () == contract_name) { + contract.execute_with_file (file); + break; + } + } + } catch (Error e) { + warning (e.message); + } + } + + private class ProjectListRowWidget : Gtk.Box { + public ProjectFolderItem project_item { get; construct; } + + private Gtk.Expander expander; + private bool expander_locked = false; + private Gtk.Revealer revealer; + + public ProjectListRowWidget (ProjectFolderItem project) { + Object ( + project_item: project + ); + + } + + construct { + var label = new Granite.HeaderLabel ("") { + label = project_item.name, + secondary_text = "Branch name goes here", + hexpand = true + }; + + expander = new Gtk.Expander ("") { + hexpand = false, + expanded = false, + sensitive = false, // We do not want expander interfering with listboxrow activate + valign = CENTER + }; + + var label_box = new Gtk.Box (HORIZONTAL, 6); + label_box.append (label); + label_box.append (expander); + + revealer = new Gtk.Revealer () { + child = project_item.folder_tree, + reveal_child = false + }; + + // We want expander arrow on right so separate expander and revealer + // We put the label and the expander in one box (horizontal) which also + // receives menu requests. The revealer with folder tree goes below + orientation = VERTICAL; + append (label_box); + append (revealer); + + project_item.bind_property ("is-expanded", revealer, "reveal-child", BIDIRECTIONAL | SYNC_CREATE); + revealer.bind_property ("reveal-child", expander, "expanded", BIDIRECTIONAL | SYNC_CREATE); + + var button_controller = new Gtk.GestureClick () { + propagation_phase = CAPTURE, + button = 0 + }; + + // Assume menu is static for now + var menu_model = project_item.get_context_menu (); + var menu = new Gtk.PopoverMenu.from_model (menu_model) { + position = BOTTOM + }; + + this.append (menu); + + label_box.add_controller (button_controller); + button_controller.pressed.connect ((n_press, x, y) => { + var event = button_controller.get_last_event (null); + if (event.triggers_context_menu ()) { // Only true for press events + menu.pointing_to = Gdk.Rectangle () {x = (int)x, y = (int)y, height = 1, width = 1}; + menu.popup (); + } + }); + } + + public void toggle_expanded () { + var show = !revealer.child_revealed; + revealer.reveal_child = show; + } + } + +} diff --git a/src/MainWindow.vala b/src/MainWindow.vala index 36e36bc74c..516c34af8d 100644 --- a/src/MainWindow.vala +++ b/src/MainWindow.vala @@ -39,17 +39,18 @@ namespace Scratch { } } - public Scratch.Widgets.DocumentView document_view; + public Scratch.Widgets.DocumentView document_view { get; private set; } - // Widgets + // These Widgets may be accessed by plugins so public public Scratch.HeaderBar toolbar; public Scratch.Widgets.SearchBar search_bar; + // Not available to plugins private Code.WelcomeView welcome_view; private Code.Terminal terminal; - private FolderManager.FileView folder_manager_view; + private Code.ProjectList folder_manager_view; private Scratch.Services.DocumentManager document_manager; // Plugins - private Scratch.Services.PluginsManager plugins; + public Scratch.Services.PluginsManager plugins { get; private set;} // Widgets for Plugins public Code.Sidebar sidebar; @@ -73,6 +74,7 @@ namespace Scratch { public const string ACTION_FIND_PREVIOUS = "action-find-previous"; public const string ACTION_FIND_GLOBAL = "action-find-global"; public const string ACTION_OPEN = "action-open"; + public const string ACTION_SHOW_OR_OPEN_DOCUMENT = "action-document"; public const string ACTION_OPEN_FOLDER = "action-open-folder"; public const string ACTION_OPEN_PROJECT = "action-open-project"; public const string ACTION_COLLAPSE_ALL_FOLDERS = "action-collapse-all-folders"; @@ -132,7 +134,8 @@ namespace Scratch { { ACTION_FIND_PREVIOUS, action_find_previous }, { ACTION_FIND_GLOBAL, action_find_global, "s" }, { ACTION_OPEN, action_open }, - { ACTION_OPEN_FOLDER, action_open_folder, "s" }, + { ACTION_SHOW_OR_OPEN_DOCUMENT,action_document, "s" }, + { ACTION_OPEN_FOLDER, action_open_folder_as_project, "s" }, { ACTION_OPEN_PROJECT, action_open_project }, { ACTION_COLLAPSE_ALL_FOLDERS, action_collapse_all_folders }, { ACTION_PREFERENCES, action_preferences }, @@ -463,36 +466,38 @@ namespace Scratch { sidebar = new Code.Sidebar (); - folder_manager_view = new FolderManager.FileView (plugins); + folder_manager_view = new Code.ProjectList (); sidebar.add_tab (folder_manager_view); + // sidebar.add_tab (new Granite.HeaderLabel ("Dummy Sidebar")); - folder_manager_view.file_activate.connect ((file) => { - // var file = new Scratch.FolderManager.File (a); - var doc = new Scratch.Services.Document (actions, file.file); + // folder_manager_view.file_activate.connect ((file) => { + // warning ("file activate %s", file.path); + // // var file = new Code.File (a); + // var doc = new Scratch.Services.Document (actions, file.file); - if (file.is_valid_textfile) { - open_document.begin (doc); - } else { - open_binary (file.file); - } - }); + // if (file.is_valid_textfile) { + // open_document.begin (doc); + // } else { + // open_binary (file.file); + // } + // }); - folder_manager_view.rename_request.connect ((file) => { - var allow = true; - foreach (var window in app.get_windows ()) { - var win = (MainWindow)window; - foreach (var doc in win.document_view.docs) { - if (doc.file.equal (file.file)) { - // Only allow sidebar to rename docs that are in sync with their file in - // all windows - allow = allow && !doc.locked && doc.saved; - } - } - } + // folder_manager_view.rename_request.connect ((file) => { + // var allow = true; + // foreach (var window in app.get_windows ()) { + // var win = (MainWindow)window; + // foreach (var doc in win.document_view.docs) { + // if (doc.file.equal (file.file)) { + // // Only allow sidebar to rename docs that are in sync with their file in + // // all windows + // allow = allow && !doc.locked && doc.saved; + // } + // } + // } - return allow; - }); + // return allow; + // }); terminal = new Code.Terminal () { visible = false @@ -546,6 +551,7 @@ namespace Scratch { hook_func (); }); + warning ("window realize sidebar is %s null", sidebar != null ? "NOT" : ""); hook_func (); // Allow secondary windows to have a different pane state @@ -582,7 +588,7 @@ namespace Scratch { document_view.tab_removed.connect ((doc) => { update_find_actions (); - folder_manager_view.unselect_file (doc.file); + // folder_manager_view.unselect_file (doc.file); }); document_view.document_change.connect ((doc) => { @@ -594,7 +600,7 @@ namespace Scratch { toolbar.set_document_focus (doc); - folder_manager_view.select_path (doc.file.get_path ()); + // folder_manager_view.select_path (doc.file.get_path ()); // Must follow setting focus document for editorconfig plug plugins.hook_document (doc); @@ -612,6 +618,7 @@ namespace Scratch { set_widgets_sensitive (false); } + private void open_binary (File file) { if (!file.query_exists ()) { return; @@ -647,18 +654,26 @@ namespace Scratch { if (file.query_exists ()) { var is_focused = uri == focused_uri; if (is_focused) { - focused_file = file; + focused_file = file; // remember focused file for focusing after all docs loaded } //TODO Check files valid (settings could have been manually altered) - var doc = new Scratch.Services.Document (actions, file); - if (doc.exists () || !doc.is_file_temporary) { - if (restore_override != null && (file.get_path () == restore_override.file.get_path ())) { - yield open_document_at_selected_range (doc, true, restore_override.range, true); + // var doc = new Scratch.Services.Document (actions, file); + // if (doc.exists () || !doc.is_file_temporary) { + if (restore_override != null && + (file.get_path () == restore_override.file.get_path ())) { + + // yield open_document_at_selected_range (doc, true, restore_override.range, true); + yield document_view.open_document (uri, true, 0, restore_override.range); was_restore_overriden = true; } else { - yield open_document (doc, was_restore_overriden ? false : is_focused, pos); + yield document_view.open_document ( + uri, + //Prevent focus being grabbed if there was an override + was_restore_overriden ? false : is_focused, + pos + ); } - } + // } } } } @@ -668,7 +683,7 @@ namespace Scratch { document_view.update_outline_visible (); restore_override = null; if (focused_file != null) { - folder_manager_view.expand_to_path (focused_file.get_path ()); + // folder_manager_view.expand_to_path (focused_file.get_path ()); } } @@ -743,31 +758,36 @@ namespace Scratch { return search_bar.search_occurrences > 0; } - public void open_folder (File folder) { - var foldermanager_file = new FolderManager.File (folder.get_path ()); - folder_manager_view.open_folder (foldermanager_file); - } - - public async void open_document (Scratch.Services.Document doc, - bool focus = true, - int cursor_position = 0) { - - FolderManager.ProjectFolderItem? project = folder_manager_view.get_project_for_file (doc.file); - doc.source_view.project = project; - yield document_view.open_document (doc, focus, cursor_position); - } - - public async void open_document_at_selected_range (Scratch.Services.Document doc, - bool focus = true, - SelectionRange range = SelectionRange.EMPTY, - bool is_override = false) { - if (restore_override != null && is_override == false) { - return; - } - - doc.source_view.project = folder_manager_view.get_project_for_file (doc.file); - yield document_view.open_document (doc, focus, 0, range); - } + public void open_folder_as_project (File folder) { + warning ("opening %s as project", folder.get_path ()); + var foldermanager_file = new Code.File (folder.get_path ()); + folder_manager_view.open_project_folder (foldermanager_file); + } + + // public async void open_document ( + // string path, + // bool focus = true, + // int cursor_position = 0 + // ) { + // // Code.ProjectFolderItem? project = folder_manager_view.get_project_for_file (doc.file); + // // doc.source_view.project = project; + // yield document_view.open_document (path, focus, cursor_position); + // } + + // public async void open_document_at_selected_range ( + // // Scratch.Services.Document doc, + // string doc_path, + // bool focus = true, + // SelectionRange range = SelectionRange.EMPTY, + // bool is_override = false + // ) { + // if (restore_override != null && is_override == false) { + // return; + // } + + // // doc.source_view.project = folder_manager_view.get_project_for_file (doc.file); + // yield document_view.open_document (doc_path, focus, 0, range); + // } // Close a document public void close_document (Scratch.Services.Document doc) { @@ -938,6 +958,7 @@ namespace Scratch { } private void action_open () { + warning ("action open"); var all_files_filter = new Gtk.FileFilter (); all_files_filter.set_filter_name (_("All files")); all_files_filter.add_pattern ("*"); @@ -966,19 +987,22 @@ namespace Scratch { } file_chooser.response.connect ((res) => { + warning ("got response %s", res.to_string ()); var files = file_chooser.get_files (); // Returns ListModel of GFile objects // var uris = file_chooser.get_uris (); file_chooser.destroy (); // Close now so it does not stay open during lengthy or failed loading if (res == Gtk.ResponseType.ACCEPT) { + warning ("ACCEPT"); var index = 0; var obj = files.get_item (index++); while (obj != null) { - var file = (GLib.File) obj; + var gfile = (GLib.File) obj; // Update last visited path - Utils.last_path = Path.get_dirname (file.get_uri ()); + var path = gfile.get_uri (); + Utils.last_path = Path.get_dirname (path); // Open the file - var doc = new Scratch.Services.Document (actions, file); - open_document.begin (doc); + // var doc = new Scratch.Services.Document (actions, gfile); + document_view.open_document.begin (path); obj = files.get_item (index++); } } @@ -987,6 +1011,11 @@ namespace Scratch { file_chooser.show (); } + // Shows document if alreadu open else open it + private void action_document (SimpleAction action, Variant? param) requires (param != null) { + document_view.open_document.begin (param.get_string ()); + } + private void action_open_in_new_window (SimpleAction action, Variant? param) { var path = param.get_string (); if (path == "") { @@ -995,9 +1024,9 @@ namespace Scratch { var new_window = new MainWindow (false); var file = File.new_for_path (path); - var doc = new Scratch.Services.Document (new_window.actions, file); + // var doc = new Scratch.Services.Document (new_window.actions, file); - new_window.open_document.begin (doc, true); + new_window.document_view.open_document.begin (path, true); } @@ -1006,6 +1035,7 @@ namespace Scratch { } private void choose_folder () { + warning ("choose folder"); var chooser = new Gtk.FileChooserNative ( "Select a folder.", this, Gtk.FileChooserAction.SELECT_FOLDER, _("_Open"), @@ -1013,17 +1043,16 @@ namespace Scratch { ); chooser.select_multiple = true; - chooser.response.connect ((res) => { var files = chooser.get_files (); chooser.destroy (); if (res == Gtk.ResponseType.ACCEPT) { -var index = 0; + var index = 0; var obj = files.get_item (index++); while (obj != null) { var file = (GLib.File) obj; - var foldermanager_file = new FolderManager.File (file.get_path ()); - folder_manager_view.open_folder (foldermanager_file); + var foldermanager_file = new Code.File (file.get_path ()); + folder_manager_view.open_project_folder (foldermanager_file); obj = files.get_item (index++); } } @@ -1032,12 +1061,12 @@ var index = 0; chooser.show (); } - private void action_open_folder (SimpleAction action, Variant? param) { + private void action_open_folder_as_project (SimpleAction action, Variant? param) { var path = param.get_string (); if (path == "") { choose_folder (); } else { - folder_manager_view.open_folder (new FolderManager.File (path)); + folder_manager_view.open_project_folder (new Code.File (path)); } } @@ -1080,7 +1109,7 @@ var index = 0; sidebar.cloning_in_progress = false; if (success) { - open_folder (workdir); + open_folder_as_project (workdir); if (this.is_active) { sidebar.notify_cloning_success (); } else { @@ -1252,8 +1281,7 @@ var index = 0; private void restore_project_docs (string project_path) { document_manager.take_restorable_paths (project_path).@foreach ((doc_path) => { - var doc = new Scratch.Services.Document (actions, File.new_for_path (doc_path)); - open_document.begin (doc); // Use this to reassociate project and document. + document_view.open_document.begin (doc_path); // Use this to reassociate project and document. return true; }); } @@ -1319,7 +1347,7 @@ var index = 0; } if (search_path != "") { - folder_manager_view.search_global (search_path, search_bar.entry_text); + // folder_manager_view.search_global (search_path, search_bar.entry_text); } else { // Fallback to standard search warning ("Unable to perform global search - search document instead"); @@ -1450,10 +1478,12 @@ var index = 0; } private void action_set_active_project (SimpleAction action, Variant? param) { +warning ("set active project"); var project_path = param.get_string (); - if (folder_manager_view.project_is_open (project_path)) { + Code.ProjectFolderItem? project = null; + if (folder_manager_view.is_existing_project_path (project_path, out project)) { git_manager.active_project_path = project_path; - folder_manager_view.collapse_other_projects (); + folder_manager_view.collapse_other_projects (project_path); //The opened folders are not changed so no need to update "opened-folders" setting } else { warning ("Attempt to set folder path %s which is not opened as active project ignored", project_path); @@ -1490,7 +1520,7 @@ var index = 0; } private void action_branch_actions (SimpleAction action, Variant? param) { - folder_manager_view.branch_actions (get_target_path_for_actions (param)); + // folder_manager_view.branch_actions (get_target_path_for_actions (param)); } private void action_move_tab_to_new_window () { diff --git a/src/Services/Document.vala b/src/Services/Document.vala index 67ae2c9a2d..6ad3cf5aa7 100644 --- a/src/Services/Document.vala +++ b/src/Services/Document.vala @@ -283,7 +283,7 @@ namespace Scratch.Services { var fc = new Gtk.EventControllerFocus (); source_view.add_controller (fc); fc.enter.connect (() => { - return_if_fail (!locked); + if (locked) { return; } // Not an error if (fc.is_focus) { if (!is_file_temporary) { check_undoable_actions (); diff --git a/src/Services/FileHandler.vala b/src/Services/FileHandler.vala index 6db312455e..e7d4db22c0 100644 --- a/src/Services/FileHandler.vala +++ b/src/Services/FileHandler.vala @@ -29,10 +29,10 @@ namespace Scratch.Services { } public class FileHandler : GLib.Object { - public static bool can_open_file (File file, out bool is_folder) { + public static bool can_open_file (GLib.File file, out bool is_folder) { is_folder = false; - if (file == null || file.get_path () == null) { - warning ("Ignoring file %s. Cannot determine path", + if (file == null || file.get_path () == null || !file.query_exists ()) { + warning ("Ignoring file %s. Cannot determine path or does not exist", file != null ? file.get_uri () ?? "null" : "null" ); diff --git a/src/Services/GitManager.vala b/src/Services/GitManager.vala index 9827d65832..bc1f3de56c 100644 --- a/src/Services/GitManager.vala +++ b/src/Services/GitManager.vala @@ -46,17 +46,27 @@ namespace Scratch.Services { construct { // Used to populate the ChooseProject popover in sorted order - project_liststore = new ListStore (typeof (FolderManager.ProjectFolderItem)); + //TODO This seems to duplicate the sidebar store - can we combine? + project_liststore = new ListStore (typeof (Code.ProjectFolderItem)); settings.bind ("active-project-path", this, "active-project-path", DEFAULT); } - public MonitoredRepository? add_project (FolderManager.ProjectFolderItem root_folder) { + public MonitoredRepository? add_project (Code.ProjectFolderItem root_folder) { var root_path = root_folder.path; MonitoredRepository? monitored_repo = null; uint position; if (project_liststore.find_with_equal_func ( root_folder, - (a, b) => { return ((FolderManager.Item) a).equal ((FolderManager.Item) b); }, + (oa, ob) => { + if (oa == null || ob == null) { + return false; + } + + var a = (Code.ProjectFolderItem) oa; + var b = (Code.ProjectFolderItem) ob; + + return str_equal (a.name, b.name); + }, out position )) { @@ -89,13 +99,13 @@ namespace Scratch.Services { } [CCode (instance_pos = -1)] - private int project_sort_func (FolderManager.ProjectFolderItem a, FolderManager.ProjectFolderItem b) { + private int project_sort_func (Code.ProjectFolderItem a, Code.ProjectFolderItem b) { GLib.File file_a = a.file.file; GLib.File file_b = b.file.file; return Path.get_basename (file_a.get_path ()).collate (Path.get_basename (file_b.get_path ())); } - public void remove_project (FolderManager.ProjectFolderItem root_folder) { + public void remove_project (Code.ProjectFolderItem root_folder) { var root_path = root_folder.file.file.get_path (); uint position; @@ -119,6 +129,7 @@ namespace Scratch.Services { build_path = build_file.get_path (); } else { warning ("build path not found %s", build_file.get_path ()); + // Just returns the project path } return build_path; diff --git a/src/Services/LocationJumpManager.vala b/src/Services/LocationJumpManager.vala index 4b4814472b..8cb3d8b495 100644 --- a/src/Services/LocationJumpManager.vala +++ b/src/Services/LocationJumpManager.vala @@ -7,9 +7,13 @@ namespace Scratch { public class LocationJumpManager : GLib.Object { + // This is created by the Application and manages parsing of commandline for a range + // and creating a restoreoverride object to be passed to the MainWindow + // for a particular file public GLib.File file { get; set; } public SelectionRange range { get; set; } + // Check the settings to see whether file was previously open and will be restored public bool has_override_target () { if (file == null) { return false; diff --git a/src/Services/PluginManager.vala b/src/Services/PluginManager.vala index 3324a47c45..348e19276a 100644 --- a/src/Services/PluginManager.vala +++ b/src/Services/PluginManager.vala @@ -30,17 +30,18 @@ public class Scratch.Services.Interface : GLib.Object { ); } - public Scratch.Services.Document open_file (File file) { - var doc = new Scratch.Services.Document (manager.window.actions, file); - manager.window.open_document.begin (doc); - return doc; - } + // public Scratch.Services.Document open_file (File file) { + // var doc = new Scratch.Services.Document (manager.window.actions, file); + // manager.window.open_document.begin (doc); + // return doc; + // } public void close_document (Scratch.Services.Document doc) { manager.window.close_document (doc); } } +//TODO Make singleton public class Scratch.Services.PluginsManager : GLib.Object { public signal void hook_window (Scratch.MainWindow window); public signal void hook_share_menu (GLib.MenuModel menu); @@ -95,6 +96,7 @@ public class Scratch.Services.PluginsManager : GLib.Object { // Connect managers signals to interface's signals this.hook_window.connect ((w) => { + warning ("manager calling hook window on interface"); plugin_iface.hook_window (w); }); diff --git a/src/Services/RestoreOverride.vala b/src/Services/RestoreOverride.vala index 08887df62d..0aa0bf9c4e 100644 --- a/src/Services/RestoreOverride.vala +++ b/src/Services/RestoreOverride.vala @@ -9,6 +9,7 @@ public class RestoreOverride : GLib.Object { public GLib.File file { get; construct; } public SelectionRange range { get; construct; } + // This is used to override the cursor position that may be restored from settings public RestoreOverride (GLib.File file, SelectionRange range) { Object ( file: file, diff --git a/src/SymbolPane/C/CtagsSymbol.vala b/src/SymbolPane/C/CtagsSymbol.vala index 1f7d82e415..3a08c0ee75 100644 --- a/src/SymbolPane/C/CtagsSymbol.vala +++ b/src/SymbolPane/C/CtagsSymbol.vala @@ -16,7 +16,7 @@ * */ -public class Scratch.Services.CtagsSymbol : Code.Widgets.SourceList.ExpandableItem, Scratch.Services.SymbolItem { +public class Scratch.Services.CtagsSymbol : Granite.TreeListItem, Scratch.Services.SymbolItem { public Scratch.Services.Document doc { get; construct set; } public SymbolType symbol_type { get; set; default = SymbolType.OTHER; } public int line { get; construct set; } @@ -25,16 +25,16 @@ public class Scratch.Services.CtagsSymbol : Code.Widgets.SourceList.ExpandableIt Scratch.Services.Document doc, string name, int line, - Icon? _icon, + string _icon_name, SymbolType? s_type = null) { Object ( doc: doc, - name: name, + text: name, line: line ); - icon = _icon; + icon_name = _icon_name; if (s_type != null) { symbol_type = s_type; } diff --git a/src/SymbolPane/C/CtagsSymbolIter.vala b/src/SymbolPane/C/CtagsSymbolIter.vala index 6b31923a4f..79bc1830e7 100644 --- a/src/SymbolPane/C/CtagsSymbolIter.vala +++ b/src/SymbolPane/C/CtagsSymbolIter.vala @@ -20,21 +20,21 @@ public class Scratch.Services.CtagsSymbolIter : Object { public string name { get; construct set; } public string parent { get; construct set; } public int line { get; construct set; } - public Icon? icon { get; construct set; } + public string? icon_name { get; construct set; } public SymbolType? symbol_type; public CtagsSymbolIter ( string name, string parent, int line, - Icon? icon, + string? _icon_name, SymbolType? s_type = SymbolType.OTHER) { Object ( name: name, parent: parent, line: line, - icon: icon + icon_name: _icon_name ); symbol_type = s_type; diff --git a/src/SymbolPane/C/CtagsSymbolOutline.vala b/src/SymbolPane/C/CtagsSymbolOutline.vala index e3c600ddad..2cf8f61c3a 100644 --- a/src/SymbolPane/C/CtagsSymbolOutline.vala +++ b/src/SymbolPane/C/CtagsSymbolOutline.vala @@ -40,13 +40,10 @@ public class Scratch.Services.CtagsSymbolOutline : Scratch.Services.SymbolOutlin SymbolType.CONSTANT }; } + construct { - store.item_selected.connect ((selected) => { - if (selected == null) { - return; - } - doc.goto (((CtagsSymbol)selected).line); - store.selected = null; + tree_list.item_activated.connect ((item) => { + doc.goto (((CtagsSymbol)item).line); }); } @@ -75,8 +72,8 @@ public class Scratch.Services.CtagsSymbolOutline : Scratch.Services.SymbolOutlin } private async void parse_output (GLib.Subprocess subprocess) { - var parent_dependent = new Gee.LinkedList (); - var new_root = new Code.Widgets.SourceList.ExpandableItem (_("Symbols")); + var symboliter_list = new Gee.LinkedList (); + var new_root = new Granite.TreeListItem () { text = _("Symbols") }; var datainput = new GLib.DataInputStream (subprocess.get_stdout_pipe ()); try { @@ -96,72 +93,72 @@ public class Scratch.Services.CtagsSymbolOutline : Scratch.Services.SymbolOutlin var type = parts[3]; int line = int.parse (parts[4].offset ("line:".length)); string? parent = null; - GLib.Icon? parent_icon = null; + string? parent_icon_name = 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_icon_name = "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_icon_name = "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_icon_name = "lang-enum"; parent_s_type = SymbolType.ENUM; } } - Icon? icon = null; + string? icon_name = null; SymbolType? s_type = null; switch (type) { case "class": - icon = new ThemedIcon ("lang-class"); + icon_name = "lang-class"; s_type = SymbolType.CLASS; break; case "struct": - icon = new ThemedIcon ("lang-struct"); + icon_name = "lang-struct"; s_type = SymbolType.STRUCT; break; case "field": case "member": case "variable": - icon = new ThemedIcon ("lang-property"); + icon_name = "lang-property"; s_type = SymbolType.PROPERTY; break; case "enum": case "enumerator": - icon = new ThemedIcon ("lang-enum"); + icon_name = "lang-enum"; s_type = SymbolType.ENUM; break; case "macro": case "constant": case "typedef": - icon = new ThemedIcon ("lang-constant"); + icon_name = "lang-constant"; s_type = SymbolType.CONSTANT; break; case "constructor": - icon = new ThemedIcon ("lang-constructor"); + icon_name = "lang-constructor"; s_type = SymbolType.CONSTRUCTOR; break; case "destructor": case "method": case "function": - icon = new ThemedIcon ("lang-method"); + icon_name = "lang-method"; s_type = SymbolType.METHOD; break; case "namespace": - icon = new ThemedIcon ("lang-namespace"); + icon_name = "lang-namespace"; s_type = SymbolType.NAMESPACE; break; case "package": break; case "property": - icon = new ThemedIcon ("lang-property"); + icon_name = "lang-property"; s_type = SymbolType.PROPERTY; break; } @@ -171,16 +168,16 @@ public class Scratch.Services.CtagsSymbolOutline : Scratch.Services.SymbolOutlin doc, name, line, - icon, + icon_name, s_type ); - new_root.add (s); + new_root.add_child (s); } else { - parent_dependent.add (new CtagsSymbolIter ( + symboliter_list.add (new CtagsSymbolIter ( name, parent, line, - parent_icon, + parent_icon_name, parent_s_type )); } @@ -191,9 +188,9 @@ public class Scratch.Services.CtagsSymbolOutline : Scratch.Services.SymbolOutlin } var found_something = true; - while (found_something && parent_dependent.size > 0) { + while (found_something && symboliter_list.size > 0) { found_something = false; - var iter = parent_dependent.iterator (); + var iter = symboliter_list.iterator (); while (iter.has_next ()) { iter.next (); var i = iter.get (); @@ -201,11 +198,11 @@ 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 ( + parent.add_child (new CtagsSymbol ( doc, i.name, i.line, - i.icon, + i.icon_name, i.symbol_type )); iter.remove (); @@ -214,16 +211,16 @@ public class Scratch.Services.CtagsSymbolOutline : Scratch.Services.SymbolOutlin var parent_parts = i.parent.split (":", 2); parent = find_existing (parent_parts[1], new_root); if (parent != null) { - parent.name = i.name; + parent.text = i.name; switch (parent_parts[0]) { case "class": - parent.icon = new ThemedIcon ("lang-class"); + parent.icon_name = "lang-class"; break; case "struct": - parent.icon = new ThemedIcon ("lang-struct"); + parent.icon_name = "lang-struct"; break; case "enum": - parent.icon = new ThemedIcon ("lang-enum"); + parent.icon_name = "lang-enum"; break; } iter.remove (); @@ -236,16 +233,16 @@ public class Scratch.Services.CtagsSymbolOutline : Scratch.Services.SymbolOutlin doc, i.parent, i.line - 1, - new ThemedIcon ("lang-enum"), + "lang-enum", SymbolType.ENUM ); - new_root.add (e); + new_root.add_child (e); - e.add (new CtagsSymbol ( + e.add_child (new CtagsSymbol ( doc, i.name, i.line, - i.icon, + i.icon_name, i.symbol_type )); iter.remove (); @@ -255,40 +252,39 @@ public class Scratch.Services.CtagsSymbolOutline : Scratch.Services.SymbolOutlin } // just add the rest - foreach (var symbol in parent_dependent) { - new_root.add (new CtagsSymbol ( + foreach (var symbol in symboliter_list) { + new_root.add_child (new CtagsSymbol ( doc, symbol.name, symbol.line, - symbol.icon, + symbol.icon_name, symbol.symbol_type )); } Idle.add (() => { - double adjustment_value = store.vadjustment.value; - store.root.clear (); - store.root.add (new_root); - store.root.expand_all (); - store.vadjustment.set_value (adjustment_value); + double adjustment_value = vadj.value; + tree_list.remove_all (); + tree_list.add_root_item (new_root); + // store.root.expand_all (); + vadj.set_value (adjustment_value); destroy_root (root); root = new_root; - add_tooltips (store.root); + add_tooltips (null); return false; }); } - protected override void add_tooltips (Code.Widgets.SourceList.ExpandableItem root) { - foreach (var parent in root.children) { - if (parent is Code.Widgets.SourceList.ExpandableItem) { - add_tooltip ((Code.Widgets.SourceList.ExpandableItem) parent); - } - } + protected override void add_tooltips (Granite.TreeListItem? root) { + tree_list.iterate_children (root, (child) => { + add_tooltip (child); + return Granite.TreeList.ITERATE_CONTINUE; + }); } - private void add_tooltip (Code.Widgets.SourceList.ExpandableItem parent) { + private void add_tooltip (Granite.TreeListItem parent) { if (parent is CtagsSymbol) { var item = ((CtagsSymbol)parent); var start = item.line; @@ -311,39 +307,45 @@ public class Scratch.Services.CtagsSymbolOutline : Scratch.Services.SymbolOutlin add_tooltips (parent); } - private void destroy_root (Code.Widgets.SourceList.ExpandableItem to_destroy) { - var children = iterate_children (to_destroy); - to_destroy.clear (); + private void destroy_root (Granite.TreeListItem to_destroy) { + var children = collect_children (to_destroy); + to_destroy.remove_all_children (); foreach (var item in children) { - item.clear (); + item.remove_all_children (); var parent = item.parent; if (parent != null) { - parent.remove (item); + parent.remove_child (item); } } } - private Gee.TreeSet iterate_children (Code.Widgets.SourceList.ExpandableItem parent) { + private Gee.TreeSet collect_children (Granite.TreeListItem? parent) { var result = new Gee.TreeSet (); - foreach (var child in parent.children) { - result.add_all (iterate_children ((CtagsSymbol)child)); - } + tree_list.iterate_children (parent, (child) => { + result.add_all (collect_children ((CtagsSymbol)child)); + return Granite.TreeList.ITERATE_CONTINUE; + }); return result; } - CtagsSymbol? find_existing (string name, Code.Widgets.SourceList.ExpandableItem parent) { + CtagsSymbol? find_existing (string name, Granite.TreeListItem parent) { CtagsSymbol match = null; - foreach (var child in parent.children) { - var child_symbol = child as CtagsSymbol; - if (child_symbol.name == name) { + // foreach (var child in parent.children) { + tree_list.iterate_children (parent, (child) => { + var child_symbol = (CtagsSymbol) child; + if (child_symbol.text == name) { match = child_symbol; - break; + return Granite.TreeList.ITERATE_STOP; } else { var res = find_existing (name, child_symbol); - if (res != null) - return res; + if (res != null) { + match = res; + return Granite.TreeList.ITERATE_STOP; + } } - } + + return Granite.TreeList.ITERATE_CONTINUE; + }); return match; } diff --git a/src/SymbolPane/SymbolOutline.vala b/src/SymbolPane/SymbolOutline.vala index ae9e7374ef..f898d7373f 100644 --- a/src/SymbolPane/SymbolOutline.vala +++ b/src/SymbolPane/SymbolOutline.vala @@ -59,26 +59,12 @@ public enum Scratch.Services.SymbolType { } } -public interface Scratch.Services.SymbolItem : Code.Widgets.SourceList.ExpandableItem { +public interface Scratch.Services.SymbolItem : Granite.TreeListItem { 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-"; - const uint SPINNER_DELAY_MSEC = 300; - 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 this; } public bool tool_box_sensitive { set { @@ -87,8 +73,18 @@ public class Scratch.Services.SymbolOutline : Gtk.Box { } } + public virtual void parse_symbols () {} + public virtual void add_tooltips (Granite.TreeListItem? root) {} + + protected static SymbolType[] filters; //Initialized by derived classes + protected Gee.HashMap checks; + protected Gtk.SearchEntry search_entry; + protected Granite.TreeList tree_list; + protected Gtk.ScrolledWindow scrolled_window; + protected Gtk.Adjustment vadj { get { return scrolled_window.vadjustment; }} + protected Granite.TreeListItem root; + protected Gtk.CssProvider source_list_style_provider; protected bool took_too_long; - private uint show_spinner_timeout_id = 0; protected void before_parse () { tool_box_sensitive = true; took_too_long = false; @@ -111,21 +107,33 @@ public class Scratch.Services.SymbolOutline : Gtk.Box { tool_box_sensitive = !took_too_long; } - public virtual void parse_symbols () {} - public virtual void add_tooltips (Code.Widgets.SourceList.ExpandableItem root) {} - + private const string ACTION_GROUP = "symbol"; + private const string ACTION_PREFIX = ACTION_GROUP + "."; + private const string ACTION_SELECT = "action-select"; + private const string ACTION_TOGGLE = "toggle-"; + private const uint SPINNER_DELAY_MSEC = 300; private Gtk.MenuButton filter_button; private Gtk.Spinner spinner; private Gtk.Stack stack; + private uint show_spinner_timeout_id = 0; + + SimpleActionGroup symbol_action_group; construct { + tree_list = new Granite.TreeList (); + scrolled_window = new Gtk.ScrolledWindow () { + child = tree_list + }; 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); + // store = new Granite.TreeList (); + // root = new Granite.TreeListItem () { + // text = _("Symbols") + // }; + + // tree_list.add_root_item (root); search_entry = new Gtk.SearchEntry () { placeholder_text = _("Find Symbol"), @@ -179,11 +187,11 @@ public class Scratch.Services.SymbolOutline : Gtk.Box { tool_box.append (search_entry); tool_box.append (stack); append (tool_box); - append (store); + append (scrolled_window); set_up_css (); realize.connect (() => { - store.set_filter_func (filter_func, false); + // store.set_filter_func (filter_func, false); search_entry.changed.connect (schedule_refilter); }); } @@ -229,13 +237,13 @@ public class Scratch.Services.SymbolOutline : Gtk.Box { // 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 (item is Granite.TreeListItem) { + var expandable = (Granite.TreeListItem) item; if (expandable.n_children > 0) { return true; } - return ((SymbolItem)item).name.contains (search_entry.text); + return ((SymbolItem)item).text.contains (search_entry.text); } return true; @@ -256,10 +264,10 @@ public class Scratch.Services.SymbolOutline : Gtk.Box { delay_refilter = false; return Source.CONTINUE; } else { - refilter_timeout_id = 0; - store.refilter (); + // refilter_timeout_id = 0; + // store.refilter (); // Ensure new visible items shown when filter removed - store.root.expand_all (true, true); + tree_list.expand_all (null); return Source.REMOVE; } }); diff --git a/src/SymbolPane/Vala/ValaComparisonHelper.vala b/src/SymbolPane/Vala/ValaComparisonHelper.vala index 4c38d32058..bbf9bc58a4 100644 --- a/src/SymbolPane/Vala/ValaComparisonHelper.vala +++ b/src/SymbolPane/Vala/ValaComparisonHelper.vala @@ -17,9 +17,9 @@ */ namespace Scratch.Services.ValaComparison { - int sort_function (Code.Widgets.SourceList.Item str1, Code.Widgets.SourceList.Item str2) { + int sort_function (Granite.TreeListItem str1, Granite.TreeListItem str2) { if (!(str1 is ValaSymbolItem && str2 is ValaSymbolItem)) - return str1.name.collate (str2.name); + return str1.text.collate (str2.text); var a = (ValaSymbolItem) str1; var b = (ValaSymbolItem) str2; var sa = a.symbol; @@ -52,7 +52,7 @@ namespace Scratch.Services.ValaComparison { return (compare_signal ((Vala.Signal) sa, sb)); else if (sa is Vala.Struct) return (compare_struct ((Vala.Struct) sa, sb)); - return str1.name.collate (str2.name); + return str1.text.collate (str2.text); } int compare_class (Vala.Class s, Vala.Symbol s2) { diff --git a/src/SymbolPane/Vala/ValaSymbolItem.vala b/src/SymbolPane/Vala/ValaSymbolItem.vala index ac3bb20f70..13dfd02282 100644 --- a/src/SymbolPane/Vala/ValaSymbolItem.vala +++ b/src/SymbolPane/Vala/ValaSymbolItem.vala @@ -16,7 +16,7 @@ * */ -public class Scratch.Services.ValaSymbolItem : Code.Widgets.SourceList.ExpandableItem, Code.Widgets.SourceListSortable, Scratch.Services.SymbolItem { +public class Scratch.Services.ValaSymbolItem : Granite.TreeListItem, Scratch.Services.SymbolItem { public Vala.Symbol symbol { get; construct; } public SymbolType symbol_type { get; set; default = SymbolType.OTHER; } public ValaSymbolItem (Vala.Symbol symbol, string _tooltip) { @@ -30,82 +30,86 @@ public class Scratch.Services.ValaSymbolItem : Code.Widgets.SourceList.Expandabl if (symbol is Vala.CreationMethod) { var klass = ((Vala.CreationMethod)symbol).class_name; if (symbol.name == ".new") { - name = klass; + text = klass; } else { - name = "%s.%s".printf (klass, symbol.name); + text = "%s.%s".printf (klass, symbol.name); } } else { - name = symbol.name; + text = symbol.name; } if (symbol is Vala.Struct) { - icon = new ThemedIcon ("lang-struct"); + icon_name = "lang-struct"; + // icon_name = "lang-struct"); symbol_type = SymbolType.STRUCT; } else if (symbol is Vala.Class) { if (((Vala.Class) symbol).is_abstract) { - icon = new ThemedIcon ("lang-class-abstract"); + // icon_name = "lang-class-abstract"); + icon_name = "lang-class-abstract"; } else { - icon = new ThemedIcon ("lang-class"); + icon_name = "lang-class"; } symbol_type = SymbolType.CLASS; } else if (symbol is Vala.Constant) { - icon = new ThemedIcon ("lang-constant"); + icon_name = "lang-constant"; symbol_type = SymbolType.CONSTANT; } else if (symbol is Vala.Enum) { - icon = new ThemedIcon ("lang-enum"); + icon_name = "lang-enum"; symbol_type = SymbolType.ENUM; } else if (symbol is Vala.Field) { - icon = new ThemedIcon ("lang-property"); + icon_name = "lang-property"; symbol_type = SymbolType.PROPERTY; } else if (symbol is Vala.Interface) { - icon = new ThemedIcon ("lang-interface"); + icon_name = "lang-interface"; symbol_type = SymbolType.INTERFACE; } else if (symbol is Vala.Property) { if (((Vala.Property) symbol).is_abstract) { - icon = new ThemedIcon ("lang-property-abstract"); + icon_name = "lang-property-abstract"; } else if (((Vala.Property) symbol).is_virtual) { - icon = new ThemedIcon ("lang-property-virtual"); + icon_name = "lang-property-virtual"; } else { - icon = new ThemedIcon ("lang-property"); + icon_name = "lang-property"; } symbol_type = SymbolType.PROPERTY; } else if (symbol is Vala.Signal) { - icon = new ThemedIcon ("lang-signal"); + icon_name = "lang-signal"; symbol_type = SymbolType.SIGNAL; } else if (symbol is Vala.CreationMethod) { - icon = new ThemedIcon ("lang-constructor"); + icon_name = "lang-constructor"; symbol_type = SymbolType.CONSTRUCTOR; } else if (symbol is Vala.Method) { if (((Vala.Method) symbol).is_abstract) { - icon = new ThemedIcon ("lang-method-abstract"); + icon_name = "lang-method-abstract"; } else if (((Vala.Method) symbol).is_virtual) { - icon = new ThemedIcon ("lang-method-virtual"); + icon_name = "lang-method-virtual"; } else if (((Vala.Method) symbol).binding == Vala.MemberBinding.STATIC) { - icon = new ThemedIcon ("lang-method-static"); + icon_name = "lang-method-static"; } else { - icon = new ThemedIcon ("lang-method"); + icon_name = "lang-method"; } symbol_type = SymbolType.METHOD; } else if (symbol is Vala.Namespace) { - icon = new ThemedIcon ("lang-namespace"); + icon_name = "lang-namespace"; symbol_type = SymbolType.NAMESPACE; } else if (symbol is Vala.ErrorDomain) { - icon = new ThemedIcon ("lang-errordomain"); + icon_name = "lang-errordomain"; } else if (symbol is Vala.Delegate) { - icon = new ThemedIcon ("lang-delegate"); + icon_name = "lang-delegate"; } else { warning (symbol.type_name); } + + warning ("new symbol text %s", text); } ~ValaSymbolItem () { debug ("Destroy Vala symbol"); } - public int compare (Code.Widgets.SourceList.Item a, Code.Widgets.SourceList.Item b) { + public int compare (Granite.TreeListItem a, Granite.TreeListItem b) { return ValaComparison.sort_function (a, b); } diff --git a/src/SymbolPane/Vala/ValaSymbolOutline.vala b/src/SymbolPane/Vala/ValaSymbolOutline.vala index 4399302ccd..377a09a9e3 100644 --- a/src/SymbolPane/Vala/ValaSymbolOutline.vala +++ b/src/SymbolPane/Vala/ValaSymbolOutline.vala @@ -48,13 +48,8 @@ public class Scratch.Services.ValaSymbolOutline : Scratch.Services.SymbolOutline parser = new Vala.Parser (); resolver = new Code.Plugins.ValaSymbolResolver (); - store.item_selected.connect ((selected) => { - if (selected == null) { - return; - } - - doc.goto (((ValaSymbolItem)selected).symbol.source_reference.begin.line); - store.selected = null; + tree_list.item_activated.connect ((item) => { + doc.goto (((ValaSymbolItem)item).symbol.source_reference.begin.line); }); doc.doc_closed.connect (doc_closed); @@ -109,33 +104,32 @@ public class Scratch.Services.ValaSymbolOutline : Scratch.Services.SymbolOutline if (!cancellable.is_cancelled () || took_too_long) { Idle.add (() => { - double adjustment_value = store.vadjustment.value; - var root_children = store.root.children; // Keep reference to children for later destruction - store.root.clear (); // This does not destroy children but disconnects signals - avoids terminal warnings - foreach (var child in root_children) { // Destroy items after clearing list to avoid memory leak - destroy_all_children ((Code.Widgets.SourceList.ExpandableItem)child); - } - - if (took_too_long) { - var warning_item = new Code.Widgets.SourceList.Item () { - icon = new ThemedIcon ("dialog-warning"), - markup = "%s".printf (_("Too Many Symbols")), - tooltip = _("%s contains too many Vala symbols.\nParsing and showing them took too long.").printf (doc.file.get_basename ()), - selectable = false - }; - - store.root.add (warning_item); - } else { - store.root.add (new_root); - } - - store.root.expand_all (); - add_tooltips (store.root); - store.vadjustment.set_value (adjustment_value); + double adjustment_value = vadj.value; + // var root_children = root.children; // Keep reference to children for later destruction + // tree_list.clear (); // This does not destroy children but disconnects signals - avoids terminal warnings + // foreach (var child in root_children) { // Destroy items after clearing list to avoid memory leak + // destroy_all_children ((Granite.TreeListItem)child); + // } + + tree_list.remove_all (); + // if (took_too_long) { + // var warning_item = new Code.Widgets.SourceList.Item () { + // icon = new ThemedIcon ("dialog-warning"), + // markup = "%s".printf (_("Too Many Symbols")), + // tooltip = _("%s contains too many Vala symbols.\nParsing and showing them took too long.").printf (doc.file.get_basename ()), + // selectable = false + // }; + + // root.add (warning_item); + // } else { + tree_list.add_root_item (new_root); + // } + + tree_list.expand_all (null); + add_tooltips (root); + vadj.set_value (adjustment_value); return Source.REMOVE; }); - } else { - destroy_all_children (new_root); } after_parse (); @@ -143,15 +137,14 @@ public class Scratch.Services.ValaSymbolOutline : Scratch.Services.SymbolOutline }); } - protected override void add_tooltips (Code.Widgets.SourceList.ExpandableItem root) { - foreach (var parent in root.children) { - if (parent is Code.Widgets.SourceList.ExpandableItem) { - add_tooltip ((Code.Widgets.SourceList.ExpandableItem) parent); - } - } + protected override void add_tooltips (Granite.TreeListItem? root = null) { + tree_list.iterate_children (root, (child) => { + add_tooltip ((Granite.TreeListItem) parent); + return Granite.TreeList.ITERATE_CONTINUE; + }); } - private void add_tooltip (Code.Widgets.SourceList.ExpandableItem parent) { + private void add_tooltip (Granite.TreeListItem parent) { if (parent is ValaSymbolItem) { var item = ((ValaSymbolItem)parent); var symbol = item.symbol; @@ -169,28 +162,29 @@ public class Scratch.Services.ValaSymbolOutline : Scratch.Services.SymbolOutline add_tooltips (parent); } - private void destroy_all_children (Code.Widgets.SourceList.ExpandableItem parent) { - foreach (var child in parent.children) { - remove (child, parent); - } - } + // private void destroy_all_children (Granite.TreeListItem parent) { + // foreach (var child in parent.children) { + // remove (child, parent); + // } + // } - private new void remove (Code.Widgets.SourceList.Item item, Code.Widgets.SourceList.ExpandableItem parent) { - if (item is Code.Widgets.SourceList.ExpandableItem) { - destroy_all_children ((Code.Widgets.SourceList.ExpandableItem)item); - } + // private new void remove (Code.Widgets.SourceList.Item item, Granite.TreeListItem parent) { + // if (item is Granite.TreeListItem) { + // destroy_all_children ((Granite.TreeListItem)item); + // } - parent.remove (item); - } + // parent.remove (item); + // } // Called from separate thread - private Code.Widgets.SourceList.ExpandableItem construct_tree (GLib.Cancellable cancellable) { + private Granite.TreeListItem construct_tree (GLib.Cancellable cancellable) { + warning ("construct tree"); var fields = resolver.get_properties_fields (); var symbols = resolver.get_symbols (); // Remove fake fields created by the vala parser. symbols.remove_all (fields); - var new_root = new Code.Widgets.SourceList.ExpandableItem (_("Symbols")); + var new_root = new Granite.TreeListItem () { text = _("Construct Tree Symbols") }; new_root.tooltip = _("Vala symbols found in %s").printf (doc.file.get_basename ()); foreach (var symbol in symbols) { if (cancellable.is_cancelled ()) @@ -211,48 +205,55 @@ public class Scratch.Services.ValaSymbolOutline : Scratch.Services.SymbolOutline private ValaSymbolItem construct_child ( Vala.Symbol symbol, - Code.Widgets.SourceList.ExpandableItem given_parent, + Granite.TreeListItem given_parent, GLib.Cancellable cancellable ) { - - Code.Widgets.SourceList.ExpandableItem parent; - if (symbol.scope.parent_scope.owner.name == null) +warning ("construct child"); + Granite.TreeListItem parent; + if (symbol.scope.parent_scope.owner.name == null) { parent = given_parent; - else + } else { parent = find_existing (symbol.scope.parent_scope.owner, given_parent, cancellable); + } if (parent == null) { parent = construct_child (symbol.scope.parent_scope.owner, given_parent, cancellable); } - var tree_child = new ValaSymbolItem ( symbol, "" ); - parent.add (tree_child); + + parent.add_child (tree_child); return tree_child; } - ValaSymbolItem? find_existing (Vala.Symbol symbol, Code.Widgets.SourceList.ExpandableItem parent, GLib.Cancellable cancellable) { + ValaSymbolItem? find_existing (Vala.Symbol symbol, Granite.TreeListItem parent, GLib.Cancellable cancellable) { ValaSymbolItem match = null; - foreach (var _child in parent.children) { - if (cancellable.is_cancelled ()) - break; + tree_list.iterate_children (null, (_child) => { + if (cancellable.is_cancelled ()) { + return Granite.TreeList.ITERATE_STOP; + } var child = _child as ValaSymbolItem; - if (child == null) - continue; + if (child == null) { + return Granite.TreeList.ITERATE_CONTINUE; + } if (child.symbol == symbol) { match = child; - break; + return Granite.TreeList.ITERATE_STOP; } else { var res = find_existing (symbol, child, cancellable); - if (res != null) - return res; + if (res != null) { + match = res; + return Granite.TreeList.ITERATE_STOP; + } } - } + + return Granite.TreeList.ITERATE_CONTINUE; + }); return match; } diff --git a/src/Utils.vala b/src/Utils.vala index 9307da5a23..ee4570659e 100644 --- a/src/Utils.vala +++ b/src/Utils.vala @@ -201,62 +201,6 @@ namespace Scratch.Utils { return false; } - public GLib.Menu? create_executable_app_items_for_file (GLib.File file, string file_type) { - var scratch_app = (Scratch.Application) (GLib.Application.get_default ()); - var this_id = scratch_app.application_id + ".desktop"; - var menu = new GLib.Menu (); - - if (scratch_app.is_running_in_flatpak) { - var menu_item = new MenuItem ( - ///TRANSLATORS '%s' represents the quoted basename of a uri to be opened with the default app - _("Show '%s' with default app").printf (file.get_basename ()), - GLib.Action.print_detailed_name ( - Scratch.FolderManager.FileView.ACTION_PREFIX - + Scratch.FolderManager.FileView.ACTION_LAUNCH_APP_WITH_FILE_PATH, - new GLib.Variant.array ( - GLib.VariantType.STRING, - { file.get_path (), "" } - ) - ) - ); - menu.append_item (menu_item); - } else { - List external_apps = null; - if (file_type == "") { - var files_appinfo = AppInfo.get_default_for_type ("inode/directory", true); - external_apps.prepend (files_appinfo); - } else { - external_apps = GLib.AppInfo.get_all_for_type (file_type); - external_apps.sort ((a, b) => { - return a.get_name ().collate (b.get_name ()); - }); - } - - foreach (AppInfo app_info in external_apps) { - string app_id = app_info.get_id (); - if (app_id == this_id) { - continue; - } - - var menu_item = new MenuItem ( - app_info.get_name (), - GLib.Action.print_detailed_name ( - Scratch.FolderManager.FileView.ACTION_PREFIX - + Scratch.FolderManager.FileView.ACTION_LAUNCH_APP_WITH_FILE_PATH, - new GLib.Variant.array ( - GLib.VariantType.STRING, - { file.get_path (), app_id } - ) - ) - ); - menu_item.set_icon (app_info.get_icon ()); - menu.append_item (menu_item); - } - } - - return menu; - } - public void launch_app_with_file (string app_id, string path) { var scratch_app = (Scratch.Application) (GLib.Application.get_default ()); if (scratch_app.is_running_in_flatpak || app_id == "") { @@ -281,51 +225,51 @@ namespace Scratch.Utils { } } - public GLib.Menu create_contract_items_for_file (GLib.File file) { - var menu = new GLib.Menu (); - - try { - var contracts = Granite.Services.ContractorProxy.get_contracts_for_file (file); - foreach (var contract in contracts) { - string contract_name = contract.get_display_name (); - var menu_item = new GLib.MenuItem ( - contract_name, - GLib.Action.print_detailed_name ( - Scratch.FolderManager.FileView.ACTION_PREFIX - + Scratch.FolderManager.FileView.ACTION_EXECUTE_CONTRACT_WITH_FILE_PATH, - new GLib.Variant.array ( - GLib.VariantType.STRING, - { file.get_path (), contract_name } - ) - ) - ); - - menu.append_item (menu_item); - } - } catch (Error e) { - warning (e.message); - } - - return menu; - } - - public void execute_contract_with_file_path (string path, string contract_name) { - var file = GLib.File.new_for_path (path); - - try { - var contracts = Granite.Services.ContractorProxy.get_contracts_for_file (file); - int length = contracts.size; - for (int i = 0; i < length; i++) { - var contract = contracts[i]; - if (contract.get_display_name () == contract_name) { - contract.execute_with_file (file); - break; - } - } - } catch (Error e) { - warning (e.message); - } - } + // public GLib.Menu create_contract_items_for_file (GLib.File file) { + // var menu = new GLib.Menu (); + + // try { + // var contracts = Granite.Services.ContractorProxy.get_contracts_for_file (file); + // foreach (var contract in contracts) { + // string contract_name = contract.get_display_name (); + // var menu_item = new GLib.MenuItem ( + // contract_name, + // GLib.Action.print_detailed_name ( + // Code.FolderTree.ACTION_PREFIX + // + Code.ProjectList.ACTION_EXECUTE_CONTRACT_WITH_FILE_PATH, + // new GLib.Variant.array ( + // GLib.VariantType.STRING, + // { file.get_path (), contract_name } + // ) + // ) + // ); + + // menu.append_item (menu_item); + // } + // } catch (Error e) { + // warning (e.message); + // } + + // return menu; + // } + + // public void execute_contract_with_file_path (string path, string contract_name) { + // var file = GLib.File.new_for_path (path); + + // try { + // var contracts = Granite.Services.ContractorProxy.get_contracts_for_file (file); + // int length = contracts.size; + // for (int i = 0; i < length; i++) { + // var contract = contracts[i]; + // if (contract.get_display_name () == contract_name) { + // contract.execute_with_file (file); + // break; + // } + // } + // } catch (Error e) { + // warning (e.message); + // } + // } public string get_accel_for_action (string detailed_action_name) { var app_instance = (Gtk.Application) GLib.Application.get_default (); diff --git a/src/Widgets/ChooseProjectButton.vala b/src/Widgets/ChooseProjectButton.vala index 70ae55a05e..4cbf30c4a2 100644 --- a/src/Widgets/ChooseProjectButton.vala +++ b/src/Widgets/ChooseProjectButton.vala @@ -51,6 +51,7 @@ public class Code.ChooseProjectButton : Gtk.Box { box.append (label_widget); box.append (cloning_spinner); menu_button = new Gtk.MenuButton () { + hexpand = true, child = box }; menu_button.set_parent (this); @@ -115,8 +116,8 @@ public class Code.ChooseProjectButton : Gtk.Box { var src = git_manager.project_liststore; for (int index = 0; index < src.n_items; index++) { var item = src.get_object (index); - if (item is Scratch.FolderManager.ProjectFolderItem) { - var row = create_project_row ((Scratch.FolderManager.ProjectFolderItem)item); + if (item is Code.ProjectFolderItem) { + var row = create_project_row ((Code.ProjectFolderItem)item); project_listbox.insert (row, index); } } @@ -125,8 +126,8 @@ public class Code.ChooseProjectButton : Gtk.Box { project_listbox.remove_all (); for (int index = (int)pos; index < pos + n_added; index++) { var item = src.get_object (index); - if (item is Scratch.FolderManager.ProjectFolderItem) { - var row = create_project_row ((Scratch.FolderManager.ProjectFolderItem)item); + if (item is Code.ProjectFolderItem) { + var row = create_project_row ((Code.ProjectFolderItem)item); project_listbox.insert (row, index); } } @@ -161,7 +162,7 @@ public class Code.ChooseProjectButton : Gtk.Box { } } - private Gtk.Widget create_project_row (Scratch.FolderManager.ProjectFolderItem project_folder) { + private Gtk.Widget create_project_row (Code.ProjectFolderItem project_folder) { var project_path = project_folder.file.file.get_path (); return new ProjectRow (project_path); } diff --git a/src/Widgets/DocumentView.vala b/src/Widgets/DocumentView.vala index 828e3981a9..7618e57d42 100644 --- a/src/Widgets/DocumentView.vala +++ b/src/Widgets/DocumentView.vala @@ -314,43 +314,46 @@ public class Scratch.Widgets.DocumentView : Gtk.Box { } public void new_document () { - var file = File.new_for_path (unsaved_file_path_builder ()); + var path = unsaved_file_path_builder (); + var file = File.new_for_path (path); try { file.create (FileCreateFlags.PRIVATE); - - var doc = new Services.Document (window.actions, file); // Must open document in order to unlock it. - open_document.begin (doc); + open_document.begin (path); } catch (Error e) { critical (e.message); } } public void new_document_from_clipboard (string clipboard) { - var file = File.new_for_path (unsaved_file_path_builder ()); + var path = unsaved_file_path_builder (); + var file = File.new_for_path (path); // Set clipboard content try { file.create (FileCreateFlags.PRIVATE); file.replace_contents (clipboard.data, null, false, 0, null); - var doc = new Services.Document (window.actions, file); - - open_document.begin (doc); - - + open_document.begin (path); } catch (Error e) { critical ("Cannot insert clipboard: %s", clipboard); } } - public async void open_document (Services.Document doc, bool focus = true, int cursor_position = 0, SelectionRange range = SelectionRange.EMPTY) { + // public async void open_document (Services.Document doc, bool focus = true, int cursor_position = 0, SelectionRange range = SelectionRange.EMPTY) { + public async void open_document ( + string doc_path, + bool focus = true, + int cursor_position = 0, + SelectionRange range = SelectionRange.EMPTY + ) { for (int n = 0; n <= docs.length (); n++) { var nth_doc = docs.nth_data (n); if (nth_doc == null) { continue; } - if (nth_doc.file != null && nth_doc.file.get_uri () == doc.file.get_uri ()) { + // if (nth_doc.file != null && nth_doc.file.get_uri () == doc.file.get_uri ()) { + if (nth_doc.file != null && nth_doc.file.get_uri () == doc_path) { if (focus) { current_document = nth_doc; } @@ -369,6 +372,10 @@ public class Scratch.Widgets.DocumentView : Gtk.Box { } } + var doc = new Scratch.Services.Document ( + window.actions, + GLib.File.new_for_path (doc_path) + ); insert_document (doc, (int) docs.length ()); if (focus) { current_document = doc; @@ -538,8 +545,7 @@ public class Scratch.Widgets.DocumentView : Gtk.Box { public void restore_closed_tab (string path) { var file = File.new_for_path (path); - var doc = new Services.Document (window.actions, file); - open_document.begin (doc); + open_document.begin (path); var menu = (Menu) tab_history_button.menu_model; for (var i = 0; i < menu.get_n_items (); i++) { diff --git a/src/Widgets/GitGutterRenderer.vala b/src/Widgets/GitGutterRenderer.vala index 3e3f12ea5a..419af28f76 100644 --- a/src/Widgets/GitGutterRenderer.vala +++ b/src/Widgets/GitGutterRenderer.vala @@ -17,7 +17,7 @@ public class Scratch.Widgets.GitGutterRenderer : GtkSource.GutterRenderer { public Gee.HashMap line_status_map; public Gee.HashMap status_color_map; - public FolderManager.ProjectFolderItem? project { get; set; default = null; } + public Code.ProjectFolderItem? project { get; set; default = null; } static construct { fallback_scheme = GtkSource.StyleSchemeManager.get_default ().get_scheme ("classic"); // We can assume this always exists @@ -62,23 +62,22 @@ public class Scratch.Widgets.GitGutterRenderer : GtkSource.GutterRenderer { status_color_map.set (status, color); } - public override void snapshot_line (Gtk.Snapshot snapshot, GtkSource.GutterLines lines, uint line) { - warning ("snapshot_line"); - //Gutter and diff lines numbers start at one, source lines start at 0 - var gutter_line_no = (int) lines.get_first (); - Gdk.RGBA? color; - if (line_status_map.has_key (gutter_line_no)) { - color = status_color_map[line_status_map[gutter_line_no]]; - } else { - color = status_color_map [Services.VCStatus.NONE]; - } + // public override void snapshot_line (Gtk.Snapshot snapshot, GtkSource.GutterLines lines, uint line) { + // //Gutter and diff lines numbers start at one, source lines start at 0 + // var gutter_line_no = (int) lines.get_first (); + // Gdk.RGBA? color; + // if (line_status_map.has_key (gutter_line_no)) { + // color = status_color_map[line_status_map[gutter_line_no]]; + // } else { + // color = status_color_map [Services.VCStatus.NONE]; + // } - var rect = Graphene.Rect () { - origin = Graphene.Point () { x = 0.0f, y = 0.0f }, - size = Graphene.Size () { width = 6.0f , height = 12.0f } - }; - snapshot.append_color (color, rect); - } + // var rect = Graphene.Rect () { + // origin = Graphene.Point () { x = 0.0f, y = 0.0f }, + // size = Graphene.Size () { width = 6.0f , height = 12.0f } + // }; + // snapshot.append_color (color, rect); + // } // public override void draw (Cairo.Context cr, // Gdk.Rectangle bg, diff --git a/src/Widgets/SearchBar.vala b/src/Widgets/SearchBar.vala index 8f5ddd06ba..0ec50bf3a8 100644 --- a/src/Widgets/SearchBar.vala +++ b/src/Widgets/SearchBar.vala @@ -26,19 +26,14 @@ public enum Scratch.CaseSensitiveMode { } namespace Scratch.Widgets { - public class SearchBar : Gtk.Box { //TODO In Gtk4 use a BinLayout Widget + public class SearchBar : Granite.Bin { public weak MainWindow window { get; construct; } + private Gtk.Button tool_arrow_up; private Gtk.Button tool_arrow_down; - - /** - * Is the search cyclic? e.g., when you are at the bottom, if you press - * "Down", it will go at the start of the file to search for the content - * of the search entry. - **/ private Granite.SwitchModelButton cycle_search_button ; private Gtk.ComboBoxText case_sensitive_search_button; private Granite.SwitchModelButton regex_search_button; @@ -116,15 +111,13 @@ namespace Scratch.Widgets { } construct { - this.orientation = HORIZONTAL; - search_entry = new Gtk.SearchEntry () { hexpand = true, placeholder_text = _("Find"), }; entry_for_search = new Gtk.Entry (); - entry_for_search.set_parent (search_entry); + // entry_for_search.set_parent (search_entry); search_entry.set_key_capture_widget (entry_for_search); search_occurence_count_label = new Gtk.Label (_("No Results")); @@ -214,7 +207,8 @@ namespace Scratch.Widgets { margin_start = 6 }; search_box.add_css_class (Granite.STYLE_CLASS_LINKED); - search_box.append (search_entry); + // search_box.append (search_entry); + search_box.append (entry_for_search); search_box.append (tool_arrow_down); search_box.append (tool_arrow_up); search_box.append (search_menubutton); @@ -229,7 +223,8 @@ namespace Scratch.Widgets { placeholder_text = _("Replace With") }; entry_for_replace = new Gtk.Entry (); - entry_for_replace.set_parent (replace_search_entry); + replace_search_entry.set_key_capture_widget (entry_for_replace); + // entry_for_replace.set_parent (replace_search_entry); entry_for_replace.set_icon_from_icon_name (Gtk.EntryIconPosition.PRIMARY, "edit-symbolic"); replace_tool_button = new Gtk.Button.with_label (_("Replace")); @@ -245,7 +240,8 @@ namespace Scratch.Widgets { margin_start = 3 }; replace_grid.add_css_class (Granite.STYLE_CLASS_LINKED); - replace_grid.append (replace_search_entry); + replace_grid.append (entry_for_replace); + // replace_grid.append (replace_search_entry); replace_grid.append (replace_tool_button); replace_grid.append (replace_all_tool_button); @@ -290,7 +286,7 @@ namespace Scratch.Widgets { reveal_child = false }; - append (revealer); + this.child = revealer; update_search_widgets (); var key_controller = new Gtk.EventControllerKey () { diff --git a/src/Widgets/Sidebar.vala b/src/Widgets/Sidebar.vala index 500cec6873..732afbeff0 100644 --- a/src/Widgets/Sidebar.vala +++ b/src/Widgets/Sidebar.vala @@ -44,6 +44,9 @@ public class Code.Sidebar : Gtk.Box { construct { orientation = Gtk.Orientation.VERTICAL; + vexpand = true; + hexpand = true; + add_css_class (Granite.STYLE_CLASS_SIDEBAR); choose_project_button = new Code.ChooseProjectButton () { @@ -62,7 +65,9 @@ public class Code.Sidebar : Gtk.Box { }; headerbar.add_css_class (Granite.STYLE_CLASS_FLAT); - stack = new Gtk.Stack (); + stack = new Gtk.Stack () { + vexpand = true + }; stack.transition_type = Gtk.StackTransitionType.SLIDE_LEFT_RIGHT; var overlay = new Gtk.Overlay () { @@ -74,7 +79,9 @@ public class Code.Sidebar : Gtk.Box { stack_switcher.visible = false; stack_switcher.stack = stack; - var actionbar = new Gtk.ActionBar (); + var actionbar = new Gtk.ActionBar () { + valign = END + }; actionbar.add_css_class (Granite.STYLE_CLASS_FLAT); var collapse_all_menu_item = new GLib.MenuItem (_("Collapse All"), Scratch.MainWindow.ACTION_PREFIX @@ -96,7 +103,8 @@ public class Code.Sidebar : Gtk.Box { project_menu_model = project_menu; var label = new Gtk.Label ( _("Manage project folders…")) { - halign = START + halign = START, + valign = CENTER }; var project_menu_button = new Gtk.MenuButton () { hexpand = true, @@ -176,8 +184,8 @@ public class Code.Sidebar : Gtk.Box { } public void focus_sidebar () { - if (stack.visible_child is Code.Widgets.SourceList) { - ((Code.Widgets.SourceList) stack.visible_child).grab_focus (); - } + // if (stack.visible_child is Granite.TreeList) { + // ((Code.Widgets.SourceList) stack.visible_child).grab_focus (); + // } } } diff --git a/src/Widgets/SourceList/CellRendererBadge.vala b/src/Widgets/SourceList/CellRendererBadge.vala deleted file mode 100644 index 6e71c1ac6a..0000000000 --- a/src/Widgets/SourceList/CellRendererBadge.vala +++ /dev/null @@ -1,125 +0,0 @@ -/* - * Copyright 2019-2024 elementary, Inc. (https://elementary.io) - * Copyright 2012–2013 Victor Eduardo - * SPDX-License-Identifier: LGPL-3.0-or-later - */ - -/** - * A badge renderer. - * - * Informs the user quickly on the content of the corresponding view. For example - * it might be used to show how much songs are in a playlist or how much updates - * are available. - * - * {{../doc/images/cellrendererbadge.png}} - * - * @since 0.2 - */ -public class Code.Widgets.CellRendererBadge : Gtk.CellRenderer { - - public string text { get; set; default = ""; } - - private Pango.Rectangle text_logical_rect; - private Pango.Layout text_layout; - private Gtk.Border margin; - private Gtk.Border padding; - private Gtk.Border border; - - public CellRendererBadge () { - } - - public override Gtk.SizeRequestMode get_request_mode () { - return Gtk.SizeRequestMode.HEIGHT_FOR_WIDTH; - } - - public override void get_preferred_width ( - Gtk.Widget widget, - out int minimum_size, - out int natural_size - ) { - update_layout_properties (widget); - - int width = text_logical_rect.width; - width += margin.left + margin.right; - width += padding.left + padding.right; - width += border.left + border.right; - - minimum_size = natural_size = width + 2 * (int) xpad; - } - - public override void get_preferred_height_for_width ( - Gtk.Widget widget, int width, - out int minimum_height, - out int natural_height - ) { - update_layout_properties (widget); - - int height = text_logical_rect.height; - height += margin.top + margin.bottom; - height += padding.top + padding.bottom; - height += border.top + border.bottom; - - minimum_height = natural_height = height + 2 * (int) ypad; - } - - private void update_layout_properties (Gtk.Widget widget) { - var ctx = widget.get_style_context (); - ctx.save (); - - // Add class before creating the pango layout and fetching paddings. - // This is needed in order to fetch the proper style information. - ctx.add_class (Granite.STYLE_CLASS_BADGE); - - // var state = ctx.get_state (); - - margin = ctx.get_margin (); - padding = ctx.get_padding (); - border = ctx.get_border (); - - text_layout = widget.create_pango_layout (text); - - ctx.restore (); - - Pango.Rectangle ink_rect; - text_layout.get_pixel_extents (out ink_rect, out text_logical_rect); - } - - public override void snapshot ( - Gtk.Snapshot snapshot, - Gtk.Widget widget, - Gdk.Rectangle bg_area, - Gdk.Rectangle cell_area, - Gtk.CellRendererState flags - ) { - - warning ("snapshot badge"); - update_layout_properties (widget); - - Gdk.Rectangle aligned_area = get_aligned_area (widget, flags, cell_area); - - int x = aligned_area.x; - int y = aligned_area.y; - int width = aligned_area.width; - int height = aligned_area.height; - - // Apply margin - x += margin.right; - y += margin.top; - width -= margin.left + margin.right; - height -= margin.top + margin.bottom; - - widget.add_css_class (Granite.STYLE_CLASS_BADGE); //TODO Use CSS for frame and background - - // Apply border width and padding offsets - x += border.right + padding.right; - y += border.top + padding.top; - width -= border.left + border.right + padding.left + padding.right; - height -= border.top + border.bottom + padding.top + padding.bottom; - - // Center text - x += text_logical_rect.x + (width - text_logical_rect.width) / 2; - y += text_logical_rect.y + (height - text_logical_rect.height) / 2; - - snapshot.append_layout (text_layout, widget.get_color ()); - } -} diff --git a/src/Widgets/SourceList/CellRendererExpander.vala b/src/Widgets/SourceList/CellRendererExpander.vala deleted file mode 100644 index 8ad49ee150..0000000000 --- a/src/Widgets/SourceList/CellRendererExpander.vala +++ /dev/null @@ -1,116 +0,0 @@ -/* - * Copyright 2012–2024 elementary, Inc. (https://elementary.io) - * SPDX-License-Identifier: LGPL-3.0-or-later - */ - -/** - * An expander renderer. - * - * For it to draw an expander, the the {@link Gtk.CellRenderer.is_expander} property must - * be set to true; otherwise nothing is drawn. The state of the expander (i.e. expanded or - * collapsed) is controlled by the {@link Gtk.CellRenderer.is_expanded} property. - * - * @since 0.2 - */ -public class Code.Widgets.CellRendererExpander : Gtk.CellRenderer { - public bool is_category_expander { get; set; default = false; } - - public CellRendererExpander () { - } - - public override Gtk.SizeRequestMode get_request_mode () { - return Gtk.SizeRequestMode.HEIGHT_FOR_WIDTH; - } - - public override void get_preferred_width ( - Gtk.Widget widget, - out int minimum_size, - out int natural_size - ) { - apply_style_changes (widget); - minimum_size = natural_size = get_arrow_size (widget) + 2 * (int) xpad; - revert_style_changes (widget); - } - - public override void get_preferred_height_for_width ( - Gtk.Widget widget, int width, - out int minimum_height, - out int natural_height - ) { - apply_style_changes (widget); - minimum_height = natural_height = get_arrow_size (widget) + 2 * (int) ypad; - revert_style_changes (widget); - } - - /** - * Gets the size of the expander arrow. - * - * The default implementation tries to retrieve the "expander-size" style property from - * //widget//, as it is primarily meant to be used along with a {@link Gtk.TreeView}. - * For those with special needs, it is recommended to override this method. - * - * @param widget Widget used to query the "expander-size" style property (should be a Gtk.TreeView.) - * @return Size of the expander arrow. - * @since 0.2 - */ - public virtual int get_arrow_size (Gtk.Widget widget) { - return 12; - } - - // public override void snapshot ( - // Gtk.Snapshot snapshot, - // Gtk.Widget widget, - // Gdk.Rectangle bg_area, - // Gdk.Rectangle cell_area, - // Gtk.CellRendererState flags - // ) { - // // No simple replacement for render_expander so leave for now - // // Renderers to be replaced with TreeListModel and TreeExpanders etc - // } - - // public override void render ( - // Cairo.Context context, - // Gtk.Widget widget, - // Gdk.Rectangle bg_area, - // Gdk.Rectangle cell_area, - // Gtk.CellRendererState flags - // ) { - // if (!is_expander) { - // return; - // } - - // unowned Gtk.StyleContext ctx = apply_style_changes (widget); - - // Gdk.Rectangle aligned_area = get_aligned_area (widget, flags, cell_area); - - // int arrow_size = int.min (get_arrow_size (widget), aligned_area.width); - - // int offset = arrow_size / 2; - // int x = aligned_area.x + aligned_area.width / 2 - offset; - // int y = aligned_area.y + aligned_area.height / 2 - offset; - - // var state = ctx.get_state (); - // const Gtk.StateFlags EXPANDED_FLAG = Gtk.StateFlags.CHECKED; - // ctx.set_state (is_expanded ? state | EXPANDED_FLAG : state & ~EXPANDED_FLAG); - - // ctx.render_expander (context, x, y, arrow_size, arrow_size); - - // revert_style_changes (widget); - // } - - private unowned Gtk.StyleContext apply_style_changes (Gtk.Widget widget) { - unowned Gtk.StyleContext ctx = widget.get_style_context (); - ctx.save (); - - if (is_category_expander) - ctx.add_class ("category-expander"); - else - ctx.add_class ("row-expander"); //TODO What Granite style is appropriate - - return ctx; - } - - private void revert_style_changes (Gtk.Widget widget) { - widget.get_style_context ().restore (); - } -} diff --git a/src/Widgets/SourceList/SourceList.vala b/src/Widgets/SourceList/SourceList.vala deleted file mode 100644 index aef0b8fd97..0000000000 --- a/src/Widgets/SourceList/SourceList.vala +++ /dev/null @@ -1,2959 +0,0 @@ -/* - * Copyright 2019-2026 elementary, Inc. (https://elementary.io) - * Copyright 2012-2014 Victor Martinez - * SPDX-License-Identifier: LGPL-3.0-or-later - */ - -namespace Code.Widgets { - -/** - * An interface for sorting items. - * - * @since 0.3 - */ -public interface SourceListSortable : SourceList.ExpandableItem { - /** - * Emitted after a user has re-ordered an item via DnD. - * - * @param moved The item that was moved to a different position by the user. - * @since 0.3 - */ - public signal void user_moved_item (SourceList.Item moved); - - /** - * Whether this item will allow users to re-arrange its children via DnD. - * - * This feature can co-exist with a sort algorithm (implemented - * by {@link Code.Widgets.SourceListSortable.compare}), but - * the actual order of the items in the list will always - * honor that method. The sort function has to be compatible with - * the kind of DnD reordering the item wants to allow, since the user can - * only reorder those items for which //compare// returns 0. - * - * @return Whether the item's children can be re-arranged by users. - * @since 0.3 - */ - public abstract bool allow_dnd_sorting (); - - /** - * Should return a negative integer, zero, or a positive integer if ''a'' - * sorts //before// ''b'', ''a'' sorts //with// ''b'', or ''a'' sorts - * //after// ''b'' respectively. If two items compare as equal, their - * order in the sorted source list is undefined. - * - * In order to ensure that the source list behaves as expected, this - * method must define a partial order on the source list tree; i.e. it - * must be reflexive, antisymmetric and transitive. Not complying with - * those requirements could make the program fall into an infinite loop - * and freeze the user interface. - * - * Should return //0// to allow any pair of items to be sortable via DnD. - * - * @param a First item. - * @param b Second item. - * @return A //negative// integer if //a// sorts before //b//, - * //zero// if //a// equals //b//, or a //positive// - * integer if //a// sorts after //b//. - * @since 0.3 - */ - public abstract int compare (SourceList.Item a, SourceList.Item b); -} - -/** - * An interface for dragging items out of the source list widget. - * - * @since 0.3 - */ -public interface SourceListDragSource : SourceList.Item { - /** - * Determines whether this item can be dragged outside the source list widget. - * - * Even if this method returns //false//, the item could still be dragged around - * within the source list if its parent allows DnD reordering. This only happens - * when the parent implements {@link Code.Widgets.SourceListSortable}. - * - * @return //true// if the item can be dragged; //false// otherwise. - * @since 0.3 - * @see Code.Widgets.SourceListSortable - */ - public abstract bool draggable (); - - /** - * This method is called when the drop site requests the data which is dragged. - * - * It is the responsibility of this method to fill //selection_data// with the - * data in the format which is indicated by {@link Gtk.SelectionData.get_target}. - * - * @param selection_data {@link Gtk.SelectionData} containing source data. - * @since 0.3 - * @see Gtk.SelectionData.set - * @see Gtk.SelectionData.set_uris - * @see Gtk.SelectionData.set_text - */ - // public abstract void prepare_selection_data (Gtk.SelectionData selection_data); -} - -/** - * An interface for receiving data from other widgets via drag-and-drop. - * - * @since 0.3 - */ -public interface SourceListDragDest : SourceList.Item { - /** - * Determines whether //data// can be dropped into this item. - * - * @param context The drag context. - * @param data {@link Gtk.SelectionData} containing source data. - * @return //true// if the drop is possible; //false// otherwise. - * @since 0.3 - */ - // public abstract bool data_drop_possible (Gdk.DragContext context, Gtk.SelectionData data); - - /** - * If a data drop is deemed possible, then this method is called - * when the data is actually dropped into this item. Any actions - * consequence of the data received should be handled here. - * - * @param context The drag context. - * @param data {@link Gtk.SelectionData} containing source data. - * @return The action taken, or //0// to indicate that the dropped data was not accepted. - * @since 0.3 - */ - // public abstract Gdk.DragAction data_received (Gdk.DragContext context, Gtk.SelectionData data); -} - -/** - * A widget that can display a list of items organized in categories. - * - * The source list widget consists of a collection of items, some of which are also expandable (and - * thus can contain more items). All the items displayed in the source list are children of the widget's - * root item. The API is meant to be used as follows: - * - * 1. Create the items you want to display in the source list, setting the appropriate values for their - * properties. The desired hierarchy is achieved by creating expandable items and adding items to them. - * These will be displayed as descendants in the widget's tree structure. The expandable items that are - * not nested inside any other item are considered to be at root level, and should be added to - * the widget's root item.<
> - * - * Expandable items located at the root level are treated as categories, and only support text. - * - * ''Example''<
> - * The final tree will have the following structure: - * {{{ - * Libraries - * Music - * Stores - * My Store - * Music - * Podcasts - * Devices - * Player 1 - * Player 2 - * }}} - * - * {{{ - * var library_category = new Code.Widgets.SourceList.ExpandableItem ("Libraries"); - * var store_category = new Code.Widgets.SourceList.ExpandableItem ("Stores"); - * var device_category = new Code.Widgets.SourceList.ExpandableItem ("Devices"); - * - * var music_item = new Code.Widgets.SourceList.Item ("Music"); - * - * // "Libraries" will be the parent category of "Music" - * library_category.add (music_item); - * - * // We plan to add sub-items to the store, so let's use an expandable item - * var my_store_item = new Code.Widgets.SourceList.ExpandableItem ("My Store"); - * store_category.add (my_store_item); - * - * var my_store_podcast_item = new Code.Widgets.SourceList.Item ("Podcasts"); - * var my_store_music_item = new Code.Widgets.SourceList.Item ("Music"); - * - * my_store_item.add (my_store_music_item); - * my_store_item.add (my_store_podcast_item); - * - * var player1_item = new Code.Widgets.SourceList.Item ("Player 1"); - * var player2_item = new Code.Widgets.SourceList.Item ("Player 2"); - * - * device_category.add (player1_item); - * device_category.add (player2_item); - * }}} - * - * 2. Create a source list widget.<
> - * {{{ - * var source_list = new Code.Widgets.SourceList (); - * }}} - * - * 3. Add root-level items to the {@link Code.Widgets.SourceList.root} item. - * This item only serves as a container, and all its properties are ignored by the widget. - * - * {{{ - * // This will add the main categories (including their children) to the source list. After - * // having being added to be widget, any other item added to any of these items - * // (or any other child item in a deeper level) will be automatically added too. - * // There's no need to deal with the source list widget directly. - * - * var root = source_list.root; - * - * root.add (library_category); - * root.add (store_category); - * root.add (device_category); - * }}} - * - * The steps mentioned above are enough for initializing the source list. Future changes to the items' - * properties are ''automatically'' reflected by the widget. - * - * Final steps would involve connecting handlers to the source list events, being - * {@link Code.Widgets.SourceList.item_selected} the most important, as it indicates that - * the selection was modified. - * - */ -public class SourceList : Gtk.Box { - - /** - * = WORKING INTERNALS = - * - * In order to offer a transparent Item-based API, and avoid the need of providing methods - * to deal with items directly on the SourceList widget, it was decided to follow a monitor-like - * implementation, where the source list permanently monitors its root item and any other - * child item added to it. The task of monitoring the properties of the items has been - * divided among different objects, as shown below: - * - * Monitored by: Object::method that receives the signals indicating the property change. - * Applied by: Object::method that actually updates the tree to reflect the property changes - * (directly or indirectly, as in the case of the tree data model). - * - * --------------------------------------------------------------------------------------------- - * PROPERTY | MONITORED BY | APPLIED BY - * --------------------------------------------------------------------------------------------- - * + Item | | - * - parent | Not monitored | N/A - * - name | DataModel::on_item_prop_changed | Tree::name_cell_data_func - * - editable | DataModel::on_item_prop_changed | Queried when needed (See Tree::start_editing_item) - * - visible | DataModel::on_item_prop_changed | DataModel::filter_visible_func - * - icon | DataModel::on_item_prop_changed | Tree::icon_cell_data_func - * - activatable | Same as @icon | Same as @icon - * + ExpandableItem | | - * - collapsible | DataModel::on_item_prop_changed | Tree::update_expansion - * | | Tree::expander_cell_data_func - * - expanded | Same as @collapsible | Same as @collapsible - * --------------------------------------------------------------------------------------------- - * * Only automatic properties are monitored. ExpandableItem's additions/removals are handled by - * DataModel::add_item() and DataModel::remove_item() - * - * Other features: - * - Sorting: this happens on the tree-model level (DataModel). - */ - - - - /** - * A source list entry. - * - * Any change made to any of its properties will be ''automatically'' reflected - * by the {@link Code.Widgets.SourceList} widget. - * - * @since 0.2 - */ - public class Item : Object { - - /** - * Emitted when the user has finished editing the item's name. - * - * By default, if the name doesn't consist of white space, it is automatically assigned - * to the {@link Code.Widgets.SourceList.Item.name} property. The default behavior can - * be changed by overriding this signal. - * @param new_name The item's new name (result of editing.) - * @since 0.2 - */ - public virtual signal void edited (string new_name) { - if (editable && new_name.strip () != "") - this.name = new_name; - } - - /** - * The {@link Code.Widgets.SourceList.Item.activatable} icon was activated. - * - * @see Code.Widgets.SourceList.Item.activatable - * @since 0.2 - */ - public virtual signal void action_activated () { } - - /** - * Emitted when the item is double-clicked or when it is selected and one of the keys: - * Space, Shift+Space, Return or Enter is pressed. This signal is //also// for - * editable items. - * - * @since 0.2 - */ - public virtual signal void activated () { } - - /** - * Parent {@link Code.Widgets.SourceList.ExpandableItem} of the item. - * ''Must not'' be modified. - * - * @since 0.2 - */ - public ExpandableItem parent { get; internal set; } - - /** - * The item's name. Primary and most important information. - * - * @since 0.2 - */ - public string name { get; set; default = ""; } - - /** - * The item's tooltip. If set to null (default), the tooltip for the item will be the - * contents of the {@link Code.Widgets.SourceList.Item.name} property. - * - * @since 5.3 - */ - public string? tooltip { get; set; default = null; } - - /** - * Markup to be used instead of {@link Code.Widgets.SourceList.ExpandableItem.name} - * This would mean that &, <, etc have to be escaped in the text, but basic formatting - * can be done on the item with HTML style tags. - * - * Note: Only the {@link Code.Widgets.SourceList.ExpandableItem.name} property - * is modified for editable items. So this property will be need to updated and - * reformatted with editable items. - * - * @since 5.0 - */ - public string? markup { get; set; default = null; } - - /** - * A badge shown next to the item's name. - * - * It can be used for displaying the number of unread messages in the "Inbox" item, - * for instance. - * - * @since 0.2 - */ - public string badge { get; set; default = ""; } - - /** - * Whether the item's name can be edited from within the source list. - * - * When this property is set to //true//, users can edit the item by pressing - * the F2 key, or by double-clicking its name. - * - * ''This property only works for selectable items''. - * - * @see Code.Widgets.SourceList.Item.selectable - * @see Code.Widgets.SourceList.start_editing_item - * @since 0.2 - */ - public bool editable { get; set; default = false; } - - /** - * Whether the item should appear in the source list's tree or not. - * - * @since 0.2 - */ - public bool visible { get; set; default = true; } - - /** - * Whether the item can be selected or not. - * - * Setting this property to true doesn't guarantee that the item will actually be - * selectable, since there are other external factors to take into account, like the - * item's {@link Code.Widgets.SourceList.Item.visible} property; whether the item is - * a category; the parent item is collapsed, etc. - * - * @see Code.Widgets.SourceList.Item.visible - * @since 0.2 - */ - public bool selectable { get; set; default = true; } - - /** - * Primary icon. - * - * This property should be used to give the user an idea of what the item represents - * (i.e. content type.) - * - * @since 0.2 - */ - public Icon icon { get; set; } - - /** - * An activatable icon that works like a button. - * - * It can be used for e.g. showing an //"eject"// icon on a device's item. - * - * @see Code.Widgets.SourceList.Item.action_activated - * @since 0.2 - */ - public Icon activatable { get; set; } - - /** - * The tooltip for the activatable icon. - * - * @since 5.0 - */ - public string activatable_tooltip { get; set; default = ""; } - - /** - * Creates a new {@link Code.Widgets.SourceList.Item}. - * - * @param name Name of the item. - * @return (transfer full) A new {@link Code.Widgets.SourceList.Item}. - * @since 0.2 - */ - public Item (string name = "") { - this.name = name; - } - - /** - * Invoked when the item is secondary-clicked or when the usual menu keys are pressed. - * - * Note that since Granite 5.0, right clicking on an item no longer selects/activates it, so - * any context menu items should be actioned on the item instance rather than the selected item - * in the SourceList - * - * @return A {@link GLib.Menu} or //null// if nothing should be displayed. - * @since 0.2 - */ - public virtual GLib.Menu? get_context_menu () { - return null; - } - } - - - - /** - * An item that can contain more items. - * - * It supports all the properties inherited from {@link Code.Widgets.SourceList.Item}, - * and behaves like a normal item, except when it is located at the root level; in that case, - * the following properties are ignored by the widget: - * - * * {@link Code.Widgets.SourceList.Item.selectable} - * * {@link Code.Widgets.SourceList.Item.editable} - * * {@link Code.Widgets.SourceList.Item.icon} - * * {@link Code.Widgets.SourceList.Item.activatable} - * * {@link Code.Widgets.SourceList.Item.badge} - * - * Root-level expandable items (i.e. Main Categories) are ''not'' displayed when they contain - * zero visible children. - * - * @since 0.2 - */ - public class ExpandableItem : Item { - - /** - * Emitted when an item is added. - * - * @param item Item added. - * @see Code.Widgets.SourceList.ExpandableItem.add - * @since 0.2 - */ - public signal void child_added (Item item); - - /** - * Emitted when an item is removed. - * - * @param item Item removed. - * @see Code.Widgets.SourceList.ExpandableItem.remove - * @since 0.2 - */ - public signal void child_removed (Item item); - - /** - * Emitted when the item is expanded or collapsed. - * - * @since 0.2 - */ - public virtual signal void toggled () { } - - /** - * Whether the item is collapsible or not. - * - * When set to //false//, the item is //always// expanded and the expander is - * not shown. Please note that this will also affect the value returned by the - * {@link Code.Widgets.SourceList.ExpandableItem.expanded} property. - * - * @see Code.Widgets.SourceList.ExpandableItem.expanded - * @since 0.2 - */ - public bool collapsible { get; set; default = true; } - - /** - * Whether the item is expanded or not. - * - * The source list widget will obey the value of this property when possible. - * - * This property has no effect when {@link Code.Widgets.SourceList.ExpandableItem.collapsible} - * is set to //false//. Also keep in mind that, __when set to //true//__, this property - * doesn't always represent the actual expansion state of an item. For example, it might - * be the case that an expandable item is collapsed because it has zero visible children, - * but its //expanded// property value is still //true//; in such case, once one of the - * item's children becomes visible, the item will be expanded again. Same applies to items - * hidden behind a collapsed parent item. - * - * If obtaining the ''actual'' expansion state of an item is important, - * use {@link Code.Widgets.SourceList.is_item_expanded} instead. - * - * @see Code.Widgets.SourceList.ExpandableItem.collapsible - * @see Code.Widgets.SourceList.is_item_expanded - * @since 0.2 - */ - private bool _expanded = false; - public bool expanded { - get { return _expanded || !collapsible; } // if not collapsible, always return true - set { - if (value != _expanded) { - _expanded = value; - toggled (); - } - } - } - - /** - * Number of children contained by the item. - * - * @since 0.2 - */ - public uint n_children { - get { return children_list.size; } - } - - /** - * The item's children. - * - * This returns a newly-created list containing the children. - * It's safe to iterate it while removing items with - * {@link Code.Widgets.SourceList.ExpandableItem.remove} - * - * @since 0.2 - */ - public Gee.Collection children { - owned get { - // Create a copy of the children so that it's safe to iterate it - // (e.g. by using foreach) while removing items. - var children_list_copy = new Gee.ArrayList (); - children_list_copy.add_all (children_list); - return children_list_copy; - } - } - - private Gee.Collection children_list = new Gee.ArrayList (); - - /** - * Creates a new {@link Code.Widgets.SourceList.ExpandableItem} - * - * @param name Title of the item. - * @return (transfer full) A new {@link Code.Widgets.SourceList.ExpandableItem}. - * @since 0.2 - */ - public ExpandableItem (string name = "") { - base (name); - } - - construct { - editable = false; - } - - /** - * Checks whether the item contains the specified child. - * - * This method only considers the item's immediate children. - * - * @param item Item to search. - * @return Whether the item was found or not. - * @since 0.2 - */ - public bool contains (Item item) { - return item in children_list; - } - - /** - * Adds an item. - * - * {@link Code.Widgets.SourceList.ExpandableItem.child_added} is fired after the item is added. - * - * While adding a child item, //the item it's being added to will set itself as the parent//. - * Please note that items are required to have their //parent// property set to //null// before - * being added, so make sure the item is removed from its previous parent before attempting - * to add it to another item. For instance: - * {{{ - * if (item.parent != null) - * item.parent.remove (item); // this will set item's parent to null - * new_parent.add (item); - * }}} - * - * @param item The item to add. Its parent __must__ be //null//. - * @see Code.Widgets.SourceList.ExpandableItem.child_added - * @see Code.Widgets.SourceList.ExpandableItem.remove - * @since 0.2 - */ - public void add (Item item) requires (item.parent == null) { - item.parent = this; - children_list.add (item); - child_added (item); - } - - /** - * Removes an item. - * - * The {@link Code.Widgets.SourceList.ExpandableItem.child_removed} signal is fired - * //after removing the item//. Finally (i.e. after all the handlers have been invoked), - * the item's {@link Code.Widgets.SourceList.Item.parent} property is set to //null//. - * This has the advantage of letting signal handlers know the parent from which //item// - * is being removed. - * - * @param item The item to remove. This will fail if item has a different parent. - * @see Code.Widgets.SourceList.ExpandableItem.child_removed - * @see Code.Widgets.SourceList.ExpandableItem.clear - * @since 0.2 - */ - public void remove (Item item) requires (item.parent == this) { - children_list.remove (item); - child_removed (item); - item.parent = null; - } - - /** - * Removes all the items contained by the item. It works similarly to - * {@link Code.Widgets.SourceList.ExpandableItem.remove}. - * - * @see Code.Widgets.SourceList.ExpandableItem.remove - * @see Code.Widgets.SourceList.ExpandableItem.child_removed - * @since 0.2 - */ - public void clear () { - foreach (var item in children) - remove (item); - } - - /** - * Expands the item and/or its children. - * - * @param inclusive Whether to also expand this item (true), or only its children (false). - * @param recursive Whether to recursively expand all the children (true), or only - * immediate children (false). - * @see Code.Widgets.SourceList.ExpandableItem.expanded - * @since 0.2 - */ - public void expand_all (bool inclusive = true, bool recursive = true) { - set_expansion (this, inclusive, recursive, true); - } - - /** - * Collapses the item and/or its children. - * - * @param inclusive Whether to also collapse this item (true), or only its children (false). - * @param recursive Whether to recursively collapse all the children (true), or only - * immediate children (false). - * @see Code.Widgets.SourceList.ExpandableItem.expanded - * @since 0.2 - */ - public void collapse_all (bool inclusive = true, bool recursive = true) { - set_expansion (this, inclusive, recursive, false); - } - - private static void set_expansion (ExpandableItem item, bool inclusive, bool recursive, bool expanded) { - if (inclusive) - item.expanded = expanded; - - foreach (var child_item in item.children) { - var child_expandable_item = child_item as ExpandableItem; - if (child_expandable_item != null) { - if (recursive) - set_expansion (child_expandable_item, true, true, expanded); - else - child_expandable_item.expanded = expanded; - } - } - } - - /** - * Recursively expands the item along with its parent(s). - * - * @see Code.Widgets.SourceList.ExpandableItem.expanded - * @since 0.2 - */ - public void expand_with_parents () { - // Update parent items first due to GtkTreeView's working internals: - // Expanding children before their parents would not always work, because - // they could be obscured behind a collapsed row by the time the treeview - // tries to expand them, obviously failing. - if (parent != null) - parent.expand_with_parents (); - expanded = true; - } - - /** - * Recursively collapses the item along with its parent(s). - * - * @see Code.Widgets.SourceList.ExpandableItem.expanded - * @since 0.2 - */ - public void collapse_with_parents () { - if (parent != null) - parent.collapse_with_parents (); - expanded = false; - } - } - - - - /** - * The model backing the SourceList tree. - * - * It monitors item property changes, and handles children additions and removals. It also controls - * the visibility of the items based on their "visible" property, and on their number of children, - * if they happen to be categories. Its main purpose is to provide an easy and practical interface - * for sorting, adding, removing and updating items, eliminating the need of repeatedly dealing with - * the Gtk.TreeModel API directly. - */ - private class DataModel : Gtk.TreeModelFilter, Gtk.TreeDragSource, Gtk.TreeDragDest { - - /** - * An object that references a particular row in a model. This class is a wrapper built around - * Gtk.TreeRowReference, and exists with the purpose of ensuring we never use invalid tree paths - * or iters in the model, since most of these errors provoke failures due to GTK+ assertions - * or, even worse, unexpected behavior. - */ - private class NodeWrapper { - - /** - * The actual reference to the node. If is is null, it is treated as invalid. - */ - private Gtk.TreeRowReference? row_reference; - - /** - * A newly-created Gtk.TreeIter pointing to the node if it exists; null otherwise. - */ - public Gtk.TreeIter? iter { - owned get { - Gtk.TreeIter? rv = null; - - if (valid) { - var _path = this.path; - if (_path != null) { - Gtk.TreeIter _iter; - if (row_reference.get_model ().get_iter (out _iter, _path)) - rv = _iter; - } - } - - return rv; - } - } - - /** - * A newly-created Gtk.TreePath pointing to the node if it exists; null otherwise. - */ - public Gtk.TreePath? path { - owned get { return valid ? row_reference.get_path () : null; } - } - - /** - * Whether the node is valid or not. When it is not valid, no valid references are - * returned by the object to avoid errors (null is returned instead). - */ - public bool valid { - get { return row_reference != null && row_reference.valid (); } - } - - public NodeWrapper (Gtk.TreeModel model, Gtk.TreeIter iter) { - row_reference = new Gtk.TreeRowReference (model, model.get_path (iter)); - } - } - - /** - * Helper object used to monitor item property changes. - */ - private class ItemMonitor { - public signal void changed (Item self, string prop_name); - private Item item; - - public ItemMonitor (Item item) { - this.item = item; - item.notify.connect_after (on_notify); - } - - ~ItemMonitor () { - item.notify.disconnect (on_notify); - } - - private void on_notify (ParamSpec prop) { - changed (item, prop.name); - } - } - - private enum Column { - ITEM, - N_COLUMNS; - - public Type type () { - switch (this) { - case ITEM: - return typeof (Item); - - default: - assert_not_reached (); // a Type must be returned for every valid column - } - } - } - - public signal void item_updated (Item item); - - /** - * Used by push_parent_update() as key to associate the respective data to the objects. - */ - private const string ITEM_PARENT_NEEDS_UPDATE = "item-parent-needs-update"; - - private ExpandableItem _root; - - /** - * Root item. - * - * This item is not actually part of the model. It's only used as a proxy - * for adding and removing items. - */ - public ExpandableItem root { - get { return _root; } - set { - if (_root != null) { - remove_children_monitor (_root); - foreach (var item in _root.children) - remove_item (item); - } - - _root = value; - - add_children_monitor (_root); - foreach (var item in _root.children) - add_item (item); - } - } - - // This hash map stores items and their respective child node references. For that reason, the - // references it contains should only be used on the child_tree model, or converted to filter - // iters/paths using convert_child_*_to_*() before using them with the filter (i.e. this) model. - private Gee.HashMap items = new Gee.HashMap (); - - private Gee.HashMap monitors = new Gee.HashMap (); - - private Gtk.TreeStore child_tree; - private unowned SourceList.VisibleFunc? filter_func; - - public DataModel () { - - } - - construct { - child_tree = new Gtk.TreeStore (Column.N_COLUMNS, Column.ITEM.type ()); - child_model = child_tree; - virtual_root = null; - - child_tree.set_default_sort_func (child_model_sort_func); - resort (); - - set_visible_func (filter_visible_func); - } - - public bool has_item (Item item) { - return items.has_key (item); - } - - public void update_item (Item item) { - if (!has_item (item)) { - return; - } - - assert (root != null); - - // Emitting row_changed() for this item's row in the child model causes the filter - // (i.e. this model) to re-evaluate whether a row is visible or not, calling - // filter_visible_func() for that row again, and that's exactly what we want. - var node_reference = items.get (item); - if (node_reference != null) { - var path = node_reference.path; - var iter = node_reference.iter; - if (path != null && iter != null) { - child_tree.row_changed (path, iter); - item_updated (item); - } - } - } - - private void add_item (Item item) requires (!has_item (item)) { - assert (root != null); - - // Find the parent iter - Gtk.TreeIter? parent_child_iter = null, child_iter; - var parent = item.parent; - - if (parent != null && parent != root) { - // Add parent if it hasn't been added yet - if (!has_item (parent)) - add_item (parent); - - // Try to find the parent's iter - parent_child_iter = get_item_child_iter (parent); - - // Parent must have been added prior to adding this item - assert (parent_child_iter != null); - } - - child_tree.append (out child_iter, parent_child_iter); - child_tree.set (child_iter, Column.ITEM, item, -1); - - items.set (item, new NodeWrapper (child_tree, child_iter)); - - // This is equivalent to a property change. The tree still needs to update - // some of the new item's properties through this signal's handler. - item_updated (item); - - add_property_monitor (item); - - push_parent_update (parent); - - // If the item is expandable, also add children - var expandable = item as ExpandableItem; - if (expandable != null) { - foreach (var child_item in expandable.children) - add_item (child_item); - - // Monitor future additions/removals through signal handlers - add_children_monitor (expandable); - } - } - - private void remove_item (Item item) requires (has_item (item)) { - assert (root != null); - - remove_property_monitor (item); - - // get_item_child_iter() depends on items.get(item) for retrieving the right reference, - // so don't unset the item from @items yet! We first get the child iter and then - // unset the value. - var child_iter = get_item_child_iter (item); - - // Now we remove the item from the table, because that way get_item_child_iter() and - // all the methods that depend on it won't return invalid iters or items when - // called. This is important because child_tree.remove() will emit row_deleted(), - // and its handlers could potentially depend on one of the methods mentioned above. - items.unset (item); - - if (child_iter != null) - child_tree.remove (ref child_iter); - - push_parent_update (item.parent); - - // If the item is expandable, also remove children - var expandable = item as ExpandableItem; - if (expandable != null) { - // No longer monitor future additions or removals - remove_children_monitor (expandable); - - foreach (var child_item in expandable.children) - remove_item (child_item); - } - } - - private void add_property_monitor (Item item) { - var wrapper = new ItemMonitor (item); - monitors[item] = wrapper; - wrapper.changed.connect (on_item_prop_changed); - } - - private void remove_property_monitor (Item item) { - var wrapper = monitors[item]; - if (wrapper != null) - wrapper.changed.disconnect (on_item_prop_changed); - monitors.unset (item); - } - - private void add_children_monitor (ExpandableItem item) { - item.child_added.connect_after (on_item_child_added); - item.child_removed.connect_after (on_item_child_removed); - } - - private void remove_children_monitor (ExpandableItem item) { - item.child_added.disconnect (on_item_child_added); - item.child_removed.disconnect (on_item_child_removed); - } - - private void on_item_child_added (Item item) { - add_item (item); - } - - private void on_item_child_removed (Item item) { - remove_item (item); - } - - private void on_item_prop_changed (Item item, string prop_name) { - if (prop_name != "parent") - update_item (item); - } - - /** - * Pushes a call to update_item() if //parent// is not //null//. - * - * This is needed because the visibility of categories depends on their n_children property, - * and also because item expansion should be updated after adding or removing items. - * If many updates are pushed, and the item has still not been updated, only one is processed. - * This guarantees efficiency as updating a category item could trigger expensive actions. - */ - private void push_parent_update (ExpandableItem? parent) { - if (parent == null) - return; - - bool needs_update = parent.get_data (ITEM_PARENT_NEEDS_UPDATE); - - // If an update is already waiting to be processed, just return, as we - // don't need to queue another one for the same item. - if (needs_update) - return; - - var path = get_item_path (parent); - - if (path != null) { - // Let's mark this item for update - parent.set_data (ITEM_PARENT_NEEDS_UPDATE, true); - - Idle.add (() => { - if (parent != null) { - update_item (parent); - - // Already updated. No longer needs an update. - parent.set_data (ITEM_PARENT_NEEDS_UPDATE, false); - } - - return false; - }); - } - } - - /** - * Returns the Item pointed by iter, or null if the iter doesn't refer to a valid item. - */ - public Item? get_item (Gtk.TreeIter iter) { - Item? item; - get (iter, Column.ITEM, out item, -1); - return item; - } - - /** - * Returns the Item pointed by path, or null if the path doesn't refer to a valid item. - */ - public Item? get_item_from_path (Gtk.TreePath path) { - Gtk.TreeIter iter; - if (get_iter (out iter, path)) - return get_item (iter); - - return null; - } - - /** - * Returns a newly-created path pointing to the item, or null in case a valid path - * is not found. - */ - public Gtk.TreePath? get_item_path (Item item) { - Gtk.TreePath? path = null, child_path = get_item_child_path (item); - - // We want a filter path, not a child_model path - if (child_path != null) - path = convert_child_path_to_path (child_path); - - return path; - } - - /** - * Returns a newly-created iterator pointing to the item, or null in case a valid iter - * was not found. - */ - public Gtk.TreeIter? get_item_iter (Item item) { - var child_iter = get_item_child_iter (item); - - if (child_iter != null) { - Gtk.TreeIter iter; - if (convert_child_iter_to_iter (out iter, child_iter)) - return iter; - } - - return null; - } - - /** - * External "extra" filter method. - */ - public void set_filter_func (SourceList.VisibleFunc? visible_func) { - this.filter_func = visible_func; - } - - /** - * Checks whether an item is a category (i.e. a root-level expandable item). - * The caller must pass an iter or path pointing to the item, but not both - * (one of them must be null.) - * - * TODO: instead of checking the position of the iter or path, we should simply - * check whether the item's parent is the root item and whether the item is - * expandable. We don't do so right now because vala still allows client code - * to access the Item.parent property, even though its setter is defined as internal. - */ - public bool is_category (Item item, Gtk.TreeIter? iter, Gtk.TreePath? path = null) { - bool is_category = false; - // either iter or path has to be null - if (item is ExpandableItem) { - if (iter != null) { - assert (path == null); - is_category = is_iter_at_root_level (iter); - } else { - assert (iter == null); - is_category = is_path_at_root_level (path); - } - } - return is_category; - } - - public bool is_iter_at_root_level (Gtk.TreeIter iter) { - return is_path_at_root_level (get_path (iter)); - } - - public bool is_path_at_root_level (Gtk.TreePath path) { - return path.get_depth () == 1; - } - - private void resort () { - //TODO Do not use deprecated model and view. - child_tree.set_sort_column_id (-1, Gtk.SortType.ASCENDING); - child_tree.set_sort_column_id (0, Gtk.SortType.ASCENDING); - } - - private int child_model_sort_func (Gtk.TreeModel model, Gtk.TreeIter a, Gtk.TreeIter b) { - int order = 0; - - Item? item_a, item_b; - child_tree.get (a, Column.ITEM, out item_a, -1); - child_tree.get (b, Column.ITEM, out item_b, -1); - - // code should only compare items at same hierarchy level - assert (item_a.parent == item_b.parent); - - var parent = item_a.parent as SourceListSortable; - if (parent != null) - order = parent.compare (item_a, item_b); - - return order; - } - - private Gtk.TreeIter? get_item_child_iter (Item item) { - Gtk.TreeIter? child_iter = null; - - var child_node_wrapper = items.get (item); - if (child_node_wrapper != null) - child_iter = child_node_wrapper.iter; - - return child_iter; - } - - private Gtk.TreePath? get_item_child_path (Item item) { - Gtk.TreePath? child_path = null; - - var child_node_wrapper = items.get (item); - if (child_node_wrapper != null) - child_path = child_node_wrapper.path; - - return child_path; - } - - /** - * Filters the child-tree items based on their "visible" property. - */ - private bool filter_visible_func (Gtk.TreeModel child_model, Gtk.TreeIter iter) { - bool item_visible = false; - - Item? item; - child_tree.get (iter, Column.ITEM, out item, -1); - - if (item != null) { - item_visible = item.visible; - - // If the item is a category, also query the number of visible children - // because empty categories should not be displayed. - var expandable = item as ExpandableItem; - if (expandable != null && child_tree.iter_depth (iter) == 0) { - uint n_visible_children = 0; - foreach (var child_item in expandable.children) { - if (child_item.visible) - n_visible_children++; - } - item_visible = item_visible && n_visible_children > 0; - } - } - - if (filter_func != null) - item_visible = item_visible && filter_func (item); - - return item_visible; - } - - /** - * TreeDragDest implementation - */ - - // public bool drag_data_received (Gtk.TreePath dest, Gtk.SelectionData selection_data) { - // Gtk.TreeModel model; - // Gtk.TreePath src_path; - - // // Check if the user is dragging a row: - // // - // // Due to Gtk.TreeModelFilter's implementation of drag_data_get the values returned by - // // tree_row_drag_data for GtkModel and GtkPath correspond to the child model and not the filter. - // if (Gtk.tree_get_row_drag_data (selection_data, out model, out src_path) && model == child_tree) { - // // get a child path representation of dest - // var child_dest = convert_path_to_child_path (dest); - - // if (child_dest != null) { - // // New GtkTreeIters will be assigned to the rows at child_dest and its children. - // if (child_tree_drag_data_received (child_dest, src_path)) - // return true; - // } - // } - - // // no new row inserted - // return false; - // } - - // private bool child_tree_drag_data_received (Gtk.TreePath dest, Gtk.TreePath src_path) { - // bool retval = false; - // Gtk.TreeIter src_iter, dest_iter; - - // if (!child_tree.get_iter (out src_iter, src_path)) - // return false; - - // var prev = dest; - - // // Get the path to insert _after_ (dest is the path to insert _before_) - // if (!prev.prev ()) { - // // dest was the first spot at the current depth; which means - // // we are supposed to prepend. - - // var parent = dest; - // Gtk.TreeIter? dest_parent = null; - - // if (parent.up () && parent.get_depth () > 0) - // child_tree.get_iter (out dest_parent, parent); - - // child_tree.prepend (out dest_iter, dest_parent); - // retval = true; - // } else if (child_tree.get_iter (out dest_iter, prev)) { - // var tmp_iter = dest_iter; - // child_tree.insert_after (out dest_iter, null, tmp_iter); - // retval = true; - // } - - // // If we succeeded in creating dest_iter, walk src_iter tree branch, - // // duplicating it below dest_iter. - // if (retval) { - // recursive_node_copy (src_iter, dest_iter); - - // // notify that the item was moved - // Item item; - // child_tree.get (src_iter, Column.ITEM, out item, -1); - // return_val_if_fail (item != null, retval); - - // // XXX Workaround: - // // GtkTreeView automatically collapses expanded items that - // // are dragged to a new location. Oddly, GtkTreeView doesn't fire - // // 'row-collapsed' for the respective path, so we cannot keep track - // // of that behavior via standard means. For now we'll just have - // // our tree view check the properties of item again and ensure - // // they're honored - // update_item (item); - - // var parent = item.parent as SourceListSortable; - // return_val_if_fail (parent != null, retval); - - // parent.user_moved_item (item); - // } - - // return retval; - // } - - private void recursive_node_copy (Gtk.TreeIter src_iter, Gtk.TreeIter dest_iter) { - move_item (src_iter, dest_iter); - - Gtk.TreeIter child; - if (child_tree.iter_children (out child, src_iter)) { - // Need to create children and recurse. Note our dependence on - // persistent iterators here. - do { - Gtk.TreeIter copy; - child_tree.append (out copy, dest_iter); - recursive_node_copy (child, copy); - } while (child_tree.iter_next (ref child)); - } - } - - private void move_item (Gtk.TreeIter src_iter, Gtk.TreeIter dest_iter) { - Item item; - child_tree.get (src_iter, Column.ITEM, out item, -1); - return_if_fail (item != null); - - // update the row reference of item with the new location - child_tree.set (dest_iter, Column.ITEM, item, -1); - items.set (item, new NodeWrapper (child_tree, dest_iter)); - } - - // public bool row_drop_possible (Gtk.TreePath dest, Gtk.SelectionData selection_data) { - // Gtk.TreeModel model; - // Gtk.TreePath src_path; - - // // Check if the user is dragging a row: - // // Due to Gtk.TreeModelFilter's implementation of drag_data_get the values returned by - // // tree_row_drag_data for GtkModel and GtkPath correspond to the child model and not the filter. - // if (!Gtk.tree_get_row_drag_data (selection_data, out model, out src_path) || model != child_tree) - // return false; - - // // get a representation of dest in the child model - // var child_dest = convert_path_to_child_path (dest); - - // // don't allow dropping an item into itself - // if (child_dest == null || src_path.compare (child_dest) == 0) - // return false; - - // // Only allow DnD between items at the same depth (indentation level) - // // This doesn't mean their parent is the same. - // int src_depth = src_path.get_depth (); - // int dest_depth = child_dest.get_depth (); - - // if (src_depth != dest_depth) - // return false; - - // // no need to check dest_depth since we know its equal to src_depth - // if (src_depth < 1) - // return false; - - // Item? parent = null; - - // // if the depth is 1, we're talking about the items at root level, - // // and by definition they share the same parent (root). We don't - // // need to verify anything else for that specific case - // if (src_depth == 1) { - // parent = root; - // } else { - // // we verified equality above. this must be true - // assert (dest_depth > 1); - - // // Only allow reordering between siblings, i.e. items with the same - // // parent. We don't want items to change their parent through DnD - // // because that would complicate our existing APIs, and may introduce - // // unpredictable behavior. - // var src_indices = src_path.get_indices (); - // var dest_indices = child_dest.get_indices (); - - // // parent index is given by indices[depth-2], where depth > 1 - // int src_parent_index = src_indices[src_depth - 2]; - // int dest_parent_index = dest_indices[dest_depth - 2]; - - // if (src_parent_index != dest_parent_index) - // return false; - - // // get parent. Note that we don't use the child path for this - // var dest_parent = dest; - - // if (!dest_parent.up () || dest_parent.get_depth () < 1) - // return false; - - // parent = get_item_from_path (dest_parent); - // } - - // var sortable = parent as SourceListSortable; - - // if (sortable == null || !sortable.allow_dnd_sorting ()) - // return false; - - // var dest_item = get_item_from_path (dest); - - // if (dest_item == null) - // return true; - - // Item? source_item = null; - // var filter_src_path = convert_child_path_to_path (src_path); - - // if (filter_src_path != null) - // source_item = get_item_from_path (filter_src_path); - - // if (source_item == null) - // return false; - - // // If order isn't indifferent (=0), 'dest' has to sort before 'source'. - // // Otherwise we'd allow the user to move the 'source_item' to a new - // // location before 'dest_item', but that location would be changed - // // later by the sort function, making the whole interaction poinless. - // // We better prevent such reorderings from the start by giving the - // // user a visual clue about the invalid drop location. - // if (sortable.compare (dest_item, source_item) >= 0) { - // if (!dest.prev ()) - // return true; - - // // 'source_item' also has to sort 'after' or 'equal' the item currently - // // preceding 'dest_item' - // var dest_item_prev = get_item_from_path (dest); - - // return dest_item_prev != null - // && dest_item_prev != source_item - // && sortable.compare (dest_item_prev, source_item) <= 0; - // } - - // return false; - // } - - // /** - // * Override default implementation of TreeDragSource - // * - // * drag_data_delete is not overriden because the default implementation - // * does exactly what we need. - // */ - - // public bool drag_data_get (Gtk.TreePath path, Gtk.SelectionData selection_data) { - // // If we're asked for a data about a row, just have the default implementation fill in - // // selection_data. Please note that it will provide information relative to child_model. - // if (selection_data.get_target () == Gdk.Atom.intern_static_string ("GTK_TREE_MODEL_ROW")) - // return base.drag_data_get (path, selection_data); - - // // check if the item at path provides DnD source data - // var drag_source_item = get_item_from_path (path) as SourceListDragSource; - // if (drag_source_item != null && drag_source_item.draggable ()) { - // drag_source_item.prepare_selection_data (selection_data); - // return true; - // } - - // return false; - // } - - // public bool row_draggable (Gtk.TreePath path) { - // if (!base.row_draggable (path)) - // return false; - - // var item = get_item_from_path (path); - - // if (item != null) { - // // check if the item's parent allows DnD sorting - // var sortable_item = item.parent as SourceListSortable; - - // if (sortable_item != null && sortable_item.allow_dnd_sorting ()) - // return true; - - // // Since the parent item does not allow DnD sorting, there's no - // // reason to allow dragging it unless the row is actually draggable. - // var drag_source_item = item as SourceListDragSource; - - // if (drag_source_item != null && drag_source_item.draggable ()) - // return true; - // } - - // return false; - // } - } - - - /** - * Class responsible for rendering Item.icon and Item.activatable. It also - * notifies about clicks through the activated() signal. - */ - // private class CellRendererIcon : Gtk.CellRendererPixbuf { - private class CellRendererIcon : Gtk.CellRenderer { - public signal void activated (string path); - public Icon gicon { get; set; } - // private Gtk.CellRendererPixbuf renderer_pixbuf; - public CellRendererIcon () { - - } - - construct { - // renderer_pixbuf = new Gtk.CellRendererPixbuf () { - // mode = Gtk.CellRendererMode.ACTIVATABLE, - // icon_size = Gtk.IconSize.NORMAL - // }; - - // bind_property ("gicon", renderer_pixbuf, "gicon"); - } - - public override bool activate ( - Gdk.Event event, - Gtk.Widget widget, - string path, - Gdk.Rectangle background_area, - Gdk.Rectangle cell_area, - Gtk.CellRendererState flags - ) { - activated (path); - return true; - } - - // public override Gdk.Rectangle get_aligned_area ( - // Gtk.Widget widget, - // Gtk.CellRendererState flags, - // Gdk.Rectangle cell_area - // ) { - // return renderer_pixbuf.get_aligned_area (widget, flags, cell_area); - // } - // public override void get_preferred_height (Gtk.Widget widget, out int minimum_size, out int natural_size) { - // int m, n; - // renderer_pixbuf.get_preferred_height (widget, out m, out n); - // minimum_size = m; - // natural_size = n; - // } - // public override void get_preferred_height_for_width (Gtk.Widget widget, int width, out int minimum_height, out int natural_height) { - // int m, n; - // renderer_pixbuf.get_preferred_height_for_width (widget, width, out m, out n); - // minimum_height = m; - // natural_height = n; - // } - // public virtual void get_preferred_width (Gtk.Widget widget, out int minimum_size, out int natural_size) { - // int m, n; - // renderer_pixbuf.get_preferred_width (widget, out m, out n); - // minimum_size = m; - // natural_size = n; - // } - // public virtual void get_preferred_width_for_height (Gtk.Widget widget, int height, out int minimum_width, out int natural_width) { - // int m, n; - // renderer_pixbuf.get_preferred_width_for_height (widget, height, out m, out n); - // minimum_width = m; - // natural_width = n; - // } - // public override Gtk.SizeRequestMode get_request_mode () { - // return renderer_pixbuf.get_request_mode (); - // } - - // public override void snapshot (Gtk.Snapshot snapshot, Gtk.Widget widget, Gdk.Rectangle background_area, Gdk.Rectangle cell_area, Gtk.CellRendererState flags) { - // renderer_pixbuf.snapshot (snapshot, widget, background_area, cell_area, flags); - // } - } - - - - /** - * A cell renderer that only adds space. - */ - private class CellRendererSpacer : Gtk.CellRenderer { - /** - * Indentation level represented by this cell renderer - */ - public int level { get; set; default = -1; } - - public override Gtk.SizeRequestMode get_request_mode () { - return Gtk.SizeRequestMode.HEIGHT_FOR_WIDTH; - } - - public override void get_preferred_width (Gtk.Widget widget, out int min_size, out int natural_size) { - min_size = natural_size = 2 * (int) xpad; - } - - public override void get_preferred_height_for_width ( - Gtk.Widget widget, - int width, - out int min_height, - out int natural_height - ) { - min_height = natural_height = 2 * (int) ypad; - } - } - - /** - * The tree that actually displays the items. - * - * All the user interaction happens here. - */ - private class Tree : Gtk.TreeView { - - public DataModel data_model { get; construct set; } - - public signal void item_selected (Item? item); - - public Item? selected_item { - get { return selected; } - set { set_selected (value, true); } - } - - public bool editing { - get { return text_cell.editing; } - } - - public Pango.EllipsizeMode ellipsize_mode { - get { return text_cell.ellipsize; } - set { text_cell.ellipsize = value; } - } - - private enum Column { - ITEM, - N_COLS - } - - private Item? selected; - private unowned Item? edited; - - private Gtk.Entry? editable_entry; - private Gtk.CellRendererText text_cell; - private Gtk.EventControllerKey key_controller; - private Gtk.GestureClick button_controller; - private CellRendererIcon icon_cell; - private CellRendererIcon activatable_cell; - private CellRendererBadge badge_cell; - private CellRendererExpander primary_expander_cell; - private CellRendererExpander secondary_expander_cell; - private Gee.HashMap spacer_cells; // cells used for left spacing - private bool unselectable_item_clicked = false; - - private const string DEFAULT_STYLESHEET = """ - .sidebar.badge { - border-radius: 10px; - border-width: 0; - padding: 1px 2px 1px 2px; - font-weight: bold; - } - """; - - private const string STYLE_PROP_LEVEL_INDENTATION = "level-indentation"; - private const string STYLE_PROP_LEFT_PADDING = "left-padding"; - private const string STYLE_PROP_EXPANDER_SPACING = "expander-spacing"; - - // static construct { - // install_style_property (new ParamSpecInt ( - // STYLE_PROP_LEVEL_INDENTATION, - // "Level Indentation", - // "Space to add at the beginning of every indentation level. Must be an even number.", - // 1, - // 50, - // 6, - // ParamFlags.READABLE - // )); - - // install_style_property (new ParamSpecInt ( - // STYLE_PROP_LEFT_PADDING, - // "Left Padding", - // "Padding added to the left side of the tree. Must be an even number.", - // 1, - // 50, - // 4, - // ParamFlags.READABLE - // )); - - // install_style_property (new ParamSpecInt ( - // STYLE_PROP_EXPANDER_SPACING, - // "Expander Spacing", - // "Space added between an item and its expander. Must be an even number.", - // 1, - // 50, - // 4, - // ParamFlags.READABLE - // )); - // } - - public Tree (DataModel data_model) { - Object (data_model: data_model); - } - - construct { - add_css_class (Granite.STYLE_CLASS_SIDEBAR); - add_css_class ("source-list"); - - var css_provider = new Gtk.CssProvider (); - css_provider.load_from_string (DEFAULT_STYLESHEET); - Gtk.StyleContext.add_provider_for_display (Gdk.Display.get_default (), css_provider, Gtk.STYLE_PROVIDER_PRIORITY_FALLBACK); - - set_model (data_model); - - halign = valign = Gtk.Align.FILL; - hexpand = true; - vexpand = true; - - enable_search = false; - headers_visible = false; - enable_grid_lines = Gtk.TreeViewGridLines.NONE; - - // Deactivate GtkTreeView's built-in expander functionality - expander_column = null; - show_expanders = false; - - var item_column = new Gtk.TreeViewColumn (); - item_column.expand = true; - - insert_column (item_column, Column.ITEM); - - // Now pack the cell renderers. We insert them in reverse order (using pack_end) - // because we want to use TreeViewColumn.pack_start exclusively for inserting - // spacer cell renderers for level-indentation purposes. - // See add_spacer_cell_for_level() for more details. - - // Second expander. Used for main categories - secondary_expander_cell = new CellRendererExpander (); - secondary_expander_cell.is_category_expander = true; - secondary_expander_cell.xpad = 10; - item_column.pack_end (secondary_expander_cell, false); - item_column.set_cell_data_func (secondary_expander_cell, expander_cell_data_func); - - activatable_cell = new CellRendererIcon (); - activatable_cell.xpad = 6; - activatable_cell.activated.connect (on_activatable_activated); - item_column.pack_end (activatable_cell, false); - item_column.set_cell_data_func (activatable_cell, icon_cell_data_func); - - badge_cell = new CellRendererBadge (); - badge_cell.xpad = 1; - badge_cell.xalign = 1; - item_column.pack_end (badge_cell, false); - item_column.set_cell_data_func (badge_cell, badge_cell_data_func); - - text_cell = new Gtk.CellRendererText (); - text_cell.editable_set = true; - text_cell.editable = false; - text_cell.editing_started.connect (on_editing_started); - text_cell.editing_canceled.connect (on_editing_canceled); - text_cell.ellipsize = Pango.EllipsizeMode.END; - text_cell.xalign = 0; - item_column.pack_end (text_cell, true); - item_column.set_cell_data_func (text_cell, name_cell_data_func); - - icon_cell = new CellRendererIcon (); - icon_cell.xpad = 2; - item_column.pack_end (icon_cell, false); - item_column.set_cell_data_func (icon_cell, icon_cell_data_func); - - // First expander. Used for normal expandable items - primary_expander_cell = new CellRendererExpander (); - - int expander_spacing = 12; - primary_expander_cell.xpad = expander_spacing / 2; - - item_column.pack_end (primary_expander_cell, false); - item_column.set_cell_data_func (primary_expander_cell, expander_cell_data_func); - - // Selection - var selection = get_selection (); - selection.mode = Gtk.SelectionMode.BROWSE; - selection.set_select_function (select_func); - - // Monitor item changes - enable_item_property_monitor (); - - // Add root-level indentation. New levels will be added by update_item_expansion() - add_spacer_cell_for_level (1); - - // // Enable basic row drag and drop - // configure_drag_source (null); - // configure_drag_dest (null, 0); - - query_tooltip.connect_after (on_query_tooltip); - has_tooltip = true; - - var key_controller = new Gtk.EventControllerKey (); - this.add_controller (key_controller); - key_controller.key_released.connect (on_key_released); - - var button_controller = new Gtk.GestureClick () { - propagation_phase = CAPTURE, - button = 0 - }; - this.add_controller (button_controller); - button_controller.pressed.connect (on_button_pressed); - button_controller.released.connect (on_button_released); - } - - ~Tree () { - disable_item_property_monitor (); - } - - // public override bool drag_motion (Gdk.DragContext context, int x, int y, uint time) { - // // call the base signal to get rows with children to spring open - // if (!base.drag_motion (context, x, y, time)) - // return false; - - // Gtk.TreePath suggested_path, current_path; - // Gtk.TreeViewDropPosition suggested_pos, current_pos; - - // if (get_dest_row_at_pos (x, y, out suggested_path, out suggested_pos)) { - // // the base implementation of drag_motion was likely to set a drop - // // destination row. If that's the case, we configure the row position - // // to only allow drops before or after it, but not into it - // get_drag_dest_row (out current_path, out current_pos); - - // if (current_path != null && suggested_path.compare (current_path) == 0) { - // // If the source widget is this treeview, we assume we're - // // just dragging rows around, because at the moment dragging - // // rows into other rows (re-parenting) is not implemented. - // var source_widget = Gtk.drag_get_source_widget (context); - // bool dragging_treemodel_row = (source_widget == this); - - // if (dragging_treemodel_row) { - // // we don't allow DnD into other rows, only in between them - // // (no row is highlighted) - // if (current_pos != Gtk.TreeViewDropPosition.BEFORE) { - // if (current_pos == Gtk.TreeViewDropPosition.INTO_OR_BEFORE) - // set_drag_dest_row (current_path, Gtk.TreeViewDropPosition.BEFORE); - // else - // set_drag_dest_row (null, Gtk.TreeViewDropPosition.AFTER); - // } - // } else { - // // for DnD originated on a different widget, we don't want to insert - // // between rows, only select the rows themselves - // if (current_pos == Gtk.TreeViewDropPosition.BEFORE) - // set_drag_dest_row (current_path, Gtk.TreeViewDropPosition.INTO_OR_BEFORE); - // else if (current_pos == Gtk.TreeViewDropPosition.AFTER) - // set_drag_dest_row (current_path, Gtk.TreeViewDropPosition.INTO_OR_AFTER); - - // // determine if external DnD is supported by the item at destination - // var dest = data_model.get_item_from_path (current_path) as SourceListDragDest; - - // if (dest != null) { - // var target_list = Gtk.drag_dest_get_target_list (this); - // var target = Gtk.drag_dest_find_target (this, context, target_list); - - // // have 'drag_get_data' call 'drag_data_received' to determine - // // if the data can actually be dropped. - // context.set_data ("suggested-dnd-action", context.get_suggested_action ()); - // Gtk.drag_get_data (this, context, target, time); - // } else { - // // dropping data here is not supported. Unset dest row - // set_drag_dest_row (null, Gtk.TreeViewDropPosition.BEFORE); - // } - // } - // } - // } else { - // // dropping into blank areas of SourceList is not allowed - // set_drag_dest_row (null, Gtk.TreeViewDropPosition.AFTER); - // return false; - // } - - // return true; - // } - - // public override void drag_data_received ( - // Gdk.DragContext context, - // int x, - // int y, - // Gtk.SelectionData selection_data, - // uint info, - // uint time - // ) { - // var target_list = Gtk.drag_dest_get_target_list (this); - // var target = Gtk.drag_dest_find_target (this, context, target_list); - - // if (target == Gdk.Atom.intern_static_string ("GTK_TREE_MODEL_ROW")) { - // base.drag_data_received (context, x, y, selection_data, info, time); - // return; - // } - - // Gtk.TreePath path; - // Gtk.TreeViewDropPosition pos; - - // if (context.get_data ("suggested-dnd-action") != 0) { - // context.set_data ("suggested-dnd-action", 0); - - // get_drag_dest_row (out path, out pos); - - // if (path != null) { - // // determine if external DnD is allowed by the item at destination - // var dest = data_model.get_item_from_path (path) as SourceListDragDest; - - // if (dest == null || !dest.data_drop_possible (context, selection_data)) { - // // dropping data here is not allowed. unset any previously - // // selected destination row - // set_drag_dest_row (null, Gtk.TreeViewDropPosition.BEFORE); - // Gdk.drag_status (context, 0, time); - // return; - // } - // } - - // Gdk.drag_status (context, context.get_suggested_action (), time); - // } else { - // if (get_dest_row_at_pos (x, y, out path, out pos)) { - // // Data coming from external source/widget was dropped into this item. - // // selection_data contains something other than a tree row; most likely - // // we're dealing with a DnD not originated within the Source List tree. - // // Let's pass the data to the corresponding item, if there's a handler. - - // var drag_dest = data_model.get_item_from_path (path) as SourceListDragDest; - - // if (drag_dest != null) { - // var action = drag_dest.data_received (context, selection_data); - // Gtk.drag_finish (context, action != 0, action == Gdk.DragAction.MOVE, time); - // return; - // } - // } - - // // failure - // Gtk.drag_finish (context, false, false, time); - // } - // } - - // public void configure_drag_source (Gtk.TargetEntry[]? src_entries) { - // // Append GTK_TREE_MODEL_ROW to src_entries and src_entries to enable row DnD. - // var entries = append_row_target_entry (src_entries); - - // unset_rows_drag_source (); - // enable_model_drag_source (Gdk.ModifierType.BUTTON1_MASK, entries, Gdk.DragAction.MOVE); - // } - - // public void configure_drag_dest (Gtk.TargetEntry[]? dest_entries, Gdk.DragAction actions) { - // // Append GTK_TREE_MODEL_ROW to dest_entries and dest_entries to enable row DnD. - // var entries = append_row_target_entry (dest_entries); - - // unset_rows_drag_dest (); - - // // DragAction.MOVE needs to be enabled for row drag-and-drop to work properly - // enable_model_drag_dest (entries, Gdk.DragAction.MOVE | actions); - // } - - private bool on_query_tooltip (int x, int y, bool keyboard_tooltip, Gtk.Tooltip tooltip) { - Gtk.TreePath path; - Gtk.TreeViewColumn column = get_column (Column.ITEM); - - get_tooltip_context (x, y, keyboard_tooltip, null, out path, null); - if (path == null) { - return false; - } - - var item = data_model.get_item_from_path (path); - if (item != null) { - bool should_show = false; - - Gdk.Rectangle start_cell_area; - get_cell_area (path, column, out start_cell_area); - - set_tooltip_row (tooltip, path); - - if (item.tooltip == null) { - if (item.name != "") { - tooltip.set_markup (item.name); - should_show = true; - } - } else if (item.tooltip != "") { - tooltip.set_markup (item.tooltip); - should_show = true; - } - - if (keyboard_tooltip) { - return should_show; - } - - if (over_cell (column, path, text_cell, x - start_cell_area.x) || - over_cell (column, path, icon_cell, x - start_cell_area.x)) { - - return should_show; - } else if (over_cell (column, path, activatable_cell, x - start_cell_area.x)) { - if (item.activatable_tooltip == "") { - return false; - } else { - tooltip.set_markup (item.activatable_tooltip); - return true; - } - } - } - - return false; - } - - // private static Gtk.TargetEntry[] append_row_target_entry (Gtk.TargetEntry[]? orig) { - // const Gtk.TargetEntry row_target_entry = { // vala-lint=naming-convention - // "GTK_TREE_MODEL_ROW", - // Gtk.TargetFlags.SAME_WIDGET, - // 0 - // }; - - // var entries = new Gtk.TargetEntry[0]; - // entries += row_target_entry; - - // if (orig != null) { - // foreach (var target_entry in orig) - // entries += target_entry; - // } - - // return entries; - // } - - private void enable_item_property_monitor () { - data_model.item_updated.connect_after (on_model_item_updated); - } - - private void disable_item_property_monitor () { - data_model.item_updated.disconnect (on_model_item_updated); - } - - private void on_model_item_updated (Item item) { - // Currently, all the other properties are updated automatically by the - // cell-data functions after a change in the model. - var expandable_item = item as ExpandableItem; - if (expandable_item != null) - update_expansion (expandable_item); - } - - private void add_spacer_cell_for_level ( - int level, - bool check_previous = true - ) requires (level > 0) { - if (spacer_cells == null) - spacer_cells = new Gee.HashMap (); - - if (!spacer_cells.has_key (level)) { - var spacer_cell = new CellRendererSpacer (); - spacer_cell.level = level; - spacer_cells[level] = spacer_cell; - - uint cell_xpadding; - - // The primary expander is not visible for root-level (i.e. first level) - // items, so for the second level of indentation we use a low padding - // because the primary expander will add enough space. For the root level, - // we use left_padding, and level_indentation for the remaining levels. - // The value of cell_xpadding will be allocated *twice* by the cell renderer, - // so we set the value to a half of actual (desired) value. - switch (level) { - case 1: // root - int left_padding = 12; - cell_xpadding = left_padding / 2; - break; - - case 2: // second level - cell_xpadding = 0; - break; - - default: // remaining levels - int level_indentation = 12; - cell_xpadding = level_indentation / 2; - break; - } - - spacer_cell.xpad = cell_xpadding; - - var item_column = get_column (Column.ITEM); - item_column.pack_start (spacer_cell, false); - item_column.set_cell_data_func (spacer_cell, spacer_cell_data_func); - - // Make sure that the previous indentation levels also exist - if (check_previous) { - for (int i = level - 1; i > 0; i--) - add_spacer_cell_for_level (i, false); - } - } - } - - /** - * Evaluates whether the item at the specified path can be selected or not. - */ - private bool select_func ( - Gtk.TreeSelection selection, - Gtk.TreeModel model, - Gtk.TreePath path, - bool path_currently_selected - ) { - bool selectable = false; - var item = data_model.get_item_from_path (path); - - if (item != null) { - // Main categories ARE NOT selectable, so check for that - if (!data_model.is_category (item, null, path)) { - selectable = item.selectable; - } - } - - return selectable; - } - - private Gtk.TreePath? get_selected_path () { - Gtk.TreePath? selected_path = null; - Gtk.TreeSelection? selection = get_selection (); - - if (selection != null) { - Gtk.TreeModel? model; - var selected_rows = selection.get_selected_rows (out model); - if (selected_rows.length () == 1) - selected_path = selected_rows.nth_data (0); - } - - return selected_path; - } - - private void set_selected (Item? item, bool scroll_to_item) { - if (item == null) { - Gtk.TreeSelection? selection = get_selection (); - if (selection != null) - selection.unselect_all (); - - // As explained in cursor_changed(), we cannot emit signals for this special - // case from there because that wouldn't allow us to implement the behavior - // we want (i.e. restoring the old selection after expanding a previously - // collapsed category) without emitting the undesired item_selected() signal - // along the way. This special case is handled manually, because it *should* - // only happen in response to client code requests and never in response to - // user interaction. We do that here because there's no way to determine - // whether the cursor change came from code (i.e. this method) or user - // interaction from cursor_changed(). - this.selected = null; - item_selected (null); - } else if (item.selectable) { - if (scroll_to_item) - this.scroll_to_item (item); - - var to_select = data_model.get_item_path (item); - if (to_select != null) - set_cursor_on_cell (to_select, get_column (Column.ITEM), text_cell, false); - } - } - - public override void cursor_changed () { - var path = get_selected_path (); - Item? new_item = path != null ? data_model.get_item_from_path (path) : null; - - // Don't do anything if @new_item is null. - // - // The only way 'this.selected' can be null is by setting it explicitly to - // that value from client code, and thus we handle that case in set_selected(). - // THIS CANNOT HAPPEN IN RESPONSE TO USER INTERACTION. For example, if an - // item is un-selected because its parent category has been collapsed, then it will - // remain as the current selected item (not in reality, just as the value of - // this.selected) and will be re-selected after the parent is expanded again. - // THIS ALL HAPPENS SILENTLY BEHIND THE SCENES, so client code will never know - // it ever happened; the value of selected_item remains unchanged and item_selected() - // is not emitted. - if (new_item != null && new_item != this.selected) { - this.selected = new_item; - item_selected (new_item); - } - } - - public bool scroll_to_item (Item item, bool use_align = false, float row_align = 0) { - bool scrolled = false; - - var path = data_model.get_item_path (item); - if (path != null) { - scroll_to_cell (path, null, use_align, row_align, 0); - scrolled = true; - } - - return scrolled; - } - - public bool start_editing_item (Item item) requires (item.editable) requires (item.selectable) { - if (editing && item == edited) {// If same item again, simply return. - return false; - } - - var path = data_model.get_item_path (item); - if (path != null) { - edited = item; - text_cell.editable = true; - set_cursor_on_cell (path, get_column (Column.ITEM), text_cell, true); - } else { - warning ("Could not edit \"%s\": path not found", item.name); - } - - return editing; - } - - public void stop_editing () { - if (editing && edited != null) { - var path = data_model.get_item_path (edited); - - // Setting the cursor on the same cell without starting an edit cancels any - // editing operation going on. - if (path != null) - set_cursor_on_cell (path, get_column (Column.ITEM), text_cell, false); - } - } - - private void on_editing_started (Gtk.CellEditable editable, string path) { - editable_entry = editable as Gtk.Entry; - if (editable_entry != null) { - editable_entry.editing_done.connect (on_editing_done); - editable_entry.editable = true; - } - } - - private void on_editing_canceled () { - if (editable_entry != null) { - editable_entry.editable = false; - editable_entry.editing_done.disconnect (on_editing_done); - } - - text_cell.editable = false; - edited = null; - } - - private void on_editing_done () { - if (edited != null && edited.editable && editable_entry != null) - edited.edited (editable_entry.get_text ()); - - // Same actions as when canceling editing - on_editing_canceled (); - } - - private void on_activatable_activated (string item_path_str) { - var item = get_item_from_path_string (item_path_str); - if (item != null) - item.action_activated (); - } - - private Item? get_item_from_path_string (string item_path_str) { - var item_path = new Gtk.TreePath.from_string (item_path_str); - return data_model.get_item_from_path (item_path); - } - - private bool toggle_expansion (ExpandableItem item) { - if (item.collapsible) { - item.expanded = !item.expanded; - return true; - } - return false; - } - - /** - * Updates the tree to reflect the ''expanded'' property of expandable_item. - */ - public void update_expansion (ExpandableItem expandable_item) { - var path = data_model.get_item_path (expandable_item); - - if (path != null) { - // Make sure that the indentation cell for the item's level exists. - // We use +1 because the method will make sure that the previous - // indentation levels exist too. - add_spacer_cell_for_level (path.get_depth () + 1); - - if (expandable_item.expanded) { - expand_row (path, false); - - // Since collapsing an item un-selects any child item previously selected, - // we need to restore the selection. This will be done silently because - // set_selected checks for equality between the previously "selected" - // item and the newly selected, and only emits the item_selected() signal - // if they are different. See cursor_changed() for a better explanation - // of this behavior. - if (selected != null && selected.parent == expandable_item) - set_selected (selected, true); - - // Collapsing expandable_item's row also collapsed all its children, - // and thus we need to update the "expanded" property of each of them - // to reflect their previous state. - foreach (var child_item in expandable_item.children) { - var child_expandable_item = child_item as ExpandableItem; - if (child_expandable_item != null) - update_expansion (child_expandable_item); - } - } else { - collapse_row (path); - } - } - } - - public override void row_expanded (Gtk.TreeIter iter, Gtk.TreePath path) { - var item = data_model.get_item (iter) as ExpandableItem; - return_if_fail (item != null); - - disable_item_property_monitor (); - item.expanded = true; - enable_item_property_monitor (); - } - - public override void row_collapsed (Gtk.TreeIter iter, Gtk.TreePath path) { - var item = data_model.get_item (iter) as ExpandableItem; - return_if_fail (item != null); - - disable_item_property_monitor (); - item.expanded = false; - enable_item_property_monitor (); - } - - public override void row_activated (Gtk.TreePath path, Gtk.TreeViewColumn? column) { - if (column != null && column == get_column (Column.ITEM)) { - var item = data_model.get_item_from_path (path); - if (item != null) { - item.activated (); - } - } - } - - private void on_key_released (uint keyval, uint keycode, Gdk.ModifierType state) { - if (selected_item == null) { - return; - } - - switch (keyval) { - case Gdk.Key.F2: - var modifiers = Gtk.accelerator_get_default_mod_mask (); - // try to start editing selected item - if ((state & modifiers) == 0 && selected_item.editable) - start_editing_item (selected_item); - break; - } - } - - private void on_button_released (int n_press, double px, double py) { - if (unselectable_item_clicked) { - unselectable_item_clicked = false; - - Gtk.TreePath path; - Gtk.TreeViewColumn column; - int x = (int) px, y = (int) py, cell_x, cell_y; - - if (get_path_at_pos (x, y, out path, out column, out cell_x, out cell_y)) { - var item = data_model.get_item_from_path (path) as ExpandableItem; - - if (item != null) { - if (!item.selectable || data_model.is_category (item, null, path)) - toggle_expansion (item); - } - } - } - } - - private void on_button_pressed (int n_press, double dx, double dy) { - Gtk.TreePath path; - Gtk.TreeViewColumn column; - int x = (int) dx, y = (int) dy, cell_x, cell_y; - - if (get_path_at_pos (x, y, out path, out column, out cell_x, out cell_y)) { - var item = data_model.get_item_from_path (path); - - // This is needed because the treeview adds an offset at the beginning of every level - Gdk.Rectangle start_cell_area; - get_cell_area (path, get_column (0), out start_cell_area); - cell_x -= start_cell_area.x; - - if (item != null && column == get_column (Column.ITEM)) { - // Cancel any editing operation going on - stop_editing (); - - var button = button_controller.get_current_button (); - if (button == Gdk.BUTTON_SECONDARY) { - popup_context_menu (item, (int) dx, (int) dy); - } else if (button == Gdk.BUTTON_PRIMARY) { - // Check whether an expander (or an equivalent area) was clicked. - bool is_expandable = item is ExpandableItem; - bool is_category = is_expandable && data_model.is_category (item, null, path); - if (n_press == 1) { - if (is_expandable) { - // Checking for secondary_expander_cell is not necessary because the entire row - // serves for this purpose when the item is a category or when the item is a - // normal expandable item that is not selectable (special care is taken to - // not break the activatable/action icons for such cases). - // The expander only works like a visual indicator for these items. - unselectable_item_clicked = is_category - || (!item.selectable && !over_cell (column, path, activatable_cell, cell_x)); - - if (!unselectable_item_clicked && over_primary_expander (column, path, cell_x)) { - toggle_expansion (item as ExpandableItem); - } - } - } else if ( - n_press == 2 - && !is_category // Main categories are *not* editable - && item.editable - && item.selectable - && over_cell (column, path, text_cell, cell_x) - ) { - // Start editing after native event handlers finished else fails - Idle.add (() => { start_editing_item (item); return Source.REMOVE; }); - } - } - } - } - } - - private bool over_primary_expander (Gtk.TreeViewColumn col, Gtk.TreePath path, int x) { - Gtk.TreeIter iter; - if (!model.get_iter (out iter, path)) - return false; - - // Call the cell-data function and make it assign the proper visibility state to the cell - expander_cell_data_func (col, primary_expander_cell, model, iter); - - if (!primary_expander_cell.visible) - return false; - - // We want to return false if the cell is not expandable (i.e. the arrow is hidden) - if (model.iter_n_children (iter) < 1) - return false; - - // Now that we're sure that the item is expandable, let's see if the user clicked - // over the expander area. We don't do so directly by querying the primary expander - // position because it's not fixed, yielding incorrect coordinates depending on whether - // a different area was re-drawn before this method was called. We know that the last - // spacer cell precedes (in a LTR fashion) the expander cell. Because the position - // of the spacer cell is fixed, we can safely query it. - int indentation_level = path.get_depth (); - var last_spacer_cell = spacer_cells[indentation_level]; - - if (last_spacer_cell != null) { - int cell_x, cell_width; - - if (col.cell_get_position (last_spacer_cell, out cell_x, out cell_width)) { - // Add a pixel so that the expander area is a bit wider - int expander_width = get_cell_width (primary_expander_cell) + 1; - - var dir = get_direction (); - if (dir == Gtk.TextDirection.NONE) { - dir = Gtk.Widget.get_default_direction (); - } - - if (dir == Gtk.TextDirection.LTR) { - int indentation_offset = cell_x + cell_width; - return x >= indentation_offset && x <= indentation_offset + expander_width; - } - - return x <= cell_x && x >= cell_x - expander_width; - } - } - - return false; - } - - private bool over_cell (Gtk.TreeViewColumn col, Gtk.TreePath path, Gtk.CellRenderer cell, int x) { - int cell_x, cell_width; - bool found = col.cell_get_position (cell, out cell_x, out cell_width); - return found && x > cell_x && x < cell_x + cell_width; - } - - private int get_cell_width (Gtk.CellRenderer cell_renderer) { - Gtk.Requisition min_req; - cell_renderer.get_preferred_size (this, out min_req, null); - return min_req.width; - } - - private bool popup_context_menu (Item? item = null, int px = 0, int py = 0) { - if (item == null) { - item = selected_item; - } - - if (item != null) { - var menu_model = item.get_context_menu (); - if (menu_model != null) { - var menu = new Gtk.PopoverMenu.from_model (menu_model) { - position = RIGHT - }; - - menu.pointing_to = Gdk.Rectangle () { x = px, y = py }; - menu.popup (); - } - } - - return false; - } - - private static Item? get_item_from_model (Gtk.TreeModel model, Gtk.TreeIter iter) { - var data_model = model as DataModel; - assert (data_model != null); - return data_model.get_item (iter); - } - - private static void spacer_cell_data_func ( - Gtk.CellLayout layout, - Gtk.CellRenderer renderer, - Gtk.TreeModel model, - Gtk.TreeIter iter - ) { - var spacer = renderer as CellRendererSpacer; - assert (spacer != null); - assert (spacer.level > 0); - - var path = model.get_path (iter); - - int level = -1; - if (path != null) - level = path.get_depth (); - - renderer.visible = spacer.level <= level; - } - - private void name_cell_data_func ( - Gtk.CellLayout layout, - Gtk.CellRenderer renderer, - Gtk.TreeModel model, - Gtk.TreeIter iter - ) { - var text_renderer = renderer as Gtk.CellRendererText; - assert (text_renderer != null); - - var text = new StringBuilder (); - var weight = Pango.Weight.NORMAL; - bool use_markup = false; - - var item = get_item_from_model (model, iter); - if (item != null) { - if (item.markup != null) { - text.append (item.markup); - use_markup = true; - } else { - text.append (item.name); - } - - if (data_model.is_category (item, iter)) - weight = Pango.Weight.BOLD; - } - - text_renderer.weight = weight; - if (use_markup) { - text_renderer.markup = text.str; - } else { - text_renderer.text = text.str; - } - } - - private void badge_cell_data_func ( - Gtk.CellLayout layout, - Gtk.CellRenderer renderer, - Gtk.TreeModel model, - Gtk.TreeIter iter - ) { - var badge_renderer = renderer as CellRendererBadge; - assert (badge_renderer != null); - - string text = ""; - bool visible = false; - - var item = get_item_from_model (model, iter); - if (item != null) { - // Badges are not displayed for main categories - visible = !data_model.is_category (item, iter) - && item.badge != null - && item.badge.strip () != ""; - - if (visible) - text = item.badge; - } - - badge_renderer.visible = visible; - badge_renderer.text = text; - } - - private void icon_cell_data_func ( - Gtk.CellLayout layout, - Gtk.CellRenderer renderer, - Gtk.TreeModel model, Gtk.TreeIter iter - ) { - var icon_renderer = renderer as CellRendererIcon; - assert (icon_renderer != null); - - bool visible = false; - Icon? icon = null; - - var item = get_item_from_model (model, iter); - if (item != null) { - // Icons are not displayed for main categories - visible = !data_model.is_category (item, iter); - - if (visible) { - if (icon_renderer == icon_cell) - icon = item.icon; - else if (icon_renderer == activatable_cell) - icon = item.activatable; - else - assert_not_reached (); - } - } - - visible = visible && icon != null; - - icon_renderer.visible = visible; - icon_renderer.gicon = visible ? icon : null; - } - - /** - * Controls expander visibility. - */ - private void expander_cell_data_func ( - Gtk.CellLayout layout, - Gtk.CellRenderer renderer, - Gtk.TreeModel model, - Gtk.TreeIter iter - ) { - var item = get_item_from_model (model, iter); - if (item != null) { - // Gtk.CellRenderer.is_expander takes into account whether the item has children or not. - // The tree-view checks for that and sets this property for us. It also sets - // Gtk.CellRenderer.is_expanded, and thus we don't need to check for that either. - var expandable_item = item as ExpandableItem; - if (expandable_item != null) - renderer.is_expander = renderer.is_expander && expandable_item.collapsible; - } - - if (renderer == primary_expander_cell) - renderer.visible = !data_model.is_iter_at_root_level (iter); - else if (renderer == secondary_expander_cell) - renderer.visible = data_model.is_category (item, iter); - else - assert_not_reached (); - } - } - - - - /** - * Emitted when the source list selection changes. - * - * @param item Selected item; //null// if nothing is selected. - * @since 0.2 - */ - public virtual signal void item_selected (Item? item) { } - - /** - * A {@link Code.Widgets.SourceList.VisibleFunc} should return true if the item should be - * visible; false otherwise. If //item//'s {@link Code.Widgets.SourceList.Item.visible} - * property is set to //false//, then it won't be displayed even if this method returns //true//. - * - * It is important to note that the method ''must not modify any property of //item//''. - * Doing so would result in an infinite loop, freezing the application's user interface. - * This happens because the source list invokes this method to "filter" an item after - * any of its properties changes, so by modifying a property this method would be invoking - * itself again. - * - * For most use cases, modifying the {@link Code.Widgets.SourceList.Item.visible} property is enough. - * - * The advantage of using this method is that its nature is non-destructive, and the - * changes it makes can be easily reverted (see {@link Code.Widgets.SourceList.refilter}). - * - * @param item Item to be checked. - * @return Whether //item// should be visible or not. - * @since 0.2 - */ - public delegate bool VisibleFunc (Item item); - - /** - * Root-level expandable item. - * - * This item contains the first-level source list items. It //only serves as an item container//. - * It is used to add and remove items to/from the widget. - * - * Internally, it allows the source list to connect to its {@link Code.Widgets.SourceList.ExpandableItem.child_added} - * and {@link Code.Widgets.SourceList.ExpandableItem.child_removed} signals in order to monitor - * new children additions/removals. - * - * @since 0.2 - */ - public ExpandableItem root { - get { return data_model.root; } - set { data_model.root = value; } - } - - /** - * The current selected item. - * - * Setting it to //null// un-selects the previously selected item, if there was any. - * {@link Code.Widgets.SourceList.ExpandableItem.expand_with_parents} is called on the - * item's parent to make sure it's possible to select it. - * - * @since 0.2 - */ - public Item? selected { - get { return tree.selected_item; } - set { - if (value != null && value.parent != null) - value.parent.expand_with_parents (); - tree.selected_item = value; - } - } - - /** - * Text ellipsize mode. - * - * @since 0.2 - */ - public Pango.EllipsizeMode ellipsize_mode { - get { return tree.ellipsize_mode; } - set { tree.ellipsize_mode = value; } - } - - /** - * Whether an item is being edited. - * - * @see Code.Widgets.SourceList.start_editing_item - * @since 0.2 - */ - public bool editing { - get { return tree.editing; } - } - - public bool activate_on_single_click { - set { - tree.activate_on_single_click = value; - } - } - - public Gtk.Adjustment vadjustment { - get { - return scrolled_window.vadjustment; - } - } - private Tree tree; - private Gtk.ScrolledWindow scrolled_window; - private DataModel data_model = new DataModel (); - /** - * Creates a new {@link Code.Widgets.SourceList}. - * - * @return A new {@link Code.Widgets.SourceList}. - * @since 0.2 - */ - public SourceList (ExpandableItem root = new ExpandableItem ()) { - this.root = root; - } - - construct { - tree = new Tree (data_model); - scrolled_window = new Gtk.ScrolledWindow () { - child = tree - }; - scrolled_window.set_policy (Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC); - append (scrolled_window); - tree.item_selected.connect ((item) => item_selected (item)); - } - - /** - * Checks whether //item// is part of the source list. - * - * @param item The item to query. - * @return //true// if the item belongs to the source list; //false// otherwise. - * @since 0.2 - */ - public bool has_item (Item item) { - return data_model.has_item (item); - } - - /** - * Sets the method used for filtering out items. - * - * @param visible_func The method to use for filtering items. - * @param refilter Whether to call {@link Code.Widgets.SourceList.refilter} using the new function. - * @see Code.Widgets.SourceList.VisibleFunc - * @see Code.Widgets.SourceList.refilter - * @since 0.2 - */ - public void set_filter_func (VisibleFunc? visible_func, bool refilter) { - data_model.set_filter_func (visible_func); - if (refilter) - this.refilter (); - } - - /** - * Applies the filter method set by {@link Code.Widgets.SourceList.set_filter_func} - * to all the items that are part of the current tree. - * - * @see Code.Widgets.SourceList.VisibleFunc - * @see Code.Widgets.SourceList.set_filter_func - * @since 0.2 - */ - public void refilter () { - data_model.refilter (); - } - - /** - * Queries the actual expansion state of //item//. - * - * @see Code.Widgets.SourceList.ExpandableItem.expanded - * @return Whether //item// is expanded or not. - * @since 0.2 - */ - public bool is_item_expanded (Item item) requires (has_item (item)) { - var path = data_model.get_item_path (item); - return path != null && tree.is_row_expanded (path); - } - - /** - * If //item// is editable, this activates the editor; otherwise, it does nothing. - * If an item was already being edited, this will fail. - * - * @param item Item to edit. - * @see Code.Widgets.SourceList.Item.editable - * @see Code.Widgets.SourceList.editing - * @see Code.Widgets.SourceList.stop_editing - * @return true if the editing started successfully; false otherwise. - * @since 0.2 - */ - public bool start_editing_item (Item item) requires (has_item (item)) { - return tree.start_editing_item (item); - } - - /** - * Cancels any editing operation going on. - * - * @see Code.Widgets.SourceList.editing - * @see Code.Widgets.SourceList.start_editing_item - * @since 0.2 - */ - public void stop_editing () { - if (editing) - tree.stop_editing (); - } - - /** - * Turns Source List into a //drag source//. - * - * This enables items that implement {@link Code.Widgets.SourceListDragSource} - * to be dragged outside the Source List and drop data into external widgets. - * - * @param src_entries an array of {@link Gtk.TargetEntry}s indicating the targets - * that the drag will support. - * @see Code.Widgets.SourceListDragSource - * @see Code.Widgets.SourceList.disable_drag_source - * @since 0.3 - */ - // public void enable_drag_source (Gtk.TargetEntry[] src_entries) { - // tree.configure_drag_source (src_entries); - // } - - /** - * Undoes the effect of {@link Code.Widgets.SourceList.enable_drag_source} - * - * @see Code.Widgets.SourceList.enable_drag_source - * @since 0.3 - */ - // public void disable_drag_source () { - // tree.configure_drag_source (null); - // } - - /** - * Turns Source List into a //drop destination//. - * - * This enables items that implement {@link Code.Widgets.SourceListDragDest} - * to receive data from external widgets via drag-and-drop. - * - * @param dest_entries an array of {@link Gtk.TargetEntry}s indicating the drop - * types that Source List items will accept. - * @param actions a bitmask of possible actions for a drop onto Source List items. - * @see Code.Widgets.SourceListDragDest - * @see Code.Widgets.SourceList.disable_drag_dest - * @since 0.3 - */ - // public void enable_drag_dest (Gtk.TargetEntry[] dest_entries, Gdk.DragAction actions) { - // tree.configure_drag_dest (dest_entries, actions); - // } - - /** - * Undoes the effect of {@link Code.Widgets.SourceList.enable_drag_dest} - * - * @see Code.Widgets.SourceList.enable_drag_dest - * @since 0.3 - */ - // public void disable_drag_dest () { - // tree.configure_drag_dest (null, 0); - // } - - /** - * Scrolls the source list tree to make //item// visible. - * - * {@link Code.Widgets.SourceList.ExpandableItem.expand_with_parents} is called - * for the item's parent if //expand_parents// is //true//, to make sure it's not - * hidden behind a collapsed row. - * - * If use_align is //false//, then the row_align argument is ignored, and the tree - * does the minimum amount of work to scroll the item onto the screen. This means that - * the item will be scrolled to the edge closest to its current position. If the item - * is currently visible on the screen, nothing is done. - * - * @param item Item to scroll to. - * @param expand_parents Whether to recursively expand item's parent in case they are collapsed. - * @param use_align Whether to use the //row_align// argument. - * @param row_align The vertical alignment of //item//. 0.0 means top, 0.5 center, and 1.0 bottom. - * @return //true// if successful; //false// otherwise. - * @since 0.2 - */ - public bool scroll_to_item ( - Item item, - bool expand_parents = true, - bool use_align = false, - float row_align = 0 - ) requires (has_item (item)) { - if (expand_parents && item.parent != null) - item.parent.expand_with_parents (); - - return tree.scroll_to_item (item, use_align, row_align); - } - - /** - * Gets the previous item with respect to //reference//. - * - * @param reference Item to use as reference. - * @return The item that appears before //reference//, or //null// if there's none. - * @since 0.2 - */ - public Item? get_previous_item (Item reference) requires (has_item (reference)) { - // this will return null for root, so iter_n_children() will always work fine - var iter = data_model.get_item_iter (reference); - if (iter != null) { - Gtk.TreeIter new_iter = iter; // workaround for valac 0.18 - if (data_model.iter_previous (ref new_iter)) - return data_model.get_item (new_iter); - } - - return null; - } - - /** - * Gets the next item with respect to //reference//. - * - * @param reference Item to use as reference. - * @return The item that appears after //reference//, or //null// if there's none. - * @since 0.2 - */ - public Item? get_next_item (Item reference) requires (has_item (reference)) { - // this will return null for root, so iter_n_children() will always work fine - var iter = data_model.get_item_iter (reference); - if (iter != null) { - Gtk.TreeIter new_iter = iter; // workaround for valac 0.18 - if (data_model.iter_next (ref new_iter)) - return data_model.get_item (new_iter); - } - - return null; - } - - /** - * Gets the first visible child of an expandable item. - * - * @param parent Parent of the child to look up. - * @return The first visible child of //parent//, or null if it was not found. - * @since 0.2 - */ - public Item? get_first_child (ExpandableItem parent) { - return get_nth_child (parent, 0); - } - - /** - * Gets the last visible child of an expandable item. - * - * @param parent Parent of the child to look up. - * @return The last visible child of //parent//, or null if it was not found. - * @since 0.2 - */ - public Item? get_last_child (ExpandableItem parent) { - return get_nth_child (parent, (int) get_n_visible_children (parent) - 1); - } - - /** - * Gets the number of visible children of an expandable item. - * - * @param parent Item to query. - * @return Number of visible children of //parent//. - * @since 0.2 - */ - public uint get_n_visible_children (ExpandableItem parent) { - // this will return null for root, so iter_n_children() will always work properly. - var parent_iter = data_model.get_item_iter (parent); - return data_model.iter_n_children (parent_iter); - } - - private Item? get_nth_child (ExpandableItem parent, int index) { - if (index < 0) - return null; - - // this will return null for root, so iter_nth_child() will always work properly. - var parent_iter = data_model.get_item_iter (parent); - - Gtk.TreeIter child_iter; - if (data_model.iter_nth_child (out child_iter, parent_iter, index)) - return data_model.get_item (child_iter); - - return null; - } - - public new void grab_focus () { - tree.grab_focus (); - } -} -} diff --git a/src/Widgets/SourceView.vala b/src/Widgets/SourceView.vala index df88dd3f15..7101aa5130 100644 --- a/src/Widgets/SourceView.vala +++ b/src/Widgets/SourceView.vala @@ -28,7 +28,7 @@ namespace Scratch.Widgets { public Gtk.TextTag error_tag; public GLib.File location { get; set; } - public FolderManager.ProjectFolderItem project { get; set; default = null; } + public Code.ProjectFolderItem project { get; set; default = null; } public SimpleActionGroup actions { get; construct; } private string font; @@ -109,12 +109,12 @@ namespace Scratch.Widgets { draw_spaces_tag.draw_spaces = true; source_buffer.tag_table.add (draw_spaces_tag); - // Make the gutter renderer and insert into the left side of the source view. - git_diff_gutter_renderer = new GitGutterRenderer (); - navmark_gutter_renderer = new NavMarkGutterRenderer (source_buffer); + // // Make the gutter renderer and insert into the left side of the source view. + // git_diff_gutter_renderer = new GitGutterRenderer (); + // navmark_gutter_renderer = new NavMarkGutterRenderer (source_buffer); - get_gutter (Gtk.TextWindowType.LEFT).insert (git_diff_gutter_renderer, 10); - get_gutter (Gtk.TextWindowType.LEFT).insert (navmark_gutter_renderer, -48); + // get_gutter (Gtk.TextWindowType.LEFT).insert (git_diff_gutter_renderer, 10); + // get_gutter (Gtk.TextWindowType.LEFT).insert (navmark_gutter_renderer, -48); smart_home_end = GtkSource.SmartHomeEndType.AFTER; @@ -216,11 +216,11 @@ namespace Scratch.Widgets { buffer.notify_property ("has-selection"); buffer.notify_property ("language"); - navmark_gutter_renderer.notify["has-marks"].connect (() => { - next_mark_action.set_enabled (navmark_gutter_renderer.has_marks); - prev_mark_action.set_enabled (next_mark_action.get_enabled ()); - }); - navmark_gutter_renderer.notify_property ("has-marks"); + // navmark_gutter_renderer.notify["has-marks"].connect (() => { + // next_mark_action.set_enabled (navmark_gutter_renderer.has_marks); + // prev_mark_action.set_enabled (next_mark_action.get_enabled ()); + // }); + // navmark_gutter_renderer.notify_property ("has-marks"); // // For Gtk3 we need to convert extra_menu to additional Gtk.MenuItems. This is omitted in Gtk4 // populate_popup.connect_after ((menu) => { @@ -383,7 +383,7 @@ namespace Scratch.Widgets { source_buffer.style_scheme = scheme ?? style_scheme_manager.get_scheme ("classic"); } - git_diff_gutter_renderer.set_style_scheme (source_buffer.style_scheme); + // git_diff_gutter_renderer.set_style_scheme (source_buffer.style_scheme); style_changed (source_buffer.style_scheme); } @@ -620,7 +620,7 @@ namespace Scratch.Widgets { Gtk.TextIter? cur_iter = null; buffer.get_iter_at_offset (out cur_iter, cursor_position); var cur_line = cur_iter.get_line (); - navmark_gutter_renderer.add_mark_at_line (cur_line); + // navmark_gutter_renderer.add_mark_at_line (cur_line); } public void goto_previous_mark () { @@ -635,12 +635,12 @@ namespace Scratch.Widgets { Gtk.TextIter? start, end; int line; if (get_current_line (out start, out end)) { - navmark_gutter_renderer.get_nearest_marked_line (start.get_line (), before, out line); - Gtk.TextIter? iter; - buffer.get_iter_at_line (out iter, line); - if (iter != null) { - cursor_position = iter.get_offset (); - } + // navmark_gutter_renderer.get_nearest_marked_line (start.get_line (), before, out line); + // Gtk.TextIter? iter; + // buffer.get_iter_at_line (out iter, line); + // if (iter != null) { + // cursor_position = iter.get_offset (); + // } } } @@ -709,7 +709,7 @@ namespace Scratch.Widgets { var name = mark.get_name (); if (name != null && name.has_prefix ("NavMark")) { warning ("NavMark deleted"); - navmark_gutter_renderer.remove_mark (mark); + // navmark_gutter_renderer.remove_mark (mark); } } @@ -736,14 +736,14 @@ namespace Scratch.Widgets { refresh_timeout_id = Timeout.add (250, () => { refresh_timeout_id = 0; if (project != null && project.is_git_repo) { - git_diff_gutter_renderer.line_status_map.clear (); + // git_diff_gutter_renderer.line_status_map.clear (); project.refresh_diff (ref git_diff_gutter_renderer.line_status_map, location.get_path ()); - git_diff_gutter_renderer.queue_draw (); + // git_diff_gutter_renderer.queue_draw (); } - if (navmark_gutter_renderer.has_marks) { - navmark_gutter_renderer.queue_draw (); - } + // if (navmark_gutter_renderer.has_marks) { + // navmark_gutter_renderer.queue_draw (); + // } return Source.REMOVE; }); diff --git a/src/Widgets/TreeList/TreeList.vala b/src/Widgets/TreeList/TreeList.vala deleted file mode 100644 index b5830733f4..0000000000 --- a/src/Widgets/TreeList/TreeList.vala +++ /dev/null @@ -1,197 +0,0 @@ -/* - * SPDX-License-Identifier: GPL-3.0-or-later - * SPDX-FileCopyrightText: 20126 elementary, Inc. - */ - -public class Code.TreeList : Gtk.Box { - private Gtk.ListView list_view; - protected GLib.ListStore root_model; - private Gtk.TreeListModel tree_model; - protected Gtk.SelectionModel selection_model; - - public bool activate_on_single_click { get; set; } - - public signal void item_activated (TreeListItem item); - // public TreeListItem? selected { get; set; } // Selection handled by SelectionModel - - construct { - root_model = new GLib.ListStore (typeof (TreeListItem)); - // Passthrough false (create Gtk.TreeListRows), autoexpand false - tree_model = new Gtk.TreeListModel (root_model, false, false, create_model_func); - selection_model = new Gtk.SingleSelection (tree_model); - var tree_list_factory = new Gtk.SignalListItemFactory (); - var tree_header_factory = new Gtk.SignalListItemFactory (); - list_view = new Gtk.ListView (selection_model, tree_list_factory) { - header_factory = tree_header_factory - }; - - bind_property ("activate-on-single-click", list_view, "activate-on-single-click", BIDIRECTIONAL | SYNC_CREATE); - list_view.activate.connect ((pos) => { - item_activated ((TreeListItem) selection_model.get_item (pos)); - }); - // LIST ITEM FACTORY HANDLERS - tree_list_factory.setup.connect ((obj) => { - var listitem = (Gtk.ListItem) obj; - create_listitem_child (listitem); - // By default just create a use a label (not expandable) - }); - tree_list_factory.teardown.connect ((obj) => { - var listitem = (Gtk.ListItem) obj; - teardown_listitem_child (listitem); - }); - tree_list_factory.bind.connect ((obj) => { - var listitem = (Gtk.ListItem) obj; - var treelistrow = (Gtk.TreeListRow) (listitem.get_item ()); - var data = (Code.TreeListItem) (treelistrow.get_item ()); - bind_data_to_row (data, treelistrow, listitem); - }); - tree_list_factory.unbind.connect ((obj) => { - var listitem = (Gtk.ListItem) obj; - var treelistrow = (Gtk.TreeListRow) (listitem.item); - var data = (Code.TreeListItem) (treelistrow.item); - unbind_data_from_row (data, treelistrow, listitem); - }); - - // HEADER FACTORY HANDLERS - tree_header_factory.setup.connect ((obj) => { - // By default create header and subheader, expandable - var listitem = (Gtk.ListItem) obj; - create_headeritem_child (listitem); - }); - tree_header_factory.bind.connect ((obj) => { - var listitem = (Gtk.ListItem) obj; - var treelistrow = (Gtk.TreeListRow) listitem.item; - var data = (Code.TreeListItem) treelistrow.item; - }); - tree_header_factory.unbind.connect (() => {}); - tree_header_factory.teardown.connect (() => {}); - - append (list_view); - } - - protected virtual void create_listitem_child (Gtk.ListItem item) { - var label = new Gtk.Label ("") { - halign = START, - }; - label.add_css_class (Granite.STYLE_CLASS_H4_LABEL); - item.child = label; - } - protected virtual void teardown_listitem_child (Gtk.ListItem item) { - // Must be paired with create_listitem child - } - protected virtual void bind_data_to_row ( - TreeListItem data, - Gtk.TreeListRow row, - Gtk.ListItem item) { - // Must be matched with create item widget when overriding - var label = (Gtk.Label)(item.child); - label.label = data.text; - } - protected virtual void unbind_data_from_row ( - TreeListItem data, - Gtk.TreeListRow row, - Gtk.ListItem item - ) { - //Must undo any signal connections etc made in bind_data_to_row - } - - protected virtual void create_headeritem_child (Gtk.ListItem item) { - var text_label = new Gtk.Label ("") { - halign = START, - }; - text_label.add_css_class (Granite.STYLE_CLASS_H3_LABEL); - var subtext_label = new Gtk.Label ("") { - halign = START - }; - subtext_label.add_css_class (Granite.STYLE_CLASS_SMALL_LABEL); - var box = new Gtk.Box (VERTICAL, 0); - box.append (text_label); - box.append (subtext_label); - var expander = new Gtk.TreeExpander () { - child = box, - }; - item.child = expander; - } - protected virtual void teardown_headeritem_child () { - // Must be paired with create_listitem child - } - protected virtual void bind_data_to_headeritem ( - TreeListItem data, - Gtk.TreeListRow row, - Gtk.ListItem item - ) { - var expander = (Gtk.TreeExpander) item.child; - var box = (Gtk.Box) expander.get_child (); - var text_label = (Gtk.Label) box.get_first_child (); - var subtext_label = (Gtk.Label) text_label.get_next_sibling (); - text_label.label = data.text; - subtext_label.label = data.sub_text; - } - - protected virtual void unbind_data_from_headerrow ( - TreeListItem data, - Gtk.TreeListRow row, - Gtk.ListItem item - ) { - //Must undo any signal connections etc made in bind_data_to_row - } - - public ListModel? create_model_func (Object item) { - var data = (TreeListItem) item; - if (data.is_expandable && data.child_model == null) { - data.child_model = new GLib.ListStore (typeof (TreeListItem)); - } - - return data.child_model; - } - - // Root items must generally be expandable - public TreeListItem add_root_item ( - TreeListItem item - ) { - root_model.append (item); - return item; - } - - public void remove_root_item (TreeListItem item) { - uint pos; - if (root_model.find (item, out pos)) { - root_model.remove (pos); - } - } - - public void remove_root_children (List to_remove) { - foreach (TreeListItem item in to_remove) { - uint pos; - if (root_model.find (item, out pos)) { - root_model.remove (pos); - } - } - } - - public void sort_root_children (CompareDataFunc sort_func) { - root_model.sort (sort_func); - } - - public uint n_root_items () { - return root_model.get_n_items (); - } - - public delegate bool ListIteratorCallback (TreeListItem item); - public static bool ITERATE_CONTINUE = true; - public static bool ITERATE_STOP = false; - public void iterate_children (TreeListItem? start, ListIteratorCallback cb) { - ListModel model; - if (start == null) { - model = root_model; - } else { - model = start.child_model; - } - - TreeListItem? item = null; - uint pos = 0; - do { - item = (TreeListItem?) (model.get_object (pos++)); - } while (item != null && cb (item)); - } - } diff --git a/src/Widgets/TreeList/TreeListItem.vala b/src/Widgets/TreeList/TreeListItem.vala deleted file mode 100644 index 296ba47bc5..0000000000 --- a/src/Widgets/TreeList/TreeListItem.vala +++ /dev/null @@ -1,118 +0,0 @@ -/* - * SPDX-License-Identifier: GPL-3.0-or-later - * SPDX-FileCopyrightText: 20126 elementary, Inc. - */ - -// Subclass to provide additional functions required for project list, symbol list etc -public class Code.TreeListItem : Object { - public string text { get; set; default = ""; } //This can include markup - public string sub_text { get; set; default = ""; } //This can include markup? - public string tooltip { get; set; default = ""; } - public Icon? icon { get; set; default = null;} - public Icon? secondary_icon { get; set; default = null;} - public string secondary_icon_tooltip { get; set; default = ""; } - public string badge = ""; // Use label styled with Granite.STYLE_CLASS_BADGE? - - public ListStore? child_model { get; set; default = null; } - public TreeListItem? parent { get; set; default = null; } - public bool is_expandable { get; construct; } - public bool is_expanded { get; set; } // gets bound to the ListItem (temporarily) - public Binding expanded_binding { get; set; } // Need to save bind so we can unbind - public bool is_activatable { get; set; default = true; } - public bool is_selectable { get; set; default = true; } - public bool is_editable { get; set; default = false; } - public bool is_dummy { get; construct; } - - public signal void child_added (TreeListItem item); // To emulate source list - - public TreeListItem () { - Object ( - is_dummy: false, - is_expandable: false - ); - } - - public TreeListItem.dummy () { - Object ( - is_dummy: true, - is_expandable: false - ); - } - - public TreeListItem.expandable () { - Object ( - is_dummy: false, - is_expandable: true - ); - } - - public ListModel create_child_model () { - child_model = new ListStore (typeof (TreeListItem)); - return child_model; - } - - public virtual void add_child (TreeListItem child) { - if (child_model == null) { - create_child_model (); - } - - child_model.insert_sorted (child, (a, b) => { - return strcmp (((TreeListItem) a).text, ((TreeListItem) b).text); - }); - - child_added (child); - } - - public virtual void remove_child (TreeListItem child) requires (child_model != null) { - uint pos; - if (child_model.find (child, out pos)) { - child_model.remove (pos); - } - } - - public void remove_all_children () { - if (child_model != null) { - child_model.remove_all (); - } - } - - public bool has_no_children () requires (child_model != null) { - return child_model.n_items == 0; - } - - public TreeListItem? get_nth_child (uint pos) { - return (TreeListItem?) child_model.get_object (pos); - } - - /** - * Collapses the item and/or its children. - * - * @param inclusive Whether to also collapse this item (true), or only its children (false). - * @param recursive Whether to recursively collapse all the children (true), or only - * immediate children (false). - */ - public void collapse_all ( - bool inclusive, - bool recursive, - uint level = 0 - ) requires (child_model != null){ - TreeListItem? child = null; - uint pos = 0; - do { - child = (TreeListItem?) (child_model.get_object (pos++)); - if (child.is_expandable) { - if (recursive) { - child.collapse_all (true, true, level++); - } else { - child.is_expanded = false; - } - } - } while (child != null); - - if (level == 0 && inclusive) { - is_expanded = false; - } else { - level--; - } - } -} diff --git a/src/codecore.deps b/src/codecore.deps index 97238c26a2..1d34725101 100644 --- a/src/codecore.deps +++ b/src/codecore.deps @@ -3,7 +3,7 @@ glib-2.0 gobject-2.0 gio-2.0 libadwaita-1 -granite-7 +granite-9 gee-0.8 libpeas-2 gtksourceview-5 diff --git a/src/meson.build b/src/meson.build index 66d547b2ee..72def3541c 100644 --- a/src/meson.build +++ b/src/meson.build @@ -31,10 +31,14 @@ code_files = files( 'Dialogs/BranchActions/BranchNameRow.vala', 'FolderManager/File.vala', 'FolderManager/FileItem.vala', - 'FolderManager/FileView.vala', + # 'FolderManager/FileView.vala', 'FolderManager/FolderItem.vala', + 'FolderManager/FolderInterface.vala', 'FolderManager/Item.vala', + 'FolderManager/FolderManagerItemInterface.vala', 'FolderManager/ProjectFolderItem.vala', + 'FolderManager/FolderTree.vala', + 'FolderManager/ProjectList.vala', 'Services/CommentToggler.vala', 'Services/Document.vala', 'Services/DocumentManager.vala', @@ -45,8 +49,8 @@ code_files = files( 'Services/PluginManager.vala', 'Services/RestoreOverride.vala', 'Services/Settings.vala', - 'Widgets/TreeList/TreeList.vala', - 'Widgets/TreeList/TreeListItem.vala', + # 'Widgets/TreeList/TreeList.vala', + # 'Widgets/TreeList/TreeListItem.vala', 'Widgets/ChooseProjectButton.vala', 'Widgets/DocumentView.vala', 'Widgets/FormatBar.vala', @@ -57,9 +61,9 @@ code_files = files( 'Widgets/PaneSwitcher.vala', 'Widgets/PopoverMenuItem.vala', 'Widgets/SearchBar.vala', - 'Widgets/SourceList/CellRendererBadge.vala', - 'Widgets/SourceList/CellRendererExpander.vala', - 'Widgets/SourceList/SourceList.vala', + # 'Widgets/SourceList/CellRendererBadge.vala', + # 'Widgets/SourceList/CellRendererExpander.vala', + # 'Widgets/SourceList/SourceList.vala', 'Widgets/SourceView.vala', 'Widgets/Terminal.vala', 'Widgets/WelcomeView.vala',