[gnome-shell] Add extensionSystem



commit aa9d3515a1afd124668e7a749d9602b3a547e924
Author: Colin Walters <walters verbum org>
Date:   Sun Oct 25 18:53:10 2009 -0400

    Add extensionSystem
    
    Consumer documentation will live at http://live.gnome.org/GnomeShell/Extensions
    
    In terms of implementation; basically we load extensions from the well-known
    directories.  Add a GConf key to disable extensions by uuid.  There is a new
    option --create-extension for the gnome-shell script which takes a bit of
    interactive input, sets up some sample files, and launches gedit.
    
    No extensions UI in this patch; that will come later.
    
    https://bugzilla.gnome.org/show_bug.cgi?id=599661

 data/gnome-shell.schemas |   15 +++++
 js/ui/extensionSystem.js |  158 ++++++++++++++++++++++++++++++++++++++++++++++
 js/ui/main.js            |    4 +
 src/gnome-shell.in       |   83 ++++++++++++++++++++++++
 src/shell-global.c       |   67 +++++++++++++++++++
 src/shell-global.h       |    6 ++
 6 files changed, 333 insertions(+), 0 deletions(-)
---
diff --git a/data/gnome-shell.schemas b/data/gnome-shell.schemas
index 9ace3bc..4e1f496 100644
--- a/data/gnome-shell.schemas
+++ b/data/gnome-shell.schemas
@@ -88,6 +88,21 @@
 	</locale>
       </schema>
 
+      <schema>
+        <key>/schemas/desktop/gnome/shell/disabled_extensions</key>
+        <applyto>/desktop/gnome/shell/disabled_extensions</applyto>
+        <owner>gnome-shell</owner>
+        <type>list</type>
+        <list_type>string</list_type>
+        <default>[]</default>
+        <locale name="C">
+          <short>Uuids of extensions to disable</short>
+          <long>
+            GNOME Shell extensions have a uuid property; this key lists extensions which should not be loaded.
+         </long>
+        </locale>
+      </schema>
+
   </schemalist>
 
 </gconfschemafile>
