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>
|
||||
</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>
|
||||
|
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 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();
|
||||
|
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")
|
||||
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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
Loading…
Reference in New Issue
Block a user