From 913acebb9944f234701e6f2ee6cabb75fbf465ab Mon Sep 17 00:00:00 2001 From: Andrey Kutejko Date: Thu, 9 Jan 2025 22:21:57 +0100 Subject: [PATCH] Reimplement plugin loading and plugin management window --- pixmaps/gnome-commander-settings-symbolic.svg | 4 + pixmaps/meson.build | 1 + src/config.rs.in | 1 + src/file_list_popup.rs | 4 +- src/gmodule.rs | 125 ++++ src/gnome-cmd-about-plugin.h | 2 +- src/gnome-cmd-application.cc | 3 - src/gnome-cmd-data.cc | 42 -- src/gnome-cmd-data.h | 3 - src/gnome-cmd-main-win.cc | 20 +- src/gnome-cmd-main-win.h | 6 +- src/gnome-cmd-user-actions.cc | 11 - src/libgcmd/configurable.rs | 60 ++ src/libgcmd/mod.rs | 18 + src/main.rs | 2 + src/main_win.rs | 27 +- src/meson.build | 2 - src/plugin_manager.cc | 457 --------------- src/plugin_manager.h | 45 -- src/plugin_manager.rs | 538 ++++++++++++++++-- src/user_actions.rs | 18 +- 21 files changed, 750 insertions(+), 639 deletions(-) create mode 100644 pixmaps/gnome-commander-settings-symbolic.svg create mode 100644 src/gmodule.rs create mode 100644 src/libgcmd/configurable.rs delete mode 100644 src/plugin_manager.cc delete mode 100644 src/plugin_manager.h diff --git a/pixmaps/gnome-commander-settings-symbolic.svg b/pixmaps/gnome-commander-settings-symbolic.svg new file mode 100644 index 000000000..f9c124a6c --- /dev/null +++ b/pixmaps/gnome-commander-settings-symbolic.svg @@ -0,0 +1,4 @@ + + + + diff --git a/pixmaps/meson.build b/pixmaps/meson.build index 62d40dee4..750f0db6f 100644 --- a/pixmaps/meson.build +++ b/pixmaps/meson.build @@ -41,6 +41,7 @@ icons_scalable = [ 'gnome-commander-down-symbolic.svg', 'gnome-commander-bookmark-outline-symbolic.svg', 'gnome-commander-info-outline-symbolic.svg', + 'gnome-commander-settings-symbolic.svg', ] install_data(icons_scalable, diff --git a/src/config.rs.in b/src/config.rs.in index bbe5e3466..db4c594d7 100644 --- a/src/config.rs.in +++ b/src/config.rs.in @@ -21,6 +21,7 @@ pub const PACKAGE: &str = @PACKAGE@; pub const PREFIX: &str = @PREFIX@; pub const DATADIR: &str = @DATADIR@; pub const PIXMAPS_DIR: &str = @PIXMAPS_DIR@; +pub const PLUGIN_DIR: &str = @PLUGIN_DIR@; pub const PACKAGE_NAME: &str = @PACKAGE_NAME@; pub const PACKAGE_VERSION: &str = @PACKAGE_VERSION@; diff --git a/src/file_list_popup.rs b/src/file_list_popup.rs index bf83bb446..442ddb133 100644 --- a/src/file_list_popup.rs +++ b/src/file_list_popup.rs @@ -28,7 +28,7 @@ use crate::{ file_descriptor::FileDescriptorExt, }, main_win::MainWindow, - plugin_manager::{plugin_manager_get_active_plugins, wrap_plugin_menu}, + plugin_manager::wrap_plugin_menu, utils::MenuBuilderExt, }; use gettextrs::gettext; @@ -234,7 +234,7 @@ pub fn file_popup_menu(main_win: &MainWindow, file_list: &FileList) -> Option() { if let Some(menu) = file_actions.create_popup_menu_items(&main_win.state()) { let menu = wrap_plugin_menu(&action_group_name, &menu); diff --git a/src/gmodule.rs b/src/gmodule.rs new file mode 100644 index 000000000..d13a69590 --- /dev/null +++ b/src/gmodule.rs @@ -0,0 +1,125 @@ +/* + * Copyright 2025 Andrey Kutejko + * + * 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 . + * + * For more details see the file COPYING. + */ + +use gtk::glib::{ + self, + ffi::gpointer, + translate::{from_glib_full, ToGlibPtr}, +}; +use std::path::Path; + +pub mod ffi { + use glib::ffi::{gboolean, gpointer, GError, GQuark}; + use std::ffi::{c_char, c_uint}; + + pub type GModuleFlags = c_uint; + pub const G_MODULE_BIND_LAZY: GModuleFlags = 1; + pub const G_MODULE_BIND_LOCAL: GModuleFlags = 2; + pub const G_MODULE_BIND_MASK: GModuleFlags = 3; + + #[repr(C)] + #[derive(Debug, Copy, Clone)] + pub struct GModule { + _unused: [u8; 0], + } + + pub type GModuleError = c_uint; + pub const G_MODULE_ERROR_FAILED: GModuleError = 0; + pub const G_MODULE_ERROR_CHECK_FAILED: GModuleError = 1; + + extern "C" { + pub fn g_module_supported() -> gboolean; + + pub fn g_module_open_full( + file_name: *const c_char, + flags: GModuleFlags, + error: *mut *mut GError, + ) -> *mut GModule; + + pub fn g_module_close(module: *mut GModule) -> gboolean; + + pub fn g_module_make_resident(module: *mut GModule); + + pub fn g_module_error_quark() -> GQuark; + + pub fn g_module_symbol( + module: *mut GModule, + symbol_name: *const c_char, + symbol: *mut gpointer, + ) -> gboolean; + + pub fn g_module_name(module: *mut GModule) -> *const c_char; + } +} + +#[repr(C)] +pub struct GModule(*mut ffi::GModule); + +impl GModule { + pub fn open(file_name: &Path, flags: GModuleFlags) -> Result { + let mut f: ffi::GModuleFlags = 0; + if flags.lazy { + f |= ffi::G_MODULE_BIND_LAZY; + } + if flags.local { + f |= ffi::G_MODULE_BIND_LOCAL; + } + + let mut error = std::ptr::null_mut(); + let module = unsafe { ffi::g_module_open_full(file_name.to_glib_none().0, f, &mut error) }; + + if module.is_null() { + return Err(unsafe { from_glib_full(error) }); + } + + Ok(Self(module)) + } + + pub fn symbol(&self, symbol_name: &str) -> Option { + let mut symbol = std::ptr::null_mut(); + let result = + unsafe { ffi::g_module_symbol(self.0, symbol_name.to_glib_none().0, &mut symbol) != 0 }; + if result { + Some(symbol) + } else { + None + } + } + + pub fn leak(mut self) -> *mut ffi::GModule { + std::mem::replace(&mut self.0, std::ptr::null_mut()) + } +} + +impl Drop for GModule { + fn drop(&mut self) { + if !self.0.is_null() { + let status = unsafe { ffi::g_module_close(self.0) }; + if status == 0 { + eprintln!("Failed to close a GModule."); + } + } + } +} + +#[derive(Clone, Copy)] +pub struct GModuleFlags { + pub lazy: bool, + pub local: bool, +} diff --git a/src/gnome-cmd-about-plugin.h b/src/gnome-cmd-about-plugin.h index f42e8de8c..eb264597e 100644 --- a/src/gnome-cmd-about-plugin.h +++ b/src/gnome-cmd-about-plugin.h @@ -67,6 +67,6 @@ struct GnomeCmdAboutPluginClass GtkDialogClass parent_class; }; -GType gnome_cmd_about_plugin_get_type (void) G_GNUC_CONST; +extern "C" GType gnome_cmd_about_plugin_get_type (void) G_GNUC_CONST; GtkWidget *gnome_cmd_about_plugin_new (PluginInfo *info); diff --git a/src/gnome-cmd-application.cc b/src/gnome-cmd-application.cc index ef70ef001..f74f811db 100644 --- a/src/gnome-cmd-application.cc +++ b/src/gnome-cmd-application.cc @@ -31,7 +31,6 @@ #include "gnome-cmd-con.h" #include "utils.h" #include "imageloader.h" -#include "plugin_manager.h" #include "tags/gnome-cmd-tags.h" @@ -132,13 +131,11 @@ static void gnome_cmd_application_activate(GApplication *application) gcmd_owner.load_async(); gcmd_tags_init(); - plugin_manager_init (); } static void gnome_cmd_application_shutdown(GApplication *application) { - plugin_manager_shutdown (); gcmd_tags_shutdown (); IMAGE_free (); diff --git a/src/gnome-cmd-data.cc b/src/gnome-cmd-data.cc index 597ff49c1..43b0ab92f 100644 --- a/src/gnome-cmd-data.cc +++ b/src/gnome-cmd-data.cc @@ -47,7 +47,6 @@ GnomeCmdData gnome_cmd_data; struct GnomeCmdData::Private { GnomeCmdConList *con_list; - GList *auto_load_plugins; }; GSettingsSchemaSource* GnomeCmdData::GetGlobalSchemaSource() @@ -89,7 +88,6 @@ struct _GcmdSettings GSettings *programs; GSettings *network; GSettings *internalviewer; - GSettings *plugins; }; G_DEFINE_TYPE (GcmdSettings, gcmd_settings, G_TYPE_OBJECT) @@ -114,7 +112,6 @@ static void gcmd_settings_dispose (GObject *object) g_clear_object (&gs->programs); g_clear_object (&gs->network); g_clear_object (&gs->internalviewer); - g_clear_object (&gs->plugins); G_OBJECT_CLASS (gcmd_settings_parent_class)->dispose (object); } @@ -982,9 +979,6 @@ static void gcmd_settings_init (GcmdSettings *gs) global_schema = g_settings_schema_source_lookup (global_schema_source, GCMD_PREF_INTERNAL_VIEWER, FALSE); gs->internalviewer = g_settings_new_full (global_schema, nullptr, nullptr); - - global_schema = g_settings_schema_source_lookup (global_schema_source, GCMD_PREF_PLUGINS, FALSE); - gs->plugins = g_settings_new_full (global_schema, nullptr, nullptr); } @@ -2160,16 +2154,6 @@ inline void GnomeCmdData::save_intviewer_defaults() set_gsettings_enum_when_changed (options.gcmd_settings->internalviewer, GCMD_SETTINGS_IV_SEARCH_MODE, intviewer_defaults.search_mode); } -/** - * This function saves all entries of the auto load plugin list in the associated GSettings string array. - * @returns the return value of set_gsettings_string_array_from_glist(). - */ -inline gboolean GnomeCmdData::save_auto_load_plugins() -{ - return set_gsettings_string_array_from_glist(options.gcmd_settings->plugins, GCMD_SETTINGS_PLUGINS_AUTOLOAD, priv->auto_load_plugins); -} - - /** * Returns a GList with newly allocated char strings */ @@ -2220,18 +2204,6 @@ inline void GnomeCmdData::load_intviewer_defaults() } -/** - * This function pushes the list of plugins to be automatically loaded into the - * associated Glist. - */ -inline void GnomeCmdData::load_auto_load_plugins() -{ - g_list_free(priv->auto_load_plugins); - - priv->auto_load_plugins = get_list_from_gsettings_string_array (options.gcmd_settings->plugins, GCMD_SETTINGS_PLUGINS_AUTOLOAD); -} - - GnomeCmdData::GnomeCmdData(): search_defaults(profiles) { quick_connect = nullptr; @@ -2581,7 +2553,6 @@ void GnomeCmdData::load() g_free (quick_connect_uri); load_intviewer_defaults(); - load_auto_load_plugins(); set_g_volume_monitor (); } @@ -2715,7 +2686,6 @@ void GnomeCmdData::save(GnomeCmdMainWin *main_win) save_bookmarks (); save_advrename_profiles (); save_intviewer_defaults (); - save_auto_load_plugins (); g_settings_sync (); } @@ -2914,18 +2884,6 @@ gpointer gnome_cmd_data_get_con_list () } -GList *gnome_cmd_data_get_auto_load_plugins () -{ - return gnome_cmd_data.priv->auto_load_plugins; -} - - -void gnome_cmd_data_set_auto_load_plugins (GList *plugins) -{ - gnome_cmd_data.priv->auto_load_plugins = plugins; -} - - const gchar *gnome_cmd_data_get_symlink_prefix () { char *symlink_prefix; diff --git a/src/gnome-cmd-data.h b/src/gnome-cmd-data.h index bca868394..7a6fddebd 100644 --- a/src/gnome-cmd-data.h +++ b/src/gnome-cmd-data.h @@ -206,9 +206,6 @@ GcmdSettings *gcmd_settings_new (); #define GCMD_SETTINGS_IV_SEARCH_PATTERN_TEXT "search-pattern-text" #define GCMD_SETTINGS_IV_SEARCH_PATTERN_HEX "search-pattern-hex" -#define GCMD_PREF_PLUGINS "org.gnome.gnome-commander.plugins.general" -#define GCMD_SETTINGS_PLUGINS_AUTOLOAD "autoload" - //gKeyFile constants #define DEVICES_FILENAME "devices" #define DEVICES_DEVICE "device" diff --git a/src/gnome-cmd-main-win.cc b/src/gnome-cmd-main-win.cc index 97750c37b..08352c0f6 100644 --- a/src/gnome-cmd-main-win.cc +++ b/src/gnome-cmd-main-win.cc @@ -105,6 +105,8 @@ struct GnomeCmdMainWin::Private bool state_saved; GnomeCmdShortcuts *gcmd_shortcuts; GObject *color_themes; + + GObject *plugin_manager; }; @@ -112,6 +114,7 @@ G_DEFINE_TYPE (GnomeCmdMainWin, gnome_cmd_main_win, GTK_TYPE_APPLICATION_WINDOW) extern "C" GType gnome_cmd_color_themes_get_type(); +extern "C" GType plugin_manager_get_type (); static gboolean set_equal_panes_idle (gpointer *user_data); @@ -494,6 +497,8 @@ static void dispose (GObject *object) g_clear_object (&main_win->priv->color_themes); + g_clear_object (&main_win->priv->plugin_manager); + G_OBJECT_CLASS (gnome_cmd_main_win_parent_class)->dispose (object); } @@ -553,6 +558,8 @@ static void gnome_cmd_main_win_init (GnomeCmdMainWin *mw) "display", gdk_display_get_default(), nullptr)); + mw->priv->plugin_manager = G_OBJECT (g_object_new (plugin_manager_get_type (), nullptr)); + gtk_window_set_title (GTK_WINDOW (mw), gcmd_owner.is_root() ? _("GNOME Commander — ROOT PRIVILEGES") @@ -619,6 +626,7 @@ static void gnome_cmd_main_win_init (GnomeCmdMainWin *mw) g_signal_connect (gnome_cmd_con_list_get (), "list-changed", G_CALLBACK (on_con_list_list_changed), mw); g_signal_connect (mw->priv->color_themes, "theme-changed", G_CALLBACK (on_update_view), mw); + g_signal_connect_swapped (mw->priv->plugin_manager, "plugins-changed", G_CALLBACK (gnome_cmd_main_win_plugins_updated), mw); mw->focus_file_lists(); } @@ -817,7 +825,7 @@ gboolean GnomeCmdMainWin::key_pressed(GnomeCmdKeyPress *event) { case GDK_KEY_P: case GDK_KEY_p: - plugin_manager_show (*this); + g_action_group_activate_action (*this, "plugins-configure", nullptr); break; case GDK_KEY_f: @@ -1262,7 +1270,17 @@ void gnome_cmd_main_win_update_bookmarks(GnomeCmdMainWin *main_win) main_win->update_bookmarks(); } +void gnome_cmd_main_win_plugins_updated (GnomeCmdMainWin *main_win) +{ + main_win->plugins_updated(); +} + GnomeCmdShortcuts *gnome_cmd_main_win_shortcuts(GnomeCmdMainWin *main_win) { return main_win->priv->gcmd_shortcuts; } + +GObject *gnome_cmd_main_win_get_plugin_manager (GnomeCmdMainWin *main_win) +{ + return main_win->priv->plugin_manager; +} diff --git a/src/gnome-cmd-main-win.h b/src/gnome-cmd-main-win.h index f8effd111..55f61ebab 100644 --- a/src/gnome-cmd-main-win.h +++ b/src/gnome-cmd-main-win.h @@ -23,7 +23,6 @@ #include "gnome-cmd-file-selector.h" #include "gnome-cmd-cmdline.h" -#include "plugin_manager.h" #include "dialogs/gnome-cmd-advrename-dialog.h" #include "dialogs/gnome-cmd-search-dialog.h" @@ -114,5 +113,10 @@ extern "C" void gnome_cmd_main_win_focus_file_lists(GnomeCmdMainWin *main_win); extern "C" void gnome_cmd_main_win_update_bookmarks(GnomeCmdMainWin *main_win); +extern "C" void gnome_cmd_main_win_plugins_updated (GnomeCmdMainWin *main_win); + struct GnomeCmdShortcuts; extern "C" GnomeCmdShortcuts *gnome_cmd_main_win_shortcuts(GnomeCmdMainWin *main_win); + +extern "C" GObject *gnome_cmd_main_win_get_plugin_manager (GnomeCmdMainWin *main_win); + diff --git a/src/gnome-cmd-user-actions.cc b/src/gnome-cmd-user-actions.cc index 4e2598c07..524d5513a 100644 --- a/src/gnome-cmd-user-actions.cc +++ b/src/gnome-cmd-user-actions.cc @@ -33,7 +33,6 @@ #include "gnome-cmd-main-win.h" #include "gnome-cmd-user-actions.h" #include "gnome-cmd-dir-indicator.h" -#include "plugin_manager.h" #include "cap.h" #include "utils.h" #include "dialogs/gnome-cmd-advrename-dialog.h" @@ -1216,13 +1215,3 @@ void bookmarks_view (GSimpleAction *action, GVariant *parameter, gpointer user_d gnome_cmd_dir_indicator_show_bookmarks (GNOME_CMD_DIR_INDICATOR (main_win->fs (ACTIVE)->dir_indicator)); } - - -/************** Plugins Menu **************/ - -void plugins_configure (GSimpleAction *action, GVariant *parameter, gpointer user_data) -{ - auto main_win = static_cast(user_data); - - plugin_manager_show (GTK_WINDOW (main_win)); -} diff --git a/src/libgcmd/configurable.rs b/src/libgcmd/configurable.rs new file mode 100644 index 000000000..508ef4c20 --- /dev/null +++ b/src/libgcmd/configurable.rs @@ -0,0 +1,60 @@ +/* + * Copyright 2025 Andrey Kutejko + * + * 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 . + * + * For more details see the file COPYING. + */ + +use gtk::glib::{self, prelude::*, translate::*}; + +pub mod ffi { + use gtk::{ffi::GtkWindow, glib::ffi::GType}; + + #[repr(C)] + pub struct GnomeCmdConfigurable { + _data: [u8; 0], + _marker: std::marker::PhantomData<(*mut u8, std::marker::PhantomPinned)>, + } + + extern "C" { + pub fn gnome_cmd_configurable_get_type() -> GType; + + pub fn gnome_cmd_configurable_configure( + cfgbl: *mut GnomeCmdConfigurable, + parent_window: *const GtkWindow, + ); + } +} + +glib::wrapper! { + pub struct Configurable(Interface); + + match fn { + type_ => || ffi::gnome_cmd_configurable_get_type(), + } +} + +pub trait ConfigurableExt: IsA + 'static { + fn configure(&self, parent_window: >k::Window) { + unsafe { + ffi::gnome_cmd_configurable_configure( + self.as_ref().to_glib_none().0, + parent_window.to_glib_none().0, + ) + } + } +} + +impl> ConfigurableExt for O {} diff --git a/src/libgcmd/mod.rs b/src/libgcmd/mod.rs index d870f880f..90ca16fc3 100644 --- a/src/libgcmd/mod.rs +++ b/src/libgcmd/mod.rs @@ -17,6 +17,24 @@ * For more details see the file COPYING. */ +pub mod configurable; pub mod file_actions; pub mod file_descriptor; pub mod state; + +use std::ffi::c_char; + +pub const GNOME_CMD_PLUGIN_SYSTEM_CURRENT_VERSION: u32 = 1; + +#[repr(C)] +pub struct PluginInfo { + pub plugin_system_version: u32, + pub name: *const c_char, + pub version: *const c_char, + pub copyright: *const c_char, + pub comments: *const c_char, + pub authors: *const *const c_char, + pub documenters: *const *const c_char, + pub translator: *const c_char, + pub webpage: *const c_char, +} diff --git a/src/main.rs b/src/main.rs index 8989adb63..44c54253a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -60,6 +60,8 @@ mod types; mod user_actions; mod utils; +mod gmodule; + use application::GnomeCmdApplication; use gtk::{glib, prelude::*}; use std::error::Error; diff --git a/src/main_win.rs b/src/main_win.rs index 15417a047..a693ec041 100644 --- a/src/main_win.rs +++ b/src/main_win.rs @@ -32,7 +32,7 @@ use crate::{ file_actions::{FileActions, FileActionsExt}, state::{State, StateExt}, }, - plugin_manager::{plugin_manager_get_active_plugins, wrap_plugin_menu}, + plugin_manager::{wrap_plugin_menu, PluginManager}, shortcuts::Shortcuts, types::FileSelectorID, user_actions, @@ -50,9 +50,8 @@ use gtk::{ }; pub mod ffi { + use super::*; use crate::file_selector::ffi::GnomeCmdFileSelector; - use crate::shortcuts::Shortcuts; - use crate::types::FileSelectorID; use gtk::glib::ffi::GType; #[repr(C)] @@ -79,6 +78,10 @@ pub mod ffi { pub fn gnome_cmd_main_win_update_bookmarks(main_win: *mut GnomeCmdMainWin); pub fn gnome_cmd_main_win_shortcuts(main_win: *mut GnomeCmdMainWin) -> *mut Shortcuts; + + pub fn gnome_cmd_main_win_get_plugin_manager( + main_win: *mut GnomeCmdMainWin, + ) -> *mut ::GlibType; } #[derive(Copy, Clone)] @@ -137,6 +140,14 @@ impl MainWindow { state } + + pub fn plugin_manager(&self) -> PluginManager { + unsafe { + from_glib_none(ffi::gnome_cmd_main_win_get_plugin_manager( + self.to_glib_none().0, + )) + } + } } #[no_mangle] @@ -145,7 +156,7 @@ pub extern "C" fn gnome_cmd_main_win_install_actions(mw_ptr: *mut ffi::GnomeCmdM mw.add_action_entries(user_actions::USER_ACTIONS.iter().map(|a| a.action_entry())); } -fn main_menu() -> gio::Menu { +fn main_menu(main_win: &MainWindow) -> gio::Menu { let menu = gio::Menu::new(); menu.append_submenu(Some(&gettext("_File")), &{ @@ -356,7 +367,7 @@ fn main_menu() -> gio::Menu { .section({ gio::Menu::new().item(gettext("_Configure Plugins…"), "win.plugins-configure") }) - .section(create_plugins_menu()) + .section(create_plugins_menu(main_win)) }); menu.append_submenu(Some(&gettext("_Settings")), &{ @@ -463,9 +474,9 @@ fn create_bookmarks_menu() -> gio::Menu { menu } -fn create_plugins_menu() -> gio::Menu { +fn create_plugins_menu(main_win: &MainWindow) -> gio::Menu { let menu = gio::Menu::new(); - for (action_group_name, plugin) in plugin_manager_get_active_plugins() { + for (action_group_name, plugin) in main_win.plugin_manager().active_plugins() { if let Some(file_actions) = plugin.downcast_ref::() { if let Some(plugin_menu) = file_actions.create_main_menu() { let plugin_menu = wrap_plugin_menu(&action_group_name, &plugin_menu); @@ -482,7 +493,7 @@ pub extern "C" fn gnome_cmd_main_menu_new( initial: gboolean, ) -> *mut GMenu { let mw: MainWindow = unsafe { from_glib_none(mw_ptr) }; - let menu = main_menu(); + let menu = main_menu(&mw); if initial != 0 { let shortcuts = extract_menu_shortcuts(menu.upcast_ref()); let shortcuts_controller = gtk::ShortcutController::for_model(&shortcuts); diff --git a/src/meson.build b/src/meson.build index 11414093d..6f31d7f98 100644 --- a/src/meson.build +++ b/src/meson.build @@ -44,7 +44,6 @@ sources_headers = files( 'gnome-cmd-xfer.h', 'history.h', 'imageloader.h', - 'plugin_manager.h', 'utils.h', 'gnome-cmd-dialog.h', 'text-utils.h', @@ -91,7 +90,6 @@ sources_sources = files( 'gnome-cmd-xfer.cc', 'history.cc', 'imageloader.cc', - 'plugin_manager.cc', 'utils.cc', 'gnome-cmd-dialog.cc', 'text-utils.cc', diff --git a/src/plugin_manager.cc b/src/plugin_manager.cc deleted file mode 100644 index e529b7a54..000000000 --- a/src/plugin_manager.cc +++ /dev/null @@ -1,457 +0,0 @@ -/** - * @file plugin_manager.cc - * @copyright (C) 2001-2006 Marcus Bjurman\n - * @copyright (C) 2007-2012 Piotr Eljasiak\n - * @copyright (C) 2013-2024 Uwe Scholz\n - * - * @copyright 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 2 of the License, or - * (at your option) any later version. - * - * @copyright 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. - * - * @copyright You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -#include -#include - -#include "gnome-cmd-includes.h" -#include "gnome-cmd-data.h" -#include "plugin_manager.h" -#include "utils.h" -#include "imageloader.h" -#include "gnome-cmd-main-win.h" -#include "gnome-cmd-about-plugin.h" -#include "dirlist.h" -#include "gnome-cmd-dialog.h" -#include "widget-factory.h" - -using namespace std; - - -// The names of these functions shall never change -#define MODULE_INIT_FUNC "create_plugin" -#define MODULE_INFO_FUNC "get_plugin_info" - - -static GList *plugins = nullptr; - -gchar* get_plugin_config_location(); - -typedef GObject *(*PluginConstructorFunc)(void); - - -static void load_plugin (PluginData *data) -{ - GModule *module = g_module_open (data->fpath, G_MODULE_BIND_LAZY); - PluginInfoFunc info_func; - PluginConstructorFunc init_func; - GObject *plugin; - - if (!module) - { - g_printerr ("ERROR: Failed to load the plugin '%s': %s\n", data->fname, g_module_error ()); - return; - } - - // Try to get a reference to the "get_plugin_info" function - if (!g_module_symbol (module, MODULE_INFO_FUNC, (gpointer *) &info_func)) - { - g_printerr ("ERROR: The plugin-file '%s' has no function named '%s'.\n", data->fname, MODULE_INFO_FUNC); - g_module_close (module); - return; - } - - // Try to get the plugin info - data->info = info_func (); - if (!data->info) - { - g_printerr ("ERROR: The plugin-file '%s' did not return valid plugin info:\n", data->fname); - g_printerr (" The function '%s' returned NULL\n", MODULE_INFO_FUNC); - g_module_close (module); - return; - } - - // Check that the plugin is compatible - if (data->info->plugin_system_version != GNOME_CMD_PLUGIN_SYSTEM_CURRENT_VERSION) - { - g_printerr ("ERROR: The plugin '%s' is not compatible with this version of %s:\n", data->info->name, PACKAGE); - g_printerr (" Plugin system supported by the plugin is %d, it should be %d\n", data->info->plugin_system_version, - GNOME_CMD_PLUGIN_SYSTEM_CURRENT_VERSION); - return; - } - - // Try to get a reference to the "create_plugin" function - if (!g_module_symbol (module, MODULE_INIT_FUNC, (gpointer *) &init_func)) - { - g_printerr ("ERROR: The plugin '%s' has no '%s' function\n", data->info->name, MODULE_INIT_FUNC); - g_module_close (module); - return; - } - - // Try to initialize the plugin - plugin = init_func (); - if (!plugin) - { - g_printerr ("ERROR: The plugin '%s' could not be initialized:\n", data->info->name); - g_printerr (" The '%s' function returned NULL\n", MODULE_INIT_FUNC); - g_module_close (module); - return; - } - - // All is OK, everyone is happy - data->plugin = plugin; - data->module = module; - data->loaded = TRUE; -} - - -static void activate_plugin (PluginData *data) -{ - if (data->active) - return; - - if (!data->loaded) - load_plugin (data); - - if (!data->loaded) - return; - - data->active = TRUE; -} - - -static void inactivate_plugin (PluginData *data) -{ - if (!data->active) - return; - - data->active = FALSE; -} - - -static void scan_plugins_in_dir (const gchar *dpath) -{ - static int index = 0; - auto gFileInfoList = sync_dir_list(dpath); - - if (g_list_length(gFileInfoList) == 0) - return; - - for (auto gFileInfoListItem = gFileInfoList; gFileInfoListItem; gFileInfoListItem = gFileInfoListItem->next) - { - auto gFileInfo = (GFileInfo*) gFileInfoListItem->data; - if (g_file_info_get_file_type(gFileInfo) != G_FILE_TYPE_REGULAR) - continue; - if (!g_str_has_suffix (g_file_info_get_name(gFileInfo), "." G_MODULE_SUFFIX)) - continue; - - // the direntry has the correct extension and is a regular file, let's accept it - PluginData *data = g_new0 (PluginData, 1); - data->fname = g_strdup (g_file_info_get_name(gFileInfo)); - data->fpath = g_build_filename (dpath, g_file_info_get_name(gFileInfo), nullptr); - data->loaded = FALSE; - data->active = FALSE; - data->autoload = FALSE; - data->action_group_name = g_strdup_printf ("plugin%d", index++); - activate_plugin (data); - if (!data->loaded) - { - g_free (data->fname); - g_free (data->fpath); - g_free (data->action_group_name); - g_free (data); - } - else - plugins = g_list_append (plugins, data); - } - g_list_free_full(gFileInfoList, g_object_unref); -} - - -void plugin_manager_init () -{ - if (plugins) - { - g_warning ("plugin_manager already initiated"); - return; - } - - // find user plugins - gchar *user_dir = get_plugin_config_location(); - if (!is_dir_existing(user_dir)) - { - create_dir (user_dir); - } - - scan_plugins_in_dir (user_dir); - - g_free (user_dir); - - // find system plugins - scan_plugins_in_dir (PLUGIN_DIR); - - // activate plugins - for (GList *l=gnome_cmd_data_get_auto_load_plugins (); l; l=l->next) - { - char *name = (gchar *) l->data; - - for (GList *l2 = plugins; l2; l2 = l2->next) - { - auto data = static_cast (l2->data); - if (strcmp (name, data->fname) == 0) - data->autoload = TRUE; - } - } - - // deactivate plugins that shouldn't be autoloaded - for (GList *l=plugins; l; l=l->next) - { - auto data = static_cast (l->data); - if (!data->autoload) - inactivate_plugin (data); - } - - main_win->plugins_updated(); -} - - -void plugin_manager_shutdown () -{ - GList *out = nullptr; - - for (GList *l=plugins; l; l=l->next) - { - auto data = static_cast (l->data); - if (data->active) - out = g_list_append (out, data->fname); - } - - gnome_cmd_data_set_auto_load_plugins (out); -} - - -GList *plugin_manager_get_all () -{ - return plugins; -} - - -static PluginData *get_selected_plugin (GtkTreeView *view) -{ - GtkTreeModel *model; - GtkTreeIter iter; - PluginData *data; - - g_return_val_if_fail (gtk_tree_selection_get_selected (gtk_tree_view_get_selection (view), &model, &iter), nullptr); - gtk_tree_model_get (model, &iter, 4, &data, -1); - return data; -} - - -static void on_plugin_selection_changed (GtkTreeSelection *selection, GtkWidget *dialog) -{ - GtkWidget *toggle_button = lookup_widget (dialog, "toggle_button"); - GtkWidget *conf_button = lookup_widget (dialog, "conf_button"); - GtkWidget *about_button = lookup_widget (dialog, "about_button"); - GtkTreeModel *model; - GtkTreeIter iter; - PluginData *data; - - if (gtk_tree_selection_get_selected (selection, &model, &iter)) - { - gtk_tree_model_get (model, &iter, 4, &data, -1); - gtk_widget_set_sensitive (about_button, TRUE); - gtk_button_set_label (GTK_BUTTON (toggle_button), data->active ? _("Disable") : _("Enable")); - gtk_widget_set_sensitive (toggle_button, TRUE); - gtk_widget_set_sensitive (conf_button, data->active); - } - else - { - gtk_widget_set_sensitive (toggle_button, FALSE); - gtk_widget_set_sensitive (about_button, FALSE); - gtk_widget_set_sensitive (conf_button, FALSE); - } -} - - -static void update_plugin_list (GtkTreeView *view, GtkWidget *dialog) -{ - GtkTreeModel *model = gtk_tree_view_get_model (view); - GtkTreeIter iter; - gboolean only_update = gtk_tree_model_get_iter_first (model, &iter); - GtkListStore *store = GTK_LIST_STORE (model); - - GIcon *exec_icon = g_themed_icon_new ("system-run"); - - for (GList *i=plugins; i; i=i->next) - { - auto data = static_cast (i->data); - - if (only_update) - { - gtk_list_store_set (store, &iter, - 0, data->active ? exec_icon : nullptr, - 1, (gchar *) data->info->name, - 4, data, - -1); - gtk_tree_model_iter_next (model, &iter); - } - else - { - gtk_list_store_append (store, &iter); - gtk_list_store_set (store, &iter, - 0, data->active ? exec_icon : nullptr, - 1, (gchar *) data->info->name, - 2, (gchar *) data->info->version, - 3, data->fname, - 4, data, - -1); - } - } - - on_plugin_selection_changed (gtk_tree_view_get_selection (view), dialog); -} - - -inline void do_toggle (GtkWidget *dialog) -{ - GtkTreeView *view = GTK_TREE_VIEW (lookup_widget (dialog, "avail_view")); - PluginData *data = get_selected_plugin (view); - - if (!data) return; - - if (data->active) - inactivate_plugin (data); - else - activate_plugin (data); - - update_plugin_list (view, dialog); - main_win->plugins_updated(); -} - - -static void on_plugin_activated (GtkTreeView *view, GtkTreePath *path, GtkTreeViewColumn *column, GtkWidget *dialog) -{ - do_toggle (dialog); -} - - -static void on_toggle (GtkButton *button, GtkWidget *dialog) -{ - do_toggle (dialog); -} - - -static void on_configure (GtkButton *button, GtkWidget *dialog) -{ - GtkTreeView *view = GTK_TREE_VIEW (lookup_widget (dialog, "avail_view")); - PluginData *data = get_selected_plugin (view); - - g_return_if_fail (data != nullptr); - g_return_if_fail (data->active); - - if (GNOME_CMD_IS_CONFIGURABLE (data->plugin)) - gnome_cmd_configurable_configure (GNOME_CMD_CONFIGURABLE (data->plugin), GTK_WINDOW (dialog)); -} - - -static void on_about (GtkButton *button, GtkWidget *dialog) -{ - GtkTreeView *view = GTK_TREE_VIEW (lookup_widget (dialog, "avail_view")); - PluginData *data = get_selected_plugin (view); - - g_return_if_fail (data != nullptr); - - GtkWidget *about = gnome_cmd_about_plugin_new (data->info); - - gtk_window_set_transient_for (GTK_WINDOW (about), GTK_WINDOW (dialog)); - gtk_window_present (GTK_WINDOW (about)); -} - - -static void on_close (GtkButton *button, GtkWidget *dialog) -{ - gtk_window_destroy (GTK_WINDOW (dialog)); -} - - -void plugin_manager_show (GtkWindow *parent_window) -{ - GtkWidget *dialog, *hbox, *bbox, *button; - GtkListStore *avail_store; - GtkWidget *avail_view; - - dialog = gnome_cmd_dialog_new (parent_window, _("Available plugins")); - g_object_ref (dialog); - - hbox = create_vbox (dialog, FALSE, 6); - avail_store = gtk_list_store_new (5, G_TYPE_ICON, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_POINTER); - avail_view = create_treeview (dialog, "avail_view", GTK_TREE_MODEL (avail_store), 20, G_CALLBACK (on_plugin_selection_changed), nullptr); - gtk_widget_set_hexpand (GTK_WIDGET (avail_view), TRUE); - gtk_widget_set_vexpand (GTK_WIDGET (avail_view), TRUE); - create_treeview_column (avail_view, 0, 20, ""); - create_treeview_column (avail_view, 1, 200, _("Name")); - create_treeview_column (avail_view, 2, 50, _("Version")); - create_treeview_column (avail_view, 3, 50, _("File")); - - bbox = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 6); - GtkSizeGroup* bbox_size_group = gtk_size_group_new (GTK_SIZE_GROUP_HORIZONTAL); - - button = create_button (GTK_WIDGET (dialog), _("_Enable"), G_CALLBACK (on_toggle)); - g_object_set_data (G_OBJECT (dialog), "toggle_button", button); - gtk_size_group_add_widget (bbox_size_group, button); - gtk_box_append (GTK_BOX (bbox), button); - - button = create_button (GTK_WIDGET (dialog), _("_Configure"), G_CALLBACK (on_configure)); - g_object_set_data (G_OBJECT (dialog), "conf_button", button); - gtk_widget_set_sensitive (button, FALSE); - gtk_size_group_add_widget (bbox_size_group, button); - gtk_box_append (GTK_BOX (bbox), button); - - button = create_button (GTK_WIDGET (dialog), _("_About"), G_CALLBACK (on_about)); - g_object_set_data (G_OBJECT (dialog), "about_button", button); - gtk_widget_set_sensitive (button, FALSE); - gtk_size_group_add_widget (bbox_size_group, button); - gtk_box_append (GTK_BOX (bbox), button); - - gtk_box_append (GTK_BOX (hbox), avail_view); - gtk_box_append (GTK_BOX (hbox), bbox); - - gnome_cmd_dialog_add_expanding_category (GNOME_CMD_DIALOG (dialog), hbox); - - avail_view = lookup_widget (avail_view, "avail_view"); - g_signal_connect (GTK_TREE_VIEW (avail_view), "row-activated", G_CALLBACK (on_plugin_activated), dialog); - - update_plugin_list (GTK_TREE_VIEW (avail_view), dialog); - - gnome_cmd_dialog_add_button (GNOME_CMD_DIALOG (dialog), _("_Close"), G_CALLBACK(on_close), dialog); - - gtk_widget_set_size_request (GTK_WIDGET (dialog), 500, 300); - gtk_window_set_resizable (GTK_WINDOW (dialog), TRUE); - gtk_window_present (GTK_WINDOW (dialog)); -} - -gchar* get_plugin_config_location() -{ - gchar* returnString {nullptr}; - - string userPluginConfigDir = get_package_config_dir(); - userPluginConfigDir += (char*) "/plugins"; - - if (!is_dir_existing(userPluginConfigDir.c_str())) - { - create_dir (userPluginConfigDir.c_str()); - } - - returnString = g_strdup(userPluginConfigDir.c_str()); - - return returnString; -} diff --git a/src/plugin_manager.h b/src/plugin_manager.h deleted file mode 100644 index f47c32a6e..000000000 --- a/src/plugin_manager.h +++ /dev/null @@ -1,45 +0,0 @@ -/** - * @file plugin_manager.h - * @copyright (C) 2001-2006 Marcus Bjurman\n - * @copyright (C) 2007-2012 Piotr Eljasiak\n - * @copyright (C) 2013-2024 Uwe Scholz\n - * - * @copyright 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 2 of the License, or - * (at your option) any later version. - * - * @copyright 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. - * - * @copyright You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -#pragma once - -#include - -struct PluginData -{ - gboolean active; - gboolean loaded; - gboolean autoload; - gchar *fname; - gchar *fpath; - gchar *action_group_name; - GObject *plugin; - PluginInfo *info; - GModule *module; -}; - - -void plugin_manager_init (); -void plugin_manager_shutdown (); -extern "C" GList *plugin_manager_get_all (); -void plugin_manager_show (GtkWindow *parent_window); - -extern "C" GMenu *gnome_cmd_wrap_plugin_menu (const gchar *plugin, GMenuModel *menu); diff --git a/src/plugin_manager.rs b/src/plugin_manager.rs index 90adf6e38..16e194b85 100644 --- a/src/plugin_manager.rs +++ b/src/plugin_manager.rs @@ -1,5 +1,5 @@ /* - * Copyright 2024 Andrey Kutejko + * Copyright 2024-2025 Andrey Kutejko * * 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 @@ -17,16 +17,311 @@ * For more details see the file COPYING. */ +use crate::{ + config::{PACKAGE, PLUGIN_DIR}, + gmodule::{self, GModule, GModuleFlags}, + libgcmd::{ + configurable::{Configurable, ConfigurableExt}, + PluginInfo, GNOME_CMD_PLUGIN_SYSTEM_CURRENT_VERSION, + }, + utils::{dialog_button_box, handle_escape_key, NO_BUTTONS}, +}; +use gettextrs::gettext; use gtk::{ - gio::{self, ffi::GMenu}, + gio, glib::{ - ffi::{gboolean, GList}, + ffi::{gpointer, GType}, gobject_ffi::GObject, - translate::{from_glib_full, from_glib_none, ToGlibPtr}, + translate::{from_glib_full, from_glib_none, FromGlib, IntoGlib}, }, prelude::*, + subclass::prelude::*, }; -use std::ffi::{c_char, c_void}; +use std::{ + fs, io, + path::{Path, PathBuf}, +}; + +const GCMD_PREF_PLUGINS: &str = "org.gnome.gnome-commander.plugins.general"; + +mod imp { + use super::*; + use glib::subclass::Signal; + use std::{cell::RefCell, sync::OnceLock}; + + #[derive(Default)] + pub struct PluginManager { + pub plugins: RefCell>, + } + + #[glib::object_subclass] + impl ObjectSubclass for PluginManager { + const NAME: &'static str = "GnomeCmdPluginManager"; + type Type = super::PluginManager; + } + + impl ObjectImpl for PluginManager { + fn constructed(&self) { + self.parent_constructed(); + self.obj().init(); + } + + fn dispose(&self) { + self.obj().save(); + } + + fn signals() -> &'static [Signal] { + static SIGNALS: OnceLock> = OnceLock::new(); + SIGNALS.get_or_init(|| vec![Signal::builder("plugins-changed").build()]) + } + } +} + +glib::wrapper! { + pub struct PluginManager(ObjectSubclass); +} + +impl PluginManager { + fn init(&self) { + let user_plugin_dir = glib::user_config_dir().join(PACKAGE).join("plugins"); + if let Err(error) = self.scan_plugins_in_dir(&user_plugin_dir) { + eprintln!( + "ERROR: Failed to load plugins from {}: {}", + user_plugin_dir.display(), + error + ); + } + if let Err(error) = self.scan_plugins_in_dir(&Path::new(PLUGIN_DIR)) { + eprintln!( + "ERROR: Failed to load plugins from {}: {}", + PLUGIN_DIR, error + ); + } + for name in gio::Settings::new(GCMD_PREF_PLUGINS).strv("autoload") { + for data in self.imp().plugins.borrow_mut().iter_mut() { + if data.file_name == name { + data.active = true; + } + } + } + } + + fn save(&self) { + let plugins = self.imp().plugins.borrow(); + let autoload: Vec<&str> = plugins + .iter() + .filter(|d| d.active) + .map(|d| d.file_name.as_str()) + .collect(); + if let Err(error) = gio::Settings::new(GCMD_PREF_PLUGINS).set_strv("autoload", autoload) { + eprintln!( + "ERROR: Failed to save autoload plugin list: {}: {}", + PLUGIN_DIR, error.message + ); + } + } + + fn scan_plugins_in_dir(&self, path: &Path) -> io::Result<()> { + for entry in fs::read_dir(path)? { + let entry = entry?; + let path = entry.path(); + let Some(ext) = path.extension() else { + continue; + }; + if ext != "so" && ext != "dylib" && ext != "dll" { + continue; + } + let metadata = entry.metadata()?; + if !metadata.is_file() { + continue; + } + match load_plugin(path) { + Ok(mut data) => { + data.action_group_name = format!("plugin{}", self.imp().plugins.borrow().len()); + self.imp().plugins.borrow_mut().push(data); + } + Err(error) => { + eprintln!("ERROR: {}", error); + } + } + } + Ok(()) + } + + pub fn set_active(&self, plugin: &glib::Object, active: bool) { + let mut changed = false; + for plugin_data in self.imp().plugins.borrow_mut().iter_mut() { + if plugin_data.plugin == *plugin { + plugin_data.active = active; + changed = true; + } + } + if changed { + self.emit_by_name::<()>("plugins-changed", &[]); + } + } + + pub fn active_plugins(&self) -> Vec<(String, glib::Object)> { + self.imp() + .plugins + .borrow() + .iter() + .filter(|d| d.active) + .map(|d| (d.action_group_name.clone(), d.plugin.clone())) + .collect() + } +} + +pub struct PluginData { + pub active: bool, + pub file_name: String, + pub path: PathBuf, + pub action_group_name: String, + pub plugin: glib::Object, + pub info: PluginInfoOwned, + pub module: gmodule::GModule, +} + +#[derive(Clone)] +pub struct PluginInfoOwned { + pub plugin_system_version: u32, + pub name: Option, + pub version: Option, + pub copyright: Option, + pub comments: Option, + pub authors: Vec, + pub documenters: Vec, + pub translator: Option, + pub webpage: Option, +} + +impl From<&PluginInfo> for PluginInfoOwned { + fn from(value: &PluginInfo) -> Self { + unsafe { + Self { + plugin_system_version: value.plugin_system_version, + name: from_glib_none(value.name), + version: from_glib_none(value.version), + copyright: from_glib_none(value.copyright), + comments: from_glib_none(value.comments), + authors: glib::StrV::from_glib_none(value.authors) + .iter() + .map(|s| s.to_string()) + .collect(), + documenters: glib::StrV::from_glib_none(value.documenters) + .iter() + .map(|s| s.to_string()) + .collect(), + translator: from_glib_none(value.translator), + webpage: from_glib_none(value.webpage), + } + } + } +} + +const MODULE_INIT_FUNC: &str = "create_plugin"; +const MODULE_INFO_FUNC: &str = "get_plugin_info"; + +pub type PluginInfoFunc = unsafe extern "C" fn() -> *const PluginInfo; +pub type PluginConstructorFunc = unsafe extern "C" fn() -> *mut GObject; + +fn load_plugin(path: PathBuf) -> Result { + let module = GModule::open( + &path, + GModuleFlags { + lazy: true, + local: false, + }, + ) + .map_err(|error| { + format!( + "ERROR: Failed to load the plugin '{}': {}", + path.display(), + error.message() + ) + })?; + + let info_func = module + .symbol(MODULE_INFO_FUNC) + .ok_or_else(|| { + format!( + "The plugin file '{}' has no function named '{}'.", + path.display(), + MODULE_INFO_FUNC + ) + }) + .and_then(|ptr| { + if ptr.is_null() { + Err(format!( + "The plugin ile '{}' has blank function named '{}'.", + path.display(), + MODULE_INFO_FUNC + )) + } else { + Ok(unsafe { std::mem::transmute::(ptr) }) + } + })?; + + // Try to get the plugin info + let info = unsafe { info_func() }; + if info.is_null() { + return Err(format!("The plugin-file '{}' did not return valid plugin info. The function '{}' returned NULL.", path.display(), MODULE_INFO_FUNC)); + } + + // Check that the plugin is compatible + let plugin_system_version = unsafe { (*info).plugin_system_version }; + if plugin_system_version != GNOME_CMD_PLUGIN_SYSTEM_CURRENT_VERSION { + return Err(format!( + "The plugin '{}' is not compatible with this version of {}: Plugin system supported by the plugin is {}, it should be {}.", path.display(), PACKAGE, plugin_system_version, GNOME_CMD_PLUGIN_SYSTEM_CURRENT_VERSION)); + } + + let info = PluginInfoOwned::from(unsafe { &*info }); + + let init_func = module + .symbol(MODULE_INIT_FUNC) + .ok_or_else(|| { + format!( + "The plugin '{}' has no function named '{}'.", + path.display(), + MODULE_INIT_FUNC + ) + }) + .and_then(|ptr| { + if ptr.is_null() { + Err(format!( + "The plugin '{}' has blank function named '{}'.", + path.display(), + MODULE_INIT_FUNC + )) + } else { + Ok(unsafe { std::mem::transmute::(ptr) }) + } + })?; + + // Try to initialize the plugin + let plugin = unsafe { init_func() }; + if plugin.is_null() { + return Err(format!( + "The plugin '{}' could not be initialized: The '{}' function returned NULL", + path.display(), + MODULE_INIT_FUNC + )); + } + + Ok(PluginData { + file_name: path + .file_name() + .and_then(|s| s.to_str()) + .unwrap_or_default() + .to_owned(), + path, + active: false, + action_group_name: String::new(), + module, + info, + plugin: unsafe { from_glib_full(plugin) }, + }) +} #[derive(Debug, glib::Variant)] pub struct PluginActionVariant { @@ -78,70 +373,193 @@ pub fn wrap_plugin_menu(plugin: &str, menu: &gio::MenuModel) -> gio::Menu { new_menu } -#[no_mangle] -pub extern "C" fn gnome_cmd_wrap_plugin_menu( - plugin_ptr: *const c_char, - menu_ptr: *mut GMenu, -) -> *mut GMenu { - if plugin_ptr.is_null() || menu_ptr.is_null() { - return std::ptr::null_mut(); - } +pub fn show_plugin_manager(plugin_manager: &PluginManager, parent_window: >k::Window) { + let dialog = gtk::Window::builder() + .transient_for(parent_window) + .title(gettext("Available plugins")) + .modal(true) + .width_request(500) + .height_request(300) + .resizable(true) + .build(); - let plugin: String = unsafe { from_glib_none(plugin_ptr) }; - let menu: gio::Menu = unsafe { from_glib_full(menu_ptr) }; + let grid = gtk::Grid::builder() + .margin_top(12) + .margin_bottom(12) + .margin_start(12) + .margin_end(12) + .row_spacing(6) + .column_spacing(12) + .build(); + dialog.set_child(Some(&grid)); - let new_menu = wrap_plugin_menu(&plugin, menu.upcast_ref()); - new_menu.to_glib_full() -} + let view = gtk::ListBox::builder() + .selection_mode(gtk::SelectionMode::None) + .build(); + view.add_css_class("rich-list"); -extern "C" { - fn plugin_manager_get_all() -> *const GList; -} + let sw = gtk::ScrolledWindow::builder() + .child(&view) + .hexpand(true) + .vexpand(true) + .has_frame(true) + .build(); + grid.attach(&sw, 0, 0, 1, 1); -#[repr(C)] -#[derive(Clone)] -struct PluginData { - active: gboolean, - loaded: gboolean, - autoload: gboolean, - fname: *const c_char, - fpath: *const c_char, - action_group_name: *const c_char, - plugin: *const GObject, - info: *const c_void, /* PluginInfo */ - module: *const c_void, /* GModule */ -} + let close_button = gtk::Button::builder() + .label(gettext("_Close")) + .use_underline(true) + .build(); + close_button.connect_clicked(glib::clone!( + #[weak] + dialog, + move |_| dialog.close() + )); + grid.attach(&dialog_button_box(NO_BUTTONS, &[close_button]), 0, 1, 1, 1); -pub fn plugin_manager_get_active_plugin(action_group_name: &str) -> Option { - unsafe { - let mut list = plugin_manager_get_all(); - while !list.is_null() { - let plugin_data = (*list).data as *const PluginData; - if (*plugin_data).active != 0 { - let name: String = from_glib_none((*plugin_data).action_group_name); - if name == action_group_name { - return Some(from_glib_none((*plugin_data).plugin)); - } + handle_escape_key( + &dialog, + >k::CallbackAction::new(glib::clone!( + #[weak] + dialog, + #[upgrade_or] + glib::Propagation::Proceed, + move |_, _| { + dialog.close(); + glib::Propagation::Proceed } - list = (*list).next; - } + )), + ); + + for plugin_data in plugin_manager.imp().plugins.borrow().iter() { + view.append(&create_plugin_widget(plugin_manager, plugin_data)); } - None + + dialog.present(); } -pub fn plugin_manager_get_active_plugins() -> Vec<(String, glib::Object)> { - let mut result = Vec::new(); - unsafe { - let mut list = plugin_manager_get_all(); - while !list.is_null() { - let plugin_data = (*list).data as *const PluginData; - if (*plugin_data).active != 0 { - let name: String = from_glib_none((*plugin_data).action_group_name); - let plugin: glib::Object = from_glib_none((*plugin_data).plugin); - result.push((name, plugin)); +fn create_plugin_widget(plugin_manager: &PluginManager, plugin_data: &PluginData) -> gtk::Widget { + let info = plugin_data.info.clone(); + let plugin = plugin_data.plugin.clone(); + + let grid = gtk::Grid::builder() + .hexpand(true) + .column_spacing(12) + .build(); + + let name = gtk::Label::builder() + .label(info.name.as_deref().unwrap_or(&plugin_data.file_name)) + .halign(gtk::Align::Start) + .hexpand(true) + .build(); + name.add_css_class("title"); + grid.attach(&name, 0, 0, 1, 1); + + let version = gtk::Label::builder() + .label(gettext("Version: {}").replace("{}", info.version.as_deref().unwrap_or("-"))) + .halign(gtk::Align::Start) + .build(); + version.add_css_class("dim-label"); + grid.attach(&version, 0, 1, 1, 1); + + let file = gtk::Label::builder() + .label(gettext("File: {}").replace("{}", &plugin_data.file_name)) + .halign(gtk::Align::Start) + .build(); + file.add_css_class("dim-label"); + grid.attach(&file, 0, 2, 1, 1); + + let about = gtk::Button::builder() + .child(>k::Image::from_icon_name( + "gnome-commander-info-outline-symbolic", + )) + .tooltip_text(gettext("About")) + .vexpand(true) + .valign(gtk::Align::Center) + .build(); + about.add_css_class("flat"); + grid.attach(&about, 1, 0, 1, 3); + + let configure = gtk::Button::builder() + .child(>k::Image::from_icon_name( + "gnome-commander-settings-symbolic", + )) + .tooltip_text(gettext("Configure")) + .vexpand(true) + .valign(gtk::Align::Center) + .build(); + configure.add_css_class("flat"); + grid.attach(&configure, 2, 0, 1, 3); + + let switch = gtk::Switch::builder() + .active(plugin_data.active) + .vexpand(true) + .valign(gtk::Align::Center) + .build(); + grid.attach(&switch, 3, 0, 1, 3); + + about.connect_clicked(move |btn| { + if let Some(window) = btn.root().and_downcast::() { + extern "C" { + fn gnome_cmd_about_plugin_get_type() -> GType; } - list = (*list).next; + + let type_ = unsafe { glib::Type::from_glib(gnome_cmd_about_plugin_get_type()) }; + let about_dialog = glib::object::Object::with_type(type_) + .downcast::() + .unwrap(); + about_dialog.set_property("name", &info.name); + about_dialog.set_property("version", &info.version); + about_dialog.set_property("copyright", &info.copyright); + about_dialog.set_property("comments", &info.comments); + about_dialog.set_property( + "authors", + &info + .authors + .iter() + .map(|v| v.to_value()) + .collect::(), + ); + about_dialog.set_property( + "documenters", + &info + .documenters + .iter() + .map(|v| v.to_value()) + .collect::(), + ); + about_dialog.set_property("translator_credits", &info.translator); + about_dialog.set_property("webpage", &info.webpage); + + about_dialog.set_transient_for(Some(&window)); + about_dialog.present(); } - } - result + }); + + configure.connect_clicked(glib::clone!( + #[strong] + plugin, + move |btn| { + if let Some(window) = btn.root().and_downcast::() { + if let Some(cfg) = plugin.downcast_ref::() { + cfg.configure(&window); + } + } + } + )); + + switch.connect_active_notify(glib::clone!( + #[strong] + plugin_manager, + #[strong] + plugin, + move |switch| plugin_manager.set_active(&plugin, switch.is_active()) + )); + + grid.upcast() +} + +#[no_mangle] +pub extern "C" fn plugin_manager_get_type() -> GType { + PluginManager::static_type().into_glib() } diff --git a/src/user_actions.rs b/src/user_actions.rs index f95dfea53..f063b4386 100644 --- a/src/user_actions.rs +++ b/src/user_actions.rs @@ -51,7 +51,7 @@ use crate::{ file_descriptor::FileDescriptorExt, }, main_win::{ffi::*, MainWindow}, - plugin_manager::{plugin_manager_get_active_plugin, PluginActionVariant}, + plugin_manager::{show_plugin_manager, PluginActionVariant}, spawn::{spawn_async, spawn_async_command, SpawnError}, types::FileSelectorID, utils::{display_help, get_modifiers_state, ErrorMessage}, @@ -797,7 +797,14 @@ pub fn connections_close_current( /************** Plugins Menu ***********/ -c_action!(plugins_configure); +pub fn plugins_configure( + main_win: &MainWindow, + _action: &gio::SimpleAction, + _parameter: Option<&glib::Variant>, +) { + let plugin_manager = main_win.plugin_manager(); + show_plugin_manager(&plugin_manager, main_win.upcast_ref()); +} pub fn plugin_action( main_win: &MainWindow, @@ -809,7 +816,12 @@ pub fn plugin_action( return; }; - if let Some(plugin) = plugin_manager_get_active_plugin(&plugin_action.plugin) { + if let Some(plugin) = main_win + .plugin_manager() + .active_plugins() + .into_iter() + .find_map(|(name, plugin)| (name == plugin_action.plugin).then_some(plugin)) + { if let Some(file_actions) = plugin.downcast_ref::() { file_actions.execute( &plugin_action.action, -- GitLab