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
This commit is contained in:
parent
4394bc3e40
commit
aa9d3515a1
@ -88,6 +88,21 @@
|
|||||||
</locale>
|
</locale>
|
||||||
</schema>
|
</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>
|
</schemalist>
|
||||||
|
|
||||||
</gconfschemafile>
|
</gconfschemafile>
|
||||||
|
158
js/ui/extensionSystem.js
Normal file
158
js/ui/extensionSystem.js
Normal file
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -14,6 +14,7 @@ const St = imports.gi.St;
|
|||||||
|
|
||||||
const Chrome = imports.ui.chrome;
|
const Chrome = imports.ui.chrome;
|
||||||
const Environment = imports.ui.environment;
|
const Environment = imports.ui.environment;
|
||||||
|
const ExtensionSystem = imports.ui.extensionSystem;
|
||||||
const Overview = imports.ui.overview;
|
const Overview = imports.ui.overview;
|
||||||
const Panel = imports.ui.panel;
|
const Panel = imports.ui.panel;
|
||||||
const PlaceDisplay = imports.ui.placeDisplay;
|
const PlaceDisplay = imports.ui.placeDisplay;
|
||||||
@ -129,6 +130,9 @@ function start() {
|
|||||||
|
|
||||||
_relayout();
|
_relayout();
|
||||||
|
|
||||||
|
ExtensionSystem.init();
|
||||||
|
ExtensionSystem.loadExtensions();
|
||||||
|
|
||||||
panel.startupAnimation();
|
panel.startupAnimation();
|
||||||
|
|
||||||
let display = global.screen.get_display();
|
let display = global.screen.get_display();
|
||||||
|
83
src/gnome-shell.in
Executable file → Normal file
83
src/gnome-shell.in
Executable file → Normal file
@ -212,6 +212,8 @@ parser.add_option("-w", "--wide", action="store_true",
|
|||||||
help="Use widescreen (1280x800) with Xephyr")
|
help="Use widescreen (1280x800) with Xephyr")
|
||||||
parser.add_option("", "--eval-file", metavar="EVAL_FILE",
|
parser.add_option("", "--eval-file", metavar="EVAL_FILE",
|
||||||
help="Evaluate the contents of the given JavaScript 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()
|
options, args = parser.parse_args()
|
||||||
|
|
||||||
@ -219,6 +221,87 @@ if args:
|
|||||||
parser.print_usage()
|
parser.print_usage()
|
||||||
sys.exit(1)
|
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:
|
if options.eval_file:
|
||||||
import dbus
|
import dbus
|
||||||
|
|
||||||
|
@ -522,6 +522,73 @@ shell_global_display_is_grabbed (ShellGlobal *global)
|
|||||||
return meta_display_get_grab_op (display) != META_GRAB_OP_NONE;
|
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.
|
/* Code to close all file descriptors before we exec; copied from gspawn.c in GLib.
|
||||||
*
|
*
|
||||||
* Authors: Padraig O'Briain, Matthias Clasen, Lennart Poettering
|
* Authors: Padraig O'Briain, Matthias Clasen, Lennart Poettering
|
||||||
|
@ -43,6 +43,12 @@ ShellGlobal *shell_global_get (void);
|
|||||||
|
|
||||||
MetaScreen *shell_global_get_screen (ShellGlobal *global);
|
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);
|
void shell_global_grab_dbus_service (ShellGlobal *global);
|
||||||
|
|
||||||
typedef enum {
|
typedef enum {
|
||||||
|
Loading…
Reference in New Issue
Block a user