gnome-shell/js/ui/main.js
Colin Walters e941e8088b Split ShellAppMonitor into ShellWindowTracker, ShellAppUsage
The two parts were mapping windows to applications, and
recording application usage statistics.  The latter part
(now called ShellAppUsage) is much more naturally built on top of
the former (now called ShellWindowTracker).

ShellWindowTracker retains the startup-notification handling.

ShellWindowTracker also gains a focus-app property, which is
what most things in the shell UI are interested in (instead of
window focus).

ShellAppSystem moves to exporting ShellApp from more of its
public API, rather than ShellAppInfo.  ShellAppSystem also
ensures that ShellApp instances are unique by holding
a hash on the ids.

ShellApp's private API is split off into a shell-app-private.h,
so shell-app.h can be included in shell-app-system.h.

Favorites handling is removed from ShellAppSystem, now inside
appFavorites.js.

Port all of the JavaScript for these changes.

https://bugzilla.gnome.org/show_bug.cgi?id=598646
2009-10-20 12:55:07 -04:00

372 lines
12 KiB
JavaScript

/* -*- 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 ShellWindowTracker and ShellAppUsage are initialized; this will
// also initialize ShellAppSystem first. ShellAppSystem
// needs to load all the .desktop files, and ShellWindowTracker
// 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.WindowTracker.get_default();
Shell.AppUsage.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 && (Shell.get_event_state(event) & 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);
}
}