From 9a76c53c8e8808c46aaa61914f625e29a7b6cc58 Mon Sep 17 00:00:00 2001
From: Cyrille Bagard <nocbos@gmail.com>
Date: Sun, 6 Dec 2020 20:52:37 +0100
Subject: Added a panel to display recently used Python scripts.

---
 plugins/python/scripting/Makefile.am               |   6 +-
 plugins/python/scripting/core.py                   |  43 ++++++-
 plugins/python/scripting/manager.py                |  53 ++++++++
 plugins/python/scripting/panel.py                  | 140 +++++++++++++++++++++
 plugins/python/scripting/panel.ui                  |  70 +++++++++++
 .../python/scripting/python-script-icon-16x16.png  | Bin 0 -> 601 bytes
 6 files changed, 307 insertions(+), 5 deletions(-)
 create mode 100644 plugins/python/scripting/manager.py
 create mode 100644 plugins/python/scripting/panel.py
 create mode 100644 plugins/python/scripting/panel.ui
 create mode 100644 plugins/python/scripting/python-script-icon-16x16.png

diff --git a/plugins/python/scripting/Makefile.am b/plugins/python/scripting/Makefile.am
index 5d38d6e..3c44f1f 100644
--- a/plugins/python/scripting/Makefile.am
+++ b/plugins/python/scripting/Makefile.am
@@ -3,6 +3,10 @@ scriptingdir = $(pluginsdatadir)/python/scripting
 
 scripting_DATA = 							\
 	__init__.py								\
-	core.py
+	core.py									\
+	manager.py								\
+	panel.py								\
+	panel.ui								\
+	python-script-icon-16x16.png
 
 EXTRA_DIST = $(scripting_DATA)
diff --git a/plugins/python/scripting/core.py b/plugins/python/scripting/core.py
index 7aff551..71afaf2 100644
--- a/plugins/python/scripting/core.py
+++ b/plugins/python/scripting/core.py
@@ -4,8 +4,12 @@ from gi.repository import Gtk
 from pychrysalide import PluginModule
 from pychrysalide import core
 from pychrysalide.gui import core as gcore
+from pychrysalide.gui import MenuBar
 from pychrysalide.gtkext import EasyGtk
 
+from .manager import get_recent_python_script_manager, remember_python_script
+from .panel import ScriptPanel
+
 
 class ScriptingEngine(PluginModule):
     """Extend the GUI to run external Python scripts."""
@@ -15,7 +19,7 @@ class ScriptingEngine(PluginModule):
     _version = '0.1'
     _url = 'https://www.chrysalide.re/'
 
-    _actions = ( )
+    _actions = ( PluginModule.PluginAction.PLUGINS_LOADED, PluginModule.PluginAction.PANEL_CREATION )
 
 
     def __init__(self):
@@ -23,9 +27,13 @@ class ScriptingEngine(PluginModule):
 
         super(ScriptingEngine, self).__init__()
 
+        # Scripts panel
+
+        gcore.register_panel(ScriptPanel)
+
         # Insert the new menu item into 'File' submenu
 
-        bar = gcore.find_editor_item_by_key('menubar')
+        bar = gcore.find_editor_item_by_type(MenuBar)
 
         builder = gcore.get_editor_builder()
 
@@ -54,6 +62,25 @@ class ScriptingEngine(PluginModule):
         file_menu.insert(item, index)
 
 
+    def _notify_plugins_loaded(self, action):
+        """Ack the full loading of all plugins."""
+
+        if action == PluginModule.PluginAction.PLUGINS_LOADED:
+
+            filename = self.build_config_filename('recents.xbel', True)
+
+            get_recent_python_script_manager(filename)
+
+
+    def _on_panel_creation(self, action, item):
+        """Get notified of a new panel creation."""
+
+        if type(item) == ScriptPanel:
+
+            item.connect('run-requested', self._on_run_requested)
+            item.connect('ask-for-new-script', lambda x: self._on_file_run_script_activate(None))
+
+
     def _on_file_run_script_activate(self, widget):
         """Look for a new script to run."""
 
@@ -87,7 +114,9 @@ class ScriptingEngine(PluginModule):
     def _run_script_file(self, filename):
         """Run a given script file."""
 
-        core.log_message(core.LogMessageType.INFO, 'Execute the script file \'%s\'' % filename)
+        self.log_message(core.LogMessageType.INFO, 'Execute the script file \'%s\'' % filename)
+
+        remember_python_script(filename)
 
         try:
             with open(filename, 'r') as fd:
@@ -98,4 +127,10 @@ class ScriptingEngine(PluginModule):
             eval(code)
 
         except Exception as e:
