Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
7233b7c
Implement create new from template
jeremypw Jun 29, 2025
364de28
Merge branch 'master' into jeremypw/new-file-from-template
jeremypw Jul 6, 2025
abef963
Allow configuration of template directory in Preferences
jeremypw Jul 6, 2025
c91d1c3
Use validated configured template path, fallback to Environment
jeremypw Jul 6, 2025
54b5e53
Address timing issues
jeremypw Jul 7, 2025
afe3c96
Continue to fix timing issues
jeremypw Jul 7, 2025
85d4936
Revert "Allow configuration of template directory in Preferences"
jeremypw Jul 7, 2025
df3a746
Merge branch 'master' into jeremypw/new-file-from-template
jeremypw Jul 8, 2025
24b149e
Revert "Use validated configured template path, fallback to Environment"
jeremypw Jul 7, 2025
b018df5
Merge branch 'master' into jeremypw/new-file-from-template
jeremypw Jul 14, 2025
d135a98
Merge branch 'master' into jeremypw/new-file-from-template
jeremypw Jul 18, 2025
2b1dbdf
Merge branch 'master' into jeremypw/new-file-from-template
jeremypw Aug 9, 2025
13c5cb4
Merge branch 'master' into jeremypw/new-file-from-template
jeremypw Aug 18, 2025
e42a3e2
Merge branch 'master' into jeremypw/new-file-from-template
jeremypw Oct 18, 2025
77b6c68
Merge branch 'master' into jeremypw/new-file-from-template
jeremypw Nov 5, 2025
217278d
Merge branch 'master' into jeremypw/new-file-from-template
jeremypw Jan 25, 2026
f38170f
Merge branch 'master' into jeremypw/new-file-from-template
jeremypw Feb 16, 2026
cccdc2d
Merge branch 'master' into jeremypw/new-file-from-template
zeebok Feb 18, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 24 additions & 1 deletion src/FolderManager/FileView.vala
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ public class Scratch.FolderManager.FileView : Code.Widgets.SourceList, Code.Pane
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_CHECKOUT_LOCAL_BRANCH = "checkout-local-branch";
public const string ACTION_CHECKOUT_REMOTE_BRANCH = "checkout-remote-branch";
Expand All @@ -45,6 +46,7 @@ public class Scratch.FolderManager.FileView : Code.Widgets.SourceList, Code.Pane
{ 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"}
Expand Down Expand Up @@ -317,6 +319,7 @@ public class Scratch.FolderManager.FileView : Code.Widgets.SourceList, Code.Pane
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)) {
Expand Down Expand Up @@ -412,9 +415,10 @@ public class Scratch.FolderManager.FileView : Code.Widgets.SourceList, Code.Pane

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.get_string ();
var path = param != null ? param.get_string () : null;

if (path == null || path == "") {
critical ("No path");
return;
}

Expand All @@ -426,6 +430,25 @@ public class Scratch.FolderManager.FileView : Code.Widgets.SourceList, Code.Pane
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 (root, 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]);
Expand Down
212 changes: 172 additions & 40 deletions src/FolderManager/FolderItem.vala
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ namespace Scratch.FolderManager {
* 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;
Expand All @@ -35,6 +36,8 @@ namespace Scratch.FolderManager {
}
}

public signal void children_finished_loading ();

public FolderItem (File file, FileView view) {
Object (file: file, view: view);
}
Expand Down Expand Up @@ -107,6 +110,8 @@ namespace Scratch.FolderManager {
if (root != null) {
root.child_folder_loaded (this); //Updates child status emblens
}

children_finished_loading ();
}

private void on_toggled () {
Expand Down Expand Up @@ -237,12 +242,84 @@ namespace Scratch.FolderManager {
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) {
load_templates_from_folder (GLib.File.new_for_path (template_path), new_menu);
}

var new_item = new GLib.MenuItem.submenu (_("New"), new_menu);
new_item.set_submenu (new_menu);

return new_item;
}