diff --git a/js/ui/extensionSystem.js b/js/ui/extensionSystem.js
new file mode 100644
index 0000000..6e28623
--- /dev/null
+++ b/js/ui/extensionSystem.js
@@ -0,0 +1,158 @@
+/* -*- mode: js2; js2-basic-offset: 4; indent-tabs-mode: nil -*- */
+
+const GLib = imports.gi.GLib;
+const Gio = imports.gi.Gio;
+const St = imports.gi.St;
+const Shell = imports.gi.Shell;
+
+const ExtensionState = {
+    ENABLED: 1,
+    DISABLED: 2,
+    ERROR: 3,
+    OUT_OF_DATE: 4
+};
+
+const ExtensionType = {
+    SYSTEM: 1,
+    PER_USER: 2
+};
+
+// Maps uuid -> metadata object
+const extensionMeta = {};
+// Maps uuid -> importer object (extension directory tree)
+const extensions = {};
+// Array of uuids
+var disabledExtensions;
+// GFile for user extensions
+var userExtensionsDir = null;
+
+function loadExtension(dir, enabled, type) {
+    let info;
+    let baseErrorString = 'While loading extension from "' + dir.get_parse_name() + '": ';
+
+    let metadataFile = dir.get_child('metadata.json');
+    if (!metadataFile.query_exists(null)) {
+        global.logError(baseErrorString + 'Missing metadata.json');
+        return;
+    }
+
+    let [success, metadataContents, len, etag] = metadataFile.load_contents(null);
+    let meta;
+    try {
+        meta = JSON.parse(metadataContents);
+    } catch (e) {
+        global.logError(baseErrorString + 'Failed to parse metadata.json: ' + e);
+        return;
+    }
+    let requiredProperties = ['uuid', 'name', 'description'];
+    for (let i = 0; i < requiredProperties; i++) {
+        let prop = requiredProperties[i];
+        if (!meta[prop]) {
+            global.logError(baseErrorString + 'missing "' + prop + '" property in metadata.json');
+            return;
+        }
+    }
+    // Encourage people to add this
+    if (!meta['url']) {
+        global.log(baseErrorString + 'Warning: Missing "url" property in metadata.json');
+    }
+
+    let base = dir.get_basename();
+    if (base != meta.uuid) {
+        global.logError(baseErrorString + 'uuid "' + meta.uuid + '" from metadata.json does not match directory name "' + base + '"');
+        return;
+    }
+
+    extensionMeta[meta.uuid] = meta;
+    extensionMeta[meta.uuid].type = type;
+    extensionMeta[meta.uuid].path = dir.get_path();
+    if (!enabled) {
+        extensionMeta[meta.uuid].state = ExtensionState.DISABLED;
+        return;
+    }
+
+    // Default to error, we set success as the last step
+    extensionMeta[meta.uuid].state = ExtensionState.ERROR;
+
+    let extensionJs = dir.get_child('extension.js');
+    if (!extensionJs.query_exists(null)) {
+        global.logError(baseErrorString + 'Missing extension.js');
+        return;
+    }
+    let stylesheetPath = null;
+    let themeContext = St.ThemeContext.get_for_stage(global.stage);
+    let theme = themeContext.get_theme();
+    let stylesheetFile = dir.get_child('stylesheet.css');
+    if (stylesheetFile.query_exists(null)) {
+        try {
+            theme.load_stylesheet(stylesheetFile.get_path());
+        } catch (e) {
+            global.logError(baseErrorString + 'Stylesheet parse error: ' + e);
+            return;
+        }
+    }
+
+    let extensionModule;
+    try {
+        global.add_extension_importer('imports.ui.extensionSystem.extensions', meta.uuid, dir.get_path());
+        extensionModule = extensions[meta.uuid].extension;
+    } catch (e) {
+        if (stylesheetPath != null)
+            theme.unload_stylesheet(stylesheetPath);
+        global.logError(baseErrorString + e);
+        return;
+    }
+    if (!extensionModule.main) {
+        global.logError(baseErrorString + 'missing \'main\' function');
+        return;
+    }
+    try {
+        extensionModule.main();
+    } catch (e) {
+        if (stylesheetPath != null)
+            theme.unload_stylesheet(stylesheetPath);
+        global.logError(baseErrorString + 'Failed to evaluate main function:' + e);
+        return;
+    }
+    extensionMeta[meta.uuid].state = ExtensionState.ENABLED;
+    global.log('Loaded extension ' + meta.uuid);
+}
+
+function init() {
+    let userConfigPath = GLib.get_user_config_dir();
+    let userExtensionsPath = GLib.build_filenamev([userConfigPath, 'gnome-shell', 'extensions']);
+    userExtensionsDir = Gio.file_new_for_path(userExtensionsPath);
+    try {
+        userExtensionsDir.make_directory_with_parents(null);
+    } catch (e) {
+        global.logError(""+e);
+    }
+
+    disabledExtensions = Shell.GConf.get_default().get_string_list('disabled_extensions');
+}
+
+function _loadExtensionsIn(dir, type) {
+    let fileEnum = dir.enumerate_children('standard::*', Gio.FileQueryInfoFlags.NONE, null);
+    let file, info;
+    while ((info = fileEnum.next_file(null)) != null) {
+        let fileType = info.get_file_type();
+        if (fileType != Gio.FileType.DIRECTORY)
+            continue;
+        let name = info.get_name();
+        let enabled = disabledExtensions.indexOf(name) < 0;
+        let child = dir.get_child(name);
+        loadExtension(child, enabled, type);
+    }
+    fileEnum.close(null);
+}
+
+function loadExtensions() {
+    _loadExtensionsIn(userExtensionsDir, ExtensionType.PER_USER);
+    let systemDataDirs = GLib.get_system_data_dirs();
+    for (let i = 0; i < systemDataDirs.length; i++) {
+        let dirPath = systemDataDirs[i] + '/gnome-shell/extensions';
+        let dir = Gio.file_new_for_path(dirPath);
+        if (dir.query_exists(null))
+            _loadExtensionsIn(dir, ExtensionType.SYSTEM);
+    }
+}
diff --git a/js/ui/main.js b/js/ui/main.js
index d481cc7..7361ac3 100644
--- a/js/ui/main.js
+++ b/js/ui/main.js
@@ -14,6 +14,7 @@ const St = imports.gi.St;
 
 const Chrome = imports.ui.chrome;
 const Environment = imports.ui.environment;
