/* -*- mode: js2; js2-basic-offset: 4; indent-tabs-mode: nil -*- */ const Clutter = imports.gi.Clutter; const DBus = imports.dbus; const Gdk = imports.gi.Gdk; const Gio = imports.gi.Gio; const GLib = imports.gi.GLib; const Lang = imports.lang; const Mainloop = imports.mainloop; const Meta = imports.gi.Meta; const Shell = imports.gi.Shell; const Signals = imports.signals; const St = imports.gi.St; const Chrome = imports.ui.chrome; const Environment = imports.ui.environment; const Overview = imports.ui.overview; const Panel = imports.ui.panel; const RunDialog = imports.ui.runDialog; const LookingGlass = imports.ui.lookingGlass; const ShellDBus = imports.ui.shellDBus; const Sidebar = imports.ui.sidebar; const WindowManager = imports.ui.windowManager; const DEFAULT_BACKGROUND_COLOR = new Clutter.Color(); DEFAULT_BACKGROUND_COLOR.from_pixel(0x2266bbff); let chrome = null; let panel = null; let sidebar = null; let overview = null; let runDialog = null; let lookingGlass = null; let wm = null; let recorder = null; let shellDBusService = null; let modalCount = 0; let modalActorFocusStack = []; function start() { // Add a binding for "global" in the global JS namespace; (gjs // keeps the web browser convention of having that namespace be // called "window".) window.global = Shell.Global.get(); Gio.DesktopAppInfo.set_desktop_env("GNOME"); global.grab_dbus_service(); shellDBusService = new ShellDBus.GnomeShell(); // Force a connection now; dbus.js will do this internally // if we use its name acquisition stuff but we aren't right // now; to do so we'd need to convert from its async calls // back into sync ones. DBus.session.flush(); Environment.init(); // Ensure ShellAppMonitor is initialized; this will // also initialize ShellAppSystem first. ShellAppSystem // needs to load all the .desktop files, and ShellAppMonitor // will use those to associate with windows. Right now // the Monitor doesn't listen for installed app changes // and recalculate application associations, so to avoid // races for now we initialize it here. It's better to // be predictable anyways. Shell.AppMonitor.get_default(); // The background color really only matters if there is no desktop // window (say, nautilus) running. We set it mostly so things look good // when we are running inside Xephyr. global.stage.color = DEFAULT_BACKGROUND_COLOR; // Mutter currently hardcodes putting "Yessir. The compositor is running"" // in the Overview. Clear that out. let children = global.overlay_group.get_children(); for (let i = 0; i < children.length; i++) children[i].destroy(); let themeContext = St.ThemeContext.get_for_stage (global.stage); let stylesheetPath = global.datadir + "/theme/gnome-shell.css"; let theme = new St.Theme ({ application_stylesheet: stylesheetPath }); themeContext.set_theme (theme); global.connect('panel-run-dialog', function(panel) { // Make sure not more than one run dialog is shown. getRunDialog().open(); }); let shellwm = global.window_manager; shellwm.takeover_keybinding("panel_main_menu"); shellwm.connect("keybinding::panel_main_menu", function () { overview.toggle(); }); shellwm.takeover_keybinding("panel_run_dialog"); shellwm.connect("keybinding::panel_run_dialog", function () { getRunDialog().open(); }); overview = new Overview.Overview(); chrome = new Chrome.Chrome(); panel = new Panel.Panel(); sidebar = new Sidebar.Sidebar(); wm = new WindowManager.WindowManager(); global.screen.connect('toggle-recording', function() { if (recorder == null) { recorder = new Shell.Recorder({ stage: global.stage }); } if (recorder.is_recording()) { recorder.pause(); } else { recorder.record(); } }); _relayout(); panel.startupAnimation(); let display = global.screen.get_display(); display.connect('overlay-key', Lang.bind(overview, overview.toggle)); global.connect('panel-main-menu', Lang.bind(overview, overview.toggle)); global.stage.connect('captured-event', _globalKeyPressHandler); Mainloop.idle_add(_removeUnusedWorkspaces); } function _relayout() { let primary = global.get_primary_monitor(); panel.actor.set_position(primary.x, primary.y); panel.actor.set_size(primary.width, Panel.PANEL_HEIGHT); overview.relayout(); } // metacity-clutter currently uses the same prefs as plain metacity, // which probably means we'll be starting out with multiple workspaces; // remove any unused ones. (We do this from an idle handler, because // global.get_windows() still returns NULL at the point when start() // is called.) function _removeUnusedWorkspaces() { let windows = global.get_windows(); let maxWorkspace = 0; for (let i = 0; i < windows.length; i++) { let win = windows[i]; if (!win.get_meta_window().is_on_all_workspaces() && win.get_workspace() > maxWorkspace) { maxWorkspace = win.get_workspace(); } } let screen = global.screen; if (screen.n_workspaces > maxWorkspace) { for (let w = screen.n_workspaces - 1; w > maxWorkspace; w--) { let workspace = screen.get_workspace_by_index(w); screen.remove_workspace(workspace, 0); } } return false; } // This function encapsulates hacks to make certain global keybindings // work even when we are in one of our modes where global keybindings // are disabled with a global grab. (When there is a global grab, then // all key events will be delivered to the stage, so ::captured-event // on the stage can be used for global keybindings.) // // We expect to need to conditionally enable just a few keybindings // depending on circumstance; the main hackiness here is that we are // assuming that keybindings have their default values; really we // should be asking Mutter to resolve the key into an action and then // base our handling based on the action. function _globalKeyPressHandler(actor, event) { if (modalCount == 0) return false; let type = event.type(); if (type == Clutter.EventType.KEY_PRESS) { let symbol = event.get_key_symbol(); if (symbol == Clutter.Print) { // We want to be able to take screenshots of the shell at all times let gconf = Shell.GConf.get_default(); let command = gconf.get_string("/apps/metacity/keybinding_commands/command_screenshot"); if (command != null && command != "") { let [ok, len, args] = GLib.shell_parse_argv(command); let p = new Shell.Process({'args' : args}); p.run(); } return true; } } else if (type == Clutter.EventType.KEY_RELEASE) { let symbol = event.get_key_symbol(); if (symbol == Clutter.Super_L || symbol == Clutter.Super_R) { // The super key is the default for triggering the overview, and should // get us out of the overview when we are already in it. if (overview.visible) overview.hide(); return true; } else if (symbol == Clutter.F2 && (event.get_state() & Clutter.ModifierType.MOD1_MASK)) { getRunDialog().open(); } } return false; } function _findModal(actor) { for (let i = 0; i < modalActorFocusStack.length; i++) { let [stackActor, stackFocus] = modalActorFocusStack[i]; if (stackActor == actor) { return i; } } return -1; } /** * pushModal: * @actor: #ClutterActor which will be given keyboard focus * * Ensure we are in a mode where all keyboard and mouse input goes to * the stage. Multiple calls to this function act in a stacking fashion; * the effect will be undone when an equal number of popModal() invocations * have been made. * * Next, record the current Clutter keyboard focus on a stack. If the modal stack * returns to this actor, reset the focus to the actor which was focused * at the time pushModal() was invoked. * * Returns: true iff we successfully acquired a grab or already had one */ function pushModal(actor) { if (modalCount == 0) { if (!global.begin_modal(currentTime())) { log("pushModal: invocation of begin_modal failed"); return false; } } global.set_stage_input_mode(Shell.StageInputMode.FULLSCREEN); modalCount += 1; actor.connect('destroy', function() { let index = _findModal(actor); if (index >= 0) modalActorFocusStack.splice(index, 1); }); let curFocus = global.stage.get_key_focus(); if (curFocus != null) { curFocus.connect('destroy', function() { let index = _findModal(actor); if (index >= 0) modalActorFocusStack[index][1] = null; }); } modalActorFocusStack.push([actor, curFocus]); return true; } /** * popModal: * @actor: #ClutterActor passed to original invocation of pushModal(). * * Reverse the effect of pushModal(). If this invocation is undoing * the topmost invocation, then the focus will be restored to the * previous focus at the time when pushModal() was invoked. */ function popModal(actor) { modalCount -= 1; let focusIndex = _findModal(actor); if (focusIndex >= 0) { if (focusIndex == modalActorFocusStack.length - 1) { let [stackActor, stackFocus] = modalActorFocusStack[focusIndex]; global.stage.set_key_focus(stackFocus); } else { // Remove from the middle, shift the focus chain up for (let i = focusIndex; i < modalActorFocusStack.length - 1; i++) { modalActorFocusStack[i + 1][1] = modalActorFocusStack[i][1]; } } modalActorFocusStack.splice(focusIndex, 1); } if (modalCount > 0) return; global.end_modal(currentTime()); global.set_stage_input_mode(Shell.StageInputMode.NORMAL); } function createLookingGlass() { if (lookingGlass == null) { lookingGlass = new LookingGlass.LookingGlass(); lookingGlass.slaveTo(panel.actor); } return lookingGlass; } function getRunDialog() { if (runDialog == null) { runDialog = new RunDialog.RunDialog(); } return runDialog; } function createAppLaunchContext() { let context = new Gdk.AppLaunchContext(); context.set_timestamp(currentTime()); // Make sure that the app is opened on the current workspace even if // the user switches before it starts context.set_desktop(global.screen.get_active_workspace_index()); return context; } /** * currentTime: * * Gets the current X server time from the current Clutter, Gdk, or X * event. If called from outside an event handler, this may return * %Clutter.CURRENT_TIME (aka 0), or it may return a slightly * out-of-date timestamp. */ function currentTime() { // meta_display_get_current_time() will return the correct time // when handling an X or Gdk event, but will return CurrentTime // from some Clutter event callbacks. // // clutter_get_current_event_time() will return the correct time // from a Clutter event callback, but may return an out-of-date // timestamp if called at other times. // // So we try meta_display_get_current_time() first, since we // can recognize a "wrong" answer from that, and then fall back // to clutter_get_current_event_time(). let time = global.screen.get_display().get_current_time(); if (time != Clutter.CURRENT_TIME) return time; return Clutter.get_current_event_time(); } /** * activateWindow: * @window: the Meta.Window to activate * @time: (optional) current event time * * Activates @window, switching to its workspace first if necessary */ function activateWindow(window, time) { let activeWorkspaceNum = global.screen.get_active_workspace_index(); let windowWorkspaceNum = window.get_workspace().index(); if (!time) time = currentTime(); if (windowWorkspaceNum != activeWorkspaceNum) { let workspace = global.screen.get_workspace_by_index(windowWorkspaceNum); workspace.activate_with_focus(window, time); } else { window.activate(time); } }