//Adapted from Files app code
const int MAX_TEMPLATES = 2048;
private void load_templates_from_folder (GLib.File template_folder, Menu new_submenu, uint count = 0) {
GLib.List<GLib.File> template_list = null;
GLib.List<GLib.File> folder_list = null;

GLib.FileEnumerator enumerator;
var flags = GLib.FileQueryInfoFlags.NOFOLLOW_SYMLINKS;
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;
}

if (folder_list.length () > 0) {
folder_list.sort ((a, b) => {
return strcmp (a.get_basename ().down (), b.get_basename ().down ());
});

folder_list.@foreach ((folder) => {
var folder_submenu = new Menu ();
var folder_submenuitem = new MenuItem.submenu (
folder.get_basename (),
folder_submenu
);

new_submenu.append_item (folder_submenuitem);
load_templates_from_folder (folder, folder_submenu, count);
});
}

if (template_list.length () > 0) {
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 (
FileView.ACTION_PREFIX + FileView.ACTION_NEW_FROM_TEMPLATE,
new Variant ("(ss)", this.path, template.get_path ())
)
);

new_submenu.append_item (template_menuitem);
});
}
}

public void remove_all_badges () {
foreach (var child in children) {
remove_badge (child);
Expand All @@ -263,7 +340,7 @@ namespace Scratch.FolderManager {
has_dummy = false;
}

((Code.Widgets.SourceList.ExpandableItem)this).add (item);
base.add (item);
}

public new void remove (Code.Widgets.SourceList.Item item) {
Expand Down Expand Up @@ -337,7 +414,6 @@ namespace Scratch.FolderManager {
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 ());
Expand Down Expand Up @@ -386,26 +462,111 @@ namespace Scratch.FolderManager {
return null;
}

public void on_add_new (bool is_folder) {
public void on_add_template (string template_path) {
// Expand folder before trying to copy template so that child appears for renaming
if (!expanded) {
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;
}

unowned string name = is_folder ? _("untitled folder") : _("new file");
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++;
}

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;
}

// 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 = 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;
});
});
}

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++;
}
expanded = true;
var rename_item = new RenameItem (new_file.get_basename (), is_folder);

name = new_file.get_basename ();

// Expand folder before trying to rename
if (!expanded) {
ulong once = 0;
once = children_finished_loading.connect (() => {
this.disconnect (once);
rename_new (name, is_folder);
});

expanded = true; // causes async loading of children
} else {
rename_new (name, is_folder);
}
}

private void rename_new (string name, bool is_folder) requires (!view.editing) {
var rename_item = new RenameItem (name, is_folder);
add (rename_item);
/* Start editing after finishing signal handler */
GLib.Idle.add (() => {

// 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 (() => {
Expand All @@ -414,7 +575,7 @@ namespace Scratch.FolderManager {
var new_name = rename_item.name;
try {
var gfile = file.file.get_child_for_display_name (new_name);
if (is_folder) {
if (rename_item.is_folder) {
gfile.make_directory ();
} else {
gfile.create (FileCreateFlags.NONE);
Expand All @@ -431,45 +592,16 @@ namespace Scratch.FolderManager {
return Source.CONTINUE;
} else {
remove (rename_item);
return Source.REMOVE;
}

return Source.REMOVE;
});
} else {
critical ("Failed to rename new item");
remove (rename_item);
}


return Source.REMOVE;
});
}
}

internal class RenameItem : Code.Widgets.SourceList.Item {
public bool is_folder { get; construct; }

public RenameItem (string name, bool is_folder) {
Object (
name: name,
is_folder: is_folder
);
}

construct {
editable = 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;
}
}
}
}
29 changes: 29 additions & 0 deletions src/FolderManager/Item.vala
Original file line number Diff line number Diff line change
Expand Up @@ -96,5 +96,34 @@ namespace Scratch.FolderManager {
return null;
}
}

protected class RenameItem : Code.Widgets.SourceList.Item {
public bool is_folder { get; construct; }

public RenameItem (string name, bool is_folder) {
Object (
name: name,
is_folder: is_folder
);
}

construct {
editable = true;
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;
}
}
}
}
}
Loading