+const ExtensionSystem = imports.ui.extensionSystem;
 const Overview = imports.ui.overview;
 const Panel = imports.ui.panel;
 const PlaceDisplay = imports.ui.placeDisplay;
@@ -129,6 +130,9 @@ function start() {
 
     _relayout();
 
+    ExtensionSystem.init();
+    ExtensionSystem.loadExtensions();
+
     panel.startupAnimation();
 
     let display = global.screen.get_display();
diff --git a/src/gnome-shell.in b/src/gnome-shell.in
old mode 100755
new mode 100644
index b009ec4..62c86d6
--- a/src/gnome-shell.in
+++ b/src/gnome-shell.in
@@ -212,6 +212,8 @@ parser.add_option("-w", "--wide", action="store_true",
                   help="Use widescreen (1280x800) with Xephyr")
 parser.add_option("", "--eval-file", metavar="EVAL_FILE",
                   help="Evaluate the contents of the given JavaScript file")
+parser.add_option("", "--create-extension", action="store_true",
+                  help="Create a new GNOME Shell extension")
 
 options, args = parser.parse_args()
 
@@ -219,6 +221,87 @@ if args:
     parser.print_usage()
     sys.exit(1)
 
+if options.create_extension:
+    import json
+
+    print
+    print '''Name should be a very short (ideally descriptive) string.
+Examples are: "Click To Focus",  "Adblock", "Shell Window Shrinker".
+'''
+    name = raw_input('Name: ').strip()
+    print
+    print '''Description is a single-sentence explanation of what your extension does.
+Examples are: "Make windows visible on click", "Block advertisement popups"
+              "Animate windows shrinking on minimize"
+'''
+    description = raw_input('Description: ').strip()
+    underifier = re.compile('[^A-Za-z]')
+    sample_uuid = underifier.sub('_', name)
+    # TODO use evolution data server
+    hostname = subprocess.Popen(['hostname'], stdout=subprocess.PIPE).communicate()[0].strip()
+    sample_uuid = sample_uuid + '@' + hostname
+
+    print
+    print '''Uuid is a globally-unique identifier for your extension.
+This should be in the format of an email address (foo bar extensions example com), but
+need not be an actual email address, though it's a good idea to base the uuid on your
+email address.  For example, if your email address is janedoe example com, you might
+use an extension title clicktofocus janedoe example com '''
+    uuid = raw_input('Uuid [%s]: ' % (sample_uuid, )).strip()
+    if uuid == '':
+        uuid = sample_uuid
+
+    extension_path = os.path.join(os.path.expanduser('~/.config'), 'gnome-shell', 'extensions', uuid)
+    if os.path.exists(extension_path):
+        print "Extension path %r already exists" % (extension_path, )
+        sys.exit(0)
+    os.makedirs(extension_path)
+    meta = { 'name': name,
+             'description': description,
+             'uuid': uuid }
+    f = open(os.path.join(extension_path, 'metadata.json'), 'w')
+    json.dump(meta, f)
+    f.close()
+
+    extensionjs_path = os.path.join(extension_path, 'extension.js')
+    f = open(extensionjs_path, 'w')
+    f.write('''// Sample extension code, makes clicking on the panel show a message
+const St = imports.gi.St;
+const Mainloop = imports.mainloop;
+
+const Main = imports.ui.main;
+
+function _showHello() {
+    let text = new St.Label({ style_class: 'helloworld-label', text: "Hello, world!" });
+    let monitor = global.get_primary_monitor();
+    global.stage.add_actor(text);
+    text.set_position(Math.floor (monitor.width / 2 - text.width / 2), Math.floor(monitor.height / 2 - text.height / 2));
+    Mainloop.timeout_add(3000, function () { text.destroy(); });
+}
+
+// Put your extension initialization code here
+function main() {
+    Main.panel.actor.reactive = true;
+    Main.panel.actor.connect('button-release-event', _showHello);
+}
+''')
+    f.close()
+
+    f = open(os.path.join(extension_path, 'stylesheet.css'), 'w')
+    f.write('''/* Example stylesheet */
+.helloworld-label {
+    font-size: 36px;
+    font-weight: bold;
+    color: #ffffff;
+    background-color: rgba(10,10,10,0.7);
+    border-radius: 5px;
+}
+''')
+    f.close()
+
+    subprocess.Popen(['gedit', extensionjs_path])
+    sys.exit(0)
+
 if options.eval_file:
     import dbus
 
diff --git a/src/shell-global.c b/src/shell-global.c
index 542f28d..ac68bc5 100644
--- a/src/shell-global.c
+++ b/src/shell-global.c
@@ -522,6 +522,73 @@ shell_global_display_is_grabbed (ShellGlobal *global)
   return meta_display_get_grab_op (display) != META_GRAB_OP_NONE;
 }
 
+/* Defining this here for now, see
+ * https://bugzilla.gnome.org/show_bug.cgi?id=604075
+ * for upstreaming status.
+ */
+JSContext * gjs_context_get_context (GjsContext *context);
+
+/**
+ * shell_global_add_extension_importer:
+ * @target_object_script: JavaScript code evaluating to a target object
+ * @target_property: Name of property to use for importer
+ * @directory: Source directory:
+ * @error: A #GError
+ *
+ * This function sets a property named @target_property on the object
+ * resulting from the evaluation of @target_object_script code, which
+ * acts as a GJS importer for directory @directory.
+ *
+ * Returns: %TRUE on success
+ */
+gboolean
+shell_global_add_extension_importer (ShellGlobal *global,
+                                     const char  *target_object_script,
+                                     const char  *target_property,
+                                     const char  *directory,
+                                     GError     **error)
+{
+  jsval target_object;
+  JSObject *importer;
+  JSContext *context = gjs_context_get_context (global->js_context);
+  char *search_path[2] = { 0, 0 };
+
+  // This is a bit of a hack; ideally we'd be able to pass our target
+  // object directly into this function, but introspection doesn't
+  // support that at the moment.  Instead evaluate a string to get it.
+  if (!JS_EvaluateScript(context,
+                         JS_GetGlobalObject(context),
+                         target_object_script,
+                         strlen (target_object_script),
+                         "<target_object_script>",
+                         0,
+                         &target_object))
+    {
+      char *message;
+      gjs_log_exception(context,
+                        &message);
+      g_set_error(error,
+                  G_IO_ERROR,
+                  G_IO_ERROR_FAILED,
+                  "%s", message ? message : "(unknown)");
+      g_free(message);
+      return FALSE;
+    }
+
+  if (!JSVAL_IS_OBJECT (target_object))
+    {
+      g_set_error(error,
+                  G_IO_ERROR,
+                  G_IO_ERROR_FAILED,
+                  "Invalid object");
+      return FALSE;
+    }
+
+  search_path[0] = (char*)directory;
+  importer = gjs_define_importer (context, JSVAL_TO_OBJECT (target_object), target_property, (const char **)search_path, FALSE);
+  return TRUE;
+}
+
 /* Code to close all file descriptors before we exec; copied from gspawn.c in GLib.
  *
  * Authors: Padraig O'Briain, Matthias Clasen, Lennart Poettering
diff --git a/src/shell-global.h b/src/shell-global.h
index ef70b6a..12ebf50 100644
--- a/src/shell-global.h
+++ b/src/shell-global.h
@@ -43,6 +43,12 @@ ShellGlobal *shell_global_get (void);
 
 MetaScreen *shell_global_get_screen (ShellGlobal  *global);
 
+gboolean shell_global_add_extension_importer (ShellGlobal *global,
+                                              const char  *target_object_script,
+                                              const char  *target_property,
+                                              const char  *directory,
+                                              GError     **error);
+
 void shell_global_grab_dbus_service (ShellGlobal *global);
 
 typedef enum {



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]