diff --git a/data/io.elementary.code.appdata.xml.in b/data/io.elementary.code.appdata.xml.in index b40658b911..e112eaae29 100644 --- a/data/io.elementary.code.appdata.xml.in +++ b/data/io.elementary.code.appdata.xml.in @@ -26,6 +26,18 @@ + + +

Improvements:

+
    +
  • Use the OpenURI portal for launching external applications
  • +
+

Minor updates:

+
    +
  • Updated translations
  • +
+
+

Improvements:

diff --git a/meson.build b/meson.build index 763dff5629..c9301a7c98 100644 --- a/meson.build +++ b/meson.build @@ -29,6 +29,7 @@ glib_dep = dependency('glib-2.0', version: '>=2.30.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('gtk+-3.0', version: '>=3.6.0') +gdk_dep = [ dependency('gdk-x11-3.0'), dependency('gdk-wayland-3.0') ] granite_dep = dependency('granite', version: '>=6.0.0') handy_dep = dependency('libhandy-1', version: '>=0.90.0') gtksourceview_dep = dependency('gtksourceview-4') @@ -49,6 +50,7 @@ dependencies = [ gio_unix_dep, gee_dep, gtk_dep, + gdk_dep, granite_dep, handy_dep, gtksourceview_dep, diff --git a/src/FolderManager/File.vala b/src/FolderManager/File.vala index e916b00bde..1132527cee 100644 --- a/src/FolderManager/File.vala +++ b/src/FolderManager/File.vala @@ -126,6 +126,16 @@ namespace Scratch.FolderManager { } } + public bool is_writable { + get { + try { + return get_boolean_file_attribute (GLib.FileAttribute.ACCESS_CAN_WRITE); + } catch (GLib.Error e) { + return false; + } + } + } + // returns a list of all children of a directory private bool children_valid = false; private Gee.ArrayList _children = new Gee.ArrayList (); diff --git a/src/FolderManager/FileItem.vala b/src/FolderManager/FileItem.vala index 44f862e2cb..9578096ae6 100644 --- a/src/FolderManager/FileItem.vala +++ b/src/FolderManager/FileItem.vala @@ -36,18 +36,8 @@ namespace Scratch.FolderManager { new_window.open_document (doc, true); }); - var files_appinfo = AppInfo.get_default_for_type ("inode/directory", true); - - var files_item_icon = new Gtk.Image.from_gicon (files_appinfo.get_icon (), Gtk.IconSize.MENU); - files_item_icon.pixel_size = 16; - - var files_item_grid = new Gtk.Grid (); - files_item_grid.add (files_item_icon); - files_item_grid.add (new Gtk.Label (files_appinfo.get_name ())); - - var files_menuitem = new Gtk.MenuItem (); - files_menuitem.add (files_item_grid); - files_menuitem.activate.connect (() => launch_app_with_file (files_appinfo, file.file)); + var files_menuitem = new Gtk.MenuItem.with_label (_("File Manager")); + files_menuitem.activate.connect (() => launch_in_file_manager (file)); var other_menuitem = new Gtk.MenuItem.with_label (_("Other Application…")); other_menuitem.activate.connect (() => show_app_chooser (file)); @@ -57,6 +47,7 @@ namespace Scratch.FolderManager { open_in_menu.add (new_window_menuitem); open_in_menu.add (new Gtk.SeparatorMenuItem ()); } + open_in_menu.add (files_menuitem); var contractor_menu = new Gtk.Menu (); @@ -72,29 +63,6 @@ namespace Scratch.FolderManager { if (info != null) { var file_type = info.get_attribute_string (GLib.FileAttribute.STANDARD_CONTENT_TYPE); - List external_apps = GLib.AppInfo.get_all_for_type (file_type); - - foreach (AppInfo app_info in external_apps) { - if (app_info.get_id () == GLib.Application.get_default ().application_id + ".desktop") { - continue; - } - - var menuitem_icon = new Gtk.Image.from_gicon (app_info.get_icon (), Gtk.IconSize.MENU); - menuitem_icon.pixel_size = 16; - - var menuitem_grid = new Gtk.Grid (); - menuitem_grid.add (menuitem_icon); - menuitem_grid.add (new Gtk.Label (app_info.get_name ())); - - var item_app = new Gtk.MenuItem (); - item_app.add (menuitem_grid); - - item_app.activate.connect (() => { - launch_app_with_file (app_info, file.file); - }); - open_in_menu.add (item_app); - } - try { var contracts = Granite.Services.ContractorProxy.get_contracts_by_mime (file_type); foreach (var contract in contracts) { diff --git a/src/FolderManager/FolderItem.vala b/src/FolderManager/FolderItem.vala index e2a4c1cbe2..3214d8ea33 100644 --- a/src/FolderManager/FolderItem.vala +++ b/src/FolderManager/FolderItem.vala @@ -66,6 +66,9 @@ namespace Scratch.FolderManager { } public override Gtk.Menu? get_context_menu () { + var open_in_item = new Gtk.MenuItem.with_label (_("Open In Another Application…")); + open_in_item.activate.connect (() => show_app_chooser (file)); + var contractor_menu = new Gtk.Menu (); GLib.FileInfo info = null; @@ -109,7 +112,7 @@ namespace Scratch.FolderManager { }; var menu = new Gtk.Menu (); - menu.append (create_submenu_for_open_in (info, file_type)); + menu.append (open_in_item); menu.append (contractor_item); menu.append (new Gtk.SeparatorMenuItem ()); menu.append (create_submenu_for_new ()); @@ -122,53 +125,6 @@ namespace Scratch.FolderManager { return menu; } - protected Gtk.MenuItem create_submenu_for_open_in (GLib.FileInfo? info, string? file_type) { - var other_menuitem = new Gtk.MenuItem.with_label (_("Other Application…")); - other_menuitem.activate.connect (() => show_app_chooser (file)); - - file_type = file_type ?? "inode/directory"; - - var open_in_menu = new Gtk.Menu (); - - if (info != null) { - List external_apps = GLib.AppInfo.get_all_for_type (file_type); - - string this_id = GLib.Application.get_default ().application_id + ".desktop"; - - foreach (AppInfo app_info in external_apps) { - if (app_info.get_id () == this_id) { - continue; - } - - var menuitem_icon = new Gtk.Image.from_gicon (app_info.get_icon (), Gtk.IconSize.MENU); - menuitem_icon.pixel_size = 16; - - var menuitem_grid = new Gtk.Grid (); - menuitem_grid.add (menuitem_icon); - menuitem_grid.add (new Gtk.Label (app_info.get_name ())); - - var item_app = new Gtk.MenuItem (); - item_app.add (menuitem_grid); - - item_app.activate.connect (() => { - launch_app_with_file (app_info, file.file); - }); - open_in_menu.add (item_app); - } - } - - if (open_in_menu.get_children ().length () > 0) { - open_in_menu.add (new Gtk.SeparatorMenuItem ()); - } - - open_in_menu.add (other_menuitem); - - var open_in_item = new Gtk.MenuItem.with_label (_("Open In")); - open_in_item.submenu = open_in_menu; - - return open_in_item; - } - protected Gtk.MenuItem create_submenu_for_new () { var new_folder_item = new Gtk.MenuItem.with_label (_("Folder")); new_folder_item.activate.connect (() => on_add_new (true)); diff --git a/src/FolderManager/Item.vala b/src/FolderManager/Item.vala index a4169b0000..cbbf13e9b9 100644 --- a/src/FolderManager/Item.vala +++ b/src/FolderManager/Item.vala @@ -76,27 +76,71 @@ namespace Scratch.FolderManager { } public void show_app_chooser (File file) { - var dialog = new Gtk.AppChooserDialog (new Gtk.Window (), Gtk.DialogFlags.MODAL, file.file); - dialog.deletable = false; - - if (dialog.run () == Gtk.ResponseType.OK) { - var app_info = dialog.get_app_info (); - if (app_info != null) { - launch_app_with_file (app_info, file.file); + var window = (MainWindow) ((Gtk.Application) GLib.Application.get_default ()).active_window; + try { + bool writable = !file.is_valid_directory () && file.is_writable; + var portal = Portal.OpenURI.get (); + + var options = new HashTable (null, null); + options["token_handler"] = Portal.generate_token (); + options["writable"] = writable; + options["ask"] = true; + + var fd = Posix.open (file.path, (writable ? Posix.O_RDWR : Posix.O_RDONLY) | Posix.O_CLOEXEC); + if (fd == -1) { + critical ("OpenURI: cannot open file descriptor for '%s'", file.path); + return; } - } - dialog.destroy (); + window.export.begin ((obj, res) => { + var handle = window.export.end (res); + if (portal.version > 2) { + try { + portal.open_file (handle, new UnixInputStream (fd, true), options); + } catch (Error e) { + warning ("error calling portal: %s", e.message); + } + } else { + warning ("OpenURI: portal version is too old"); + } + + window.unexport (); + }); + } catch (Error e) { + warning ("cannot connect to portal: %s", e.message); + } } - public void launch_app_with_file (AppInfo app_info, GLib.File file) { - var file_list = new List (); - file_list.append (file); - + public void launch_in_file_manager (File file) { + var window = (MainWindow) ((Gtk.Application) GLib.Application.get_default ()).active_window; try { - app_info.launch (file_list, null); + var portal = Portal.OpenURI.get (); + var options = new HashTable (null, null); + options["token_handler"] = Portal.generate_token (); + + var fd = Posix.open (file.path, Posix.O_RDONLY | Posix.O_CLOEXEC); + if (fd == -1) { + critical ("OpenURI: cannot open file descriptor for '%s'", file.path); + return; + } + + // the OpenDirectory method was added in version 3 of the portal + window.export.begin ((obj, res) => { + try { + var handle = window.export.end (res); + if (portal.version > 2) { + portal.open_directory (handle, new UnixInputStream (fd, true), options); + } else { + portal.open_file (handle, new UnixInputStream (fd, true), options); + } + } catch (Error e) { + warning ("error calling portal: %s", e.message); + } + + window.unexport (); + }); } catch (Error e) { - warning (e.message); + warning ("cannot connect to portal: %s", e.message); } } diff --git a/src/FolderManager/ProjectFolderItem.vala b/src/FolderManager/ProjectFolderItem.vala index e0a8266e82..2253d88254 100644 --- a/src/FolderManager/ProjectFolderItem.vala +++ b/src/FolderManager/ProjectFolderItem.vala @@ -108,6 +108,9 @@ namespace Scratch.FolderManager { } public override Gtk.Menu? get_context_menu () { + var open_in_item = new Gtk.MenuItem.with_label (_("Open In…")); + open_in_item.activate.connect (() => show_app_chooser (file)); + var close_item = new Gtk.MenuItem.with_label (_("Close Folder")); close_item.activate.connect (() => { closed (); @@ -145,7 +148,7 @@ namespace Scratch.FolderManager { } var menu = new Gtk.Menu (); - menu.append (create_submenu_for_open_in (info, file_type)); + menu.append (open_in_item); menu.append (new Gtk.SeparatorMenuItem ()); menu.append (create_submenu_for_new ()); diff --git a/src/MainWindow.vala b/src/MainWindow.vala index fef536c599..e3b2bfb3fd 100644 --- a/src/MainWindow.vala +++ b/src/MainWindow.vala @@ -38,6 +38,8 @@ namespace Scratch { // Plugins private Scratch.Services.PluginsManager plugins; + private bool exported; + // Widgets for Plugins public Gtk.Notebook bottombar; public Code.Sidebar sidebar; @@ -1030,5 +1032,34 @@ namespace Scratch { return path; } + + public async string export () { + var window = get_window (); + + if (window is Gdk.X11.Window) { + var xid = ((Gdk.X11.Window) window).get_xid (); + return "x11:%x".printf ((uint) xid); + } else if (window is Gdk.Wayland.Window) { + var handle = "wayland:"; + ((Gdk.Wayland.Window) window).export_handle ((w, h) => { + handle += h; + export.callback (); + }); + yield; + + exported = handle != "wayland:"; + return exported ? handle : ""; + } else { + warning ("Unknown windowing system, not exporting window"); + return ""; + } + } + + public void unexport () { + if (exported) { + ((Gdk.Wayland.Window) get_window ()).unexport_handle (); + exported = false; + } + } } } diff --git a/src/Services/Portal.vala b/src/Services/Portal.vala new file mode 100644 index 0000000000..77d1993735 --- /dev/null +++ b/src/Services/Portal.vala @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2021 elementary, Inc. (https://elementary.io) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License 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 warranty of + * MERCHANTABILITY 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 . + * + */ + +namespace Portal { + const string DESKTOP_BUS_NAME = "org.freedesktop.portal.Desktop"; + const string DESKTOP_BUS_PATH = "/org/freedesktop/portal/desktop"; + OpenURI? open_uri_portal = null; + + public static string generate_token () { + return "%s_%i".printf ( + GLib.Application.get_default ().application_id.replace (".", "_"), + Random.int_range (0, int32.MAX) + ); + } + + [DBus (name = "org.freedesktop.portal.OpenURI")] + interface OpenURI : Object { + [DBus (name = "version")] + public abstract uint32 version { get; } + + public static OpenURI @get () throws Error { + if (open_uri_portal == null) { + var connection = GLib.Application.get_default ().get_dbus_connection (); + open_uri_portal = connection.get_proxy_sync (DESKTOP_BUS_NAME, DESKTOP_BUS_PATH); + } + + return open_uri_portal; + } + + public abstract ObjectPath open_uri (string parent_window, string uri, HashTable options) throws DBusError, IOError; + public abstract ObjectPath open_file (string parent_window, UnixInputStream fd, HashTable options) throws DBusError, IOError; + public abstract ObjectPath open_directory (string parent_window, UnixInputStream fd, HashTable options) throws DBusError, IOError; + } +} diff --git a/src/meson.build b/src/meson.build index 8404a86ebb..aa9c07540d 100644 --- a/src/meson.build +++ b/src/meson.build @@ -33,6 +33,7 @@ code_files = files( 'Services/GitManager.vala', 'Services/MonitoredRepository.vala', 'Services/PluginManager.vala', + 'Services/Portal.vala', 'Services/Settings.vala', 'Services/TemplateManager.vala', 'Widgets/ChooseProjectButton.vala', diff --git a/vapi/gdk-wayland-3.0.vapi b/vapi/gdk-wayland-3.0.vapi new file mode 100644 index 0000000000..441eb79e03 --- /dev/null +++ b/vapi/gdk-wayland-3.0.vapi @@ -0,0 +1,14 @@ +[CCode (cheader_filename = "gdk/gdkwayland.h")] +namespace Gdk.Wayland { + [CCode (type_id = "GDK_TYPE_WAYLAND_WINDOW", type_check_function = "GDK_IS_WAYLAND_WINDOW")] + public class Window : Gdk.Window { + protected Window (); + + public bool export_handle (owned WindowExported callback); + public bool set_transient_for_exported (string parent_handle_str); + public void unexport_handle (); + } + + [CCode (instance_pos = 2.9)] + public delegate void WindowExported (Gdk.Window window, string handle); +}