-            core.log_message(core.LogMessageType.EXT_ERROR, 'Error while running the script: %s' % str(e))
+            self.log_message(core.LogMessageType.EXT_ERROR, 'Error while running the script: %s' % str(e))
+
+
+    def _on_run_requested(self, panel, filename):
+        """Run a script file from the recents panel."""
+
+        self._run_script_file(filename)
diff --git a/plugins/python/scripting/manager.py b/plugins/python/scripting/manager.py
new file mode 100644
index 0000000..6b27b48
--- /dev/null
+++ b/plugins/python/scripting/manager.py
@@ -0,0 +1,53 @@
+
+import os
+
+from gi.repository import GLib, Gtk
+
+
+_manager = None
+
+
+def get_recent_python_script_manager(xbel = None):
+    """Provide the manager for the recently run Python scripts."""
+
+    global _manager
+
+    # As a first panel creation is forced by the Chrysalide core to register
+    # its final GType, xbel is not defined at the first call of this function.
+    # Thus relying on the definition of xbel is a better filter than relying
+    # on the existence of _manager.
+    #
+    # In that special initial case, result is None
+
+    if not(xbel is None):
+
+        assert(_manager is None)
+
+        _manager = Gtk.RecentManager(filename=xbel)
+
+    return _manager
+
+
+def remember_python_script(filename):
+    """Register a Python script into the recents list."""
+
+    uri = GLib.filename_to_uri(filename)
+
+    recent_data = Gtk.RecentData()
+    recent_data.app_name = 'Chrysalide Python plugin'
+    recent_data.app_exec = 'chrysalide'
+    recent_data.display_name = os.path.basename(filename)
+    recent_data.description = 'Python script run inside Chrysalide'
+    recent_data.mime_type = 'text/x-python'
+
+    manager = get_recent_python_script_manager()
+    manager.add_full(uri, recent_data)
+
+
+def forget_python_script(filename):
+    """Unregister a Python script from the recents list."""
+
+    uri = GLib.filename_to_uri(filename)
+
+    manager = get_recent_python_script_manager()
+    manager.remove_item(uri)
diff --git a/plugins/python/scripting/panel.py b/plugins/python/scripting/panel.py
new file mode 100644
index 0000000..75b50e3
--- /dev/null
+++ b/plugins/python/scripting/panel.py
@@ -0,0 +1,140 @@
+
+import os
+from gi.repository import Gdk, GdkPixbuf, GLib, GObject
+from pychrysalide.gtkext import BuiltNamedWidget
+from pychrysalide.gui import core
+from pychrysalide.gui import PanelItem
+
+from .manager import get_recent_python_script_manager, forget_python_script
+
+
+class ScriptPanel(PanelItem):
+
+    _key = 'pyscripting'
+
+    _path = 'MEN'
+    _key_bindings = '<Shift>F5'
+
+
+    def __init__(self):
+        """Initialize the GUI panel."""
+
+        directory = os.path.dirname(os.path.realpath(__file__))
+        filename = os.path.join(directory, 'panel.ui')
+
+        widget = BuiltNamedWidget('Python scripts', 'Recently run Python scripts', filename)
+
+        super(ScriptPanel, self).__init__(widget)
+
+        if not('run-requested' in GObject.signal_list_names(ScriptPanel)):
+
+            GObject.signal_new('run-requested', ScriptPanel, GObject.SignalFlags.RUN_FIRST,
+                               GObject.TYPE_NONE, (GObject.TYPE_STRING, ))
+
+            GObject.signal_new('ask-for-new-script', ScriptPanel, GObject.SignalFlags.RUN_FIRST,
+                               GObject.TYPE_NONE, ())
+
+        self._last_selected = None
+
+        builder = self.named_widget.builder
+
+        icon_renderer = builder.get_object('icon_renderer')
+        icon_renderer.props.xpad = 8
+
+        builder.connect_signals(self)
+
+        manager = get_recent_python_script_manager()
+
+        if manager:
+
+            manager.connect("changed", self._on_recent_list_changed)
+
+            self._on_recent_list_changed(manager)
+
+
+    def _on_row_activated(self, treeview, path, column):
+        """React on a row activation."""
+
+        store = self.named_widget.builder.get_object('store')
+
+        siter = store.get_iter(path)
+
+        self.emit('run-requested', store[siter][3])
+
+
+    def _on_selection_changed(self, selection):
+        """React on tree selection change."""
+
+        model, treeiter = selection.get_selected()
+
+        if treeiter:
+            self._last_selected = model[treeiter][3]
+        else:
+            self._last_selected = None
+
+
+    def on_key_press_event(self, widget, event):
+        """React on a key press inside the tree view."""
+
+        if event.keyval == Gdk.KEY_Delete:
+
+            selection = self.named_widget.builder.get_object('selection')
+
+            model, treeiter = selection.get_selected()
+
+            if treeiter:
+
+                forget_python_script(model[treeiter][3])
+
+        elif event.keyval == Gdk.KEY_Insert:
+
+            self.emit('ask-for-new-script')
+
+
+    def _add_entry(self, filename):
+        """Add an entry for a new recent Python script."""
+
+        directory = os.path.dirname(os.path.realpath(__file__))
+        icon_filename = os.path.join(directory, 'python-script-icon-16x16.png')
+
+        icon = GdkPixbuf.Pixbuf.new_from_file(icon_filename)
+        name = os.path.basename(filename)
+        path = os.path.dirname(filename)
+
+        store = self.named_widget.builder.get_object('store')
+
+        store.append([ icon, name, path, filename ])
+
+
+    def _on_recent_list_changed(self, manager):
+        """React on resources manager content change."""
+
+        saved = self._last_selected
+
+        # Register the new item
+
+        builder = self.named_widget.builder
+
+        store = builder.get_object('store')
+        store.clear()
+
+        for item in manager.get_items():
+
+            filename = GLib.filename_from_uri(item.get_uri())[0]
+
+            self._add_entry(filename)
+
+        # Restore previous selection
+
+        selection = builder.get_object('selection')
+
+        siter = store.iter_children()
+
+        while siter:
+
+            if store[siter][3] == saved:
+
+                selection.select_iter(siter)
+                break
+
+            siter = store.iter_next(siter)
diff --git a/plugins/python/scripting/panel.ui b/plugins/python/scripting/panel.ui
new file mode 100644
index 0000000..f87088a
--- /dev/null
+++ b/plugins/python/scripting/panel.ui
@@ -0,0 +1,70 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Generated with glade 3.37.0 -->
+<interface>
+  <requires lib="gtk+" version="3.20"/>
+  <object class="GtkListStore" id="store">
+    <columns>
+      <!-- column-name icon -->
+      <column type="GdkPixbuf"/>
+      <!-- column-name name -->
+      <column type="gchararray"/>
+      <!-- column-name path -->
+      <column type="gchararray"/>
+      <!-- column-name filename -->
+      <column type="gchararray"/>
+    </columns>
+  </object>
+  <object class="GtkScrolledWindow" id="box">
+    <property name="visible">True</property>
+    <property name="can-focus">True</property>
+    <property name="shadow-type">in</property>
+    <child>
+      <object class="GtkViewport">
+        <property name="visible">True</property>
+        <property name="can-focus">False</property>
+        <child>
+          <object class="GtkTreeView" id="treeview">
+            <property name="visible">True</property>
+            <property name="can-focus">True</property>
+            <property name="model">store</property>
+            <signal name="key-press-event" handler="on_key_press_event" swapped="no"/>
+            <signal name="row-activated" handler="_on_row_activated" swapped="no"/>
+            <child internal-child="selection">
+              <object class="GtkTreeSelection" id="selection">
+                <signal name="changed" handler="_on_selection_changed" swapped="no"/>
+              </object>
+            </child>
+            <child>
+              <object class="GtkTreeViewColumn">
+                <property name="title" translatable="yes">Filename</property>
+                <child>
+                  <object class="GtkCellRendererPixbuf" id="icon_renderer"/>
+                  <attributes>
+                    <attribute name="pixbuf">0</attribute>
+                  </attributes>
+                </child>
+                <child>
+                  <object class="GtkCellRendererText" id="name_renderer"/>
+                  <attributes>
+                    <attribute name="text">1</attribute>
+                  </attributes>
+                </child>
+              </object>
+            </child>
+            <child>
+              <object class="GtkTreeViewColumn">
+                <property name="title" translatable="yes">Path</property>
+                <child>
+                  <object class="GtkCellRendererText" id="path_renderer"/>
+                  <attributes>
+                    <attribute name="text">2</attribute>
+                  </attributes>
+                </child>
+              </object>
+            </child>
+          </object>
+        </child>
+      </object>
+    </child>
+  </object>
+</interface>
diff --git a/plugins/python/scripting/python-script-icon-16x16.png b/plugins/python/scripting/python-script-icon-16x16.png
new file mode 100644
index 0000000..c96599e
Binary files /dev/null and b/plugins/python/scripting/python-script-icon-16x16.png differ
-- 
cgit v0.11.2-87-g4458