/* -*- mode: js2; js2-basic-offset: 4; indent-tabs-mode: nil -*- */ imports.gi.versions.Clutter = '1.0'; imports.gi.versions.Gio = '2.0'; imports.gi.versions.Gdk = '3.0'; imports.gi.versions.GdkPixbuf = '2.0'; imports.gi.versions.Gtk = '3.0'; 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 GConf = imports.gi.GConf; const Lang = imports.lang; const Mainloop = imports.mainloop; const Meta = imports.gi.Meta; const Shell = imports.gi.Shell; const St = imports.gi.St; const Gettext = imports.gettext.domain('gnome-shell'); const _ = Gettext.gettext; const Chrome = imports.ui.chrome; const CtrlAltTab = imports.ui.ctrlAltTab; const EndSessionDialog = imports.ui.endSessionDialog; const PolkitAuthenticationAgent = imports.ui.polkitAuthenticationAgent; const Environment = imports.ui.environment; const ExtensionSystem = imports.ui.extensionSystem; const MessageTray = imports.ui.messageTray; const Overview = imports.ui.overview; const Panel = imports.ui.panel; const PlaceDisplay = imports.ui.placeDisplay; const RunDialog = imports.ui.runDialog; const LookingGlass = imports.ui.lookingGlass; const NotificationDaemon = imports.ui.notificationDaemon; const WindowAttentionHandler = imports.ui.windowAttentionHandler; const Scripting = imports.ui.scripting; const ShellDBus = imports.ui.shellDBus; const TelepathyClient = imports.ui.telepathyClient; const WindowManager = imports.ui.windowManager; const Magnifier = imports.ui.magnifier; const XdndHandler = imports.ui.xdndHandler; const StatusIconDispatcher = imports.ui.statusIconDispatcher; const Util = imports.misc.util; const DEFAULT_BACKGROUND_COLOR = new Clutter.Color(); DEFAULT_BACKGROUND_COLOR.from_pixel(0x2266bbff); let chrome = null; let panel = null; let hotCorners = []; let placesManager = null; let overview = null; let runDialog = null; let lookingGlass = null; let wm = null; let messageTray = null; let notificationDaemon = null; let windowAttentionHandler = null; let telepathyClient = null; let ctrlAltTabManager = null; let recorder = null; let shellDBusService = null; let modalCount = 0; let modalActorFocusStack = []; let uiGroup = null; let magnifier = null; let xdndHandler = null; let statusIconDispatcher = null; let _errorLogStack = []; let _startDate; let _defaultCssStylesheet = null; let _cssStylesheet = null; let background = null; 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(); // Now monkey patch utility functions into the global proxy; // This is easier and faster than indirecting down into global // if we want to call back up into JS. global.logError = _logError; global.log = _logDebug; // Chain up async errors reported from C global.connect('notify-error', function (global, msg, detail) { notifyError(msg, detail); }); Gio.DesktopAppInfo.set_desktop_env('GNOME'); 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(); // Load the calendar server. Note that we are careful about // not loading any events until the user presses the clock global.launch_calendar_server(); 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 stage is always covered so Clutter doesn't need to clear it; however // the color is used as the default contents for the Mutter root background // actor so set it anyways. global.stage.color = DEFAULT_BACKGROUND_COLOR; global.stage.no_clear_hint = true; _defaultCssStylesheet = global.datadir + '/theme/gnome-shell.css'; loadTheme(); 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(); }); // Set up stage hierarchy to group all UI actors under one container. uiGroup = new Clutter.Group(); global.window_group.reparent(uiGroup); global.overlay_group.reparent(uiGroup); global.stage.add_actor(uiGroup); placesManager = new PlaceDisplay.PlacesManager(); xdndHandler = new XdndHandler.XdndHandler(); ctrlAltTabManager = new CtrlAltTab.CtrlAltTabManager(); overview = new Overview.Overview(); chrome = new Chrome.Chrome(); magnifier = new Magnifier.Magnifier(); statusIconDispatcher = new StatusIconDispatcher.StatusIconDispatcher(); panel = new Panel.Panel(); wm = new WindowManager.WindowManager(); messageTray = new MessageTray.MessageTray(); notificationDaemon = new NotificationDaemon.NotificationDaemon(); windowAttentionHandler = new WindowAttentionHandler.WindowAttentionHandler(); telepathyClient = new TelepathyClient.Client(); overview.init(); statusIconDispatcher.start(messageTray.actor); _startDate = new Date(); let recorderSettings = new Gio.Settings({ schema: 'org.gnome.shell.recorder' }); global.screen.connect('toggle-recording', function() { if (recorder == null) { recorder = new Shell.Recorder({ stage: global.stage }); } if (recorder.is_recording()) { recorder.pause(); } else { // read the parameters from GSettings always in case they have changed recorder.set_framerate(recorderSettings.get_int('framerate')); recorder.set_filename('shell-%d%u-%c.' + recorderSettings.get_string('file-extension')); let pipeline = recorderSettings.get_string('pipeline'); if (!pipeline.match(/^\s*$/)) recorder.set_pipeline(pipeline); else recorder.set_pipeline(null); recorder.record(); } }); global.screen.override_workspace_layout(Meta.ScreenCorner.TOPLEFT, false, -1, 1); // Provide the bus object for gnome-session to // initiate logouts. EndSessionDialog.init(); // Attempt to become a PolicyKit authentication agent PolkitAuthenticationAgent.init() global.gdk_screen.connect('monitors-changed', _relayout); ExtensionSystem.init(); ExtensionSystem.loadExtensions(); // Perform initial relayout here _relayout(); panel.startStatusArea(); panel.startupAnimation(); let display = global.screen.get_display(); display.connect('overlay-key', Lang.bind(overview, overview.toggle)); global.stage.connect('captured-event', _globalKeyPressHandler); _log('info', 'loaded at ' + _startDate); log('GNOME Shell started at ' + _startDate); let perfModuleName = GLib.getenv("SHELL_PERF_MODULE"); if (perfModuleName) { let perfOutput = GLib.getenv("SHELL_PERF_OUTPUT"); let module = eval('imports.perf.' + perfModuleName + ';'); Scripting.runPerfScript(module, perfOutput); } global.screen.connect('notify::n-workspaces', _nWorkspacesChanged); Mainloop.idle_add(_nWorkspacesChanged); } let _workspaces = []; let _checkWorkspacesId = 0; /* * When the last window closed on a workspace is a dialog or splash * screen, we assume that it might be an initial window shown before * the main window of an application, and give the app a grace period * where it can map another window before we remove the workspace. */ const LAST_WINDOW_GRACE_TIME = 1000; function _checkWorkspaces() { let i; let emptyWorkspaces = []; for (i = 0; i < _workspaces.length; i++) { let lastRemoved = _workspaces[i]._lastRemovedWindow; if (lastRemoved && (lastRemoved.get_window_type() == Meta.WindowType.SPLASHSCREEN || lastRemoved.get_window_type() == Meta.WindowType.DIALOG || lastRemoved.get_window_type() == Meta.WindowType.MODAL_DIALOG)) emptyWorkspaces[i] = false; else emptyWorkspaces[i] = true; } let windows = global.get_window_actors(); for (i = 0; i < windows.length; i++) { let win = windows[i]; if (win.get_meta_window().is_on_all_workspaces()) continue; let workspaceIndex = win.get_workspace(); emptyWorkspaces[workspaceIndex] = false; } // If we don't have an empty workspace at the end, add one if (!emptyWorkspaces[emptyWorkspaces.length -1]) { global.screen.append_new_workspace(false, global.get_current_time()); emptyWorkspaces.push(false); } let activeWorkspaceIndex = global.screen.get_active_workspace_index(); let removingCurrentWorkspace = (emptyWorkspaces[activeWorkspaceIndex] && activeWorkspaceIndex < emptyWorkspaces.length - 1); // Don't enter the overview when removing multiple empty workspaces at startup let showOverview = (removingCurrentWorkspace && !emptyWorkspaces.every(function(x) { return x; })); if (removingCurrentWorkspace) { // "Merge" the empty workspace we are removing with the one at the end wm.blockAnimations(); } // Delete other empty workspaces; do it from the end to avoid index changes for (i = emptyWorkspaces.length - 2; i >= 0; i--) { if (emptyWorkspaces[i]) global.screen.remove_workspace(_workspaces[i], global.get_current_time()); } if (removingCurrentWorkspace) { global.screen.get_workspace_by_index(global.screen.n_workspaces - 1).activate(global.get_current_time()); wm.unblockAnimations(); if (!overview.visible && showOverview) overview.show(); } _checkWorkspacesId = 0; return false; } function _windowRemoved(workspace, window) { workspace._lastRemovedWindow = window; _queueCheckWorkspaces(); Mainloop.timeout_add(LAST_WINDOW_GRACE_TIME, function() { if (workspace._lastRemovedWindow == window) { workspace._lastRemovedWindow = null; _queueCheckWorkspaces(); } }); } function _queueCheckWorkspaces() { if (_checkWorkspacesId == 0) _checkWorkspacesId = Meta.later_add(Meta.LaterType.BEFORE_REDRAW, _checkWorkspaces); } function _nWorkspacesChanged() { let oldNumWorkspaces = _workspaces.length; let newNumWorkspaces = global.screen.n_workspaces; if (oldNumWorkspaces == newNumWorkspaces) return false; let lostWorkspaces = []; if (newNumWorkspaces > oldNumWorkspaces) { let w; // Assume workspaces are only added at the end for (w = oldNumWorkspaces; w < newNumWorkspaces; w++) _workspaces[w] = global.screen.get_workspace_by_index(w); for (w = oldNumWorkspaces; w < newNumWorkspaces; w++) { let workspace = _workspaces[w]; workspace._windowAddedId = workspace.connect('window-added', _queueCheckWorkspaces); workspace._windowRemovedId = workspace.connect('window-removed', _windowRemoved); } } else { // Assume workspaces are only removed sequentially // (e.g. 2,3,4 - not 2,4,7) let removedIndex; let removedNum = oldNumWorkspaces - newNumWorkspaces; for (let w = 0; w < oldNumWorkspaces; w++) { let workspace = global.screen.get_workspace_by_index(w); if (_workspaces[w] != workspace) { removedIndex = w; break; } } let lostWorkspaces = _workspaces.splice(removedIndex, removedNum); lostWorkspaces.forEach(function(workspace) { workspace.disconnect(workspace._windowAddedId); workspace.disconnect(workspace._windowRemovedId); }); } _queueCheckWorkspaces(); return false; } /** * getThemeStylesheet: * * Get the theme CSS file that the shell will load * * Returns: A file path that contains the theme CSS, * null if using the default */ function getThemeStylesheet() { return _cssStylesheet; } /** * setThemeStylesheet: * @cssStylesheet: A file path that contains the theme CSS, * set it to null to use the default * * Set the theme CSS file that the shell will load */ function setThemeStylesheet(cssStylesheet) { _cssStylesheet = cssStylesheet; } /** * loadTheme: * * Reloads the theme CSS file */ function loadTheme() { let themeContext = St.ThemeContext.get_for_stage (global.stage); let cssStylesheet = _defaultCssStylesheet; if (_cssStylesheet != null) cssStylesheet = _cssStylesheet; let theme = new St.Theme ({ application_stylesheet: cssStylesheet }); themeContext.set_theme (theme); } /** * notifyError: * @msg: An error message * @details: Additional information * * See shell_global_notify_problem(). */ function notifyError(msg, details) { // Also print to stderr so it's logged somewhere if (details) log("error: " + msg + ": " + details); else log("error: " + msg) let source = new MessageTray.SystemNotificationSource(); messageTray.add(source); let notification = new MessageTray.Notification(source, msg, details); notification.setTransient(true); source.notify(notification); } /** * _log: * @category: string message type ('info', 'error') * @msg: A message string * ...: Any further arguments are converted into JSON notation, * and appended to the log message, separated by spaces. * * Log a message into the LookingGlass error * stream. This is primarily intended for use by the * extension system as well as debugging. */ function _log(category, msg) { let text = msg; if (arguments.length > 2) { text += ': '; for (let i = 2; i < arguments.length; i++) { text += JSON.stringify(arguments[i]); if (i < arguments.length - 1) text += ' '; } } _errorLogStack.push({timestamp: new Date().getTime(), category: category, message: text }); } function _logError(msg) { return _log('error', msg); } function _logDebug(msg) { return _log('debug', msg); } // Used by the error display in lookingGlass.js function _getAndClearErrorStack() { let errors = _errorLogStack; _errorLogStack = []; return errors; } function _relayout() { let monitors = global.get_monitors(); if (monitors.length != hotCorners.length) { // destroy old corners for (let i = 0; i < hotCorners.length; i++) hotCorners[i].destroy(); hotCorners = []; for (let i = 0; i < monitors.length; i++) hotCorners[i] = new Panel.HotCorner(); } let primary = global.get_primary_monitor(); for (let i = 0; i < monitors.length; i++) { let monitor = monitors[i]; let corner = hotCorners[i]; let isPrimary = (monitor.x == primary.x && monitor.y == primary.y && monitor.width == primary.width && monitor.height == primary.height); if (St.Widget.get_default_direction() == St.TextDirection.RTL) corner.actor.set_position(monitor.x + monitor.width, monitor.y); else corner.actor.set_position(monitor.x, monitor.y); if (isPrimary) panel.setHotCorner(corner); } panel.relayout(); overview.relayout(); // To avoid updating the position and size of the workspaces // in the overview, we just hide the overview. The positions // will be updated when it is next shown. overview.hide(); } function isWindowActorDisplayedOnWorkspace(win, workspaceIndex) { return win.get_workspace() == workspaceIndex || (win.get_meta_window() && win.get_meta_window().is_on_all_workspaces()); } function getWindowActorsForWorkspace(workspaceIndex) { return global.get_window_actors().filter(function (win) { return isWindowActorDisplayedOnWorkspace(win, workspaceIndex); }); } // 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.) function _globalKeyPressHandler(actor, event) { if (modalCount == 0) return false; if (event.type() != Clutter.EventType.KEY_PRESS) return false; let symbol = event.get_key_symbol(); let keyCode = event.get_key_code(); let modifierState = Shell.get_event_state(event); let display = global.screen.get_display(); // This relies on the fact that Clutter.ModifierType is the same as Gdk.ModifierType let action = display.get_keybinding_action(keyCode, modifierState); // The screenshot action should always be available (even if a // modal dialog is present) if (action == Meta.KeyBindingAction.COMMAND_SCREENSHOT) { let gconf = GConf.Client.get_default(); let command = gconf.get_string('/apps/metacity/keybinding_commands/command_screenshot'); if (command != null && command != '') Util.spawnCommandLine(command); return true; } // Other bindings are only available when the overview is up and // no modal dialog is present. if (!overview.visible || modalCount > 1) return false; // This isn't a Meta.KeyBindingAction yet if (symbol == Clutter.Super_L || symbol == Clutter.Super_R) { overview.hide(); return true; } switch (action) { // left/right would effectively act as synonyms for up/down if we enabled them; // but that could be considered confusing; we also disable them in the main view. // // case Meta.KeyBindingAction.WORKSPACE_LEFT: // wm.actionMoveWorkspaceLeft(); // return true; // case Meta.KeyBindingAction.WORKSPACE_RIGHT: // wm.actionMoveWorkspaceRight(); // return true; case Meta.KeyBindingAction.WORKSPACE_UP: wm.actionMoveWorkspaceUp(); return true; case Meta.KeyBindingAction.WORKSPACE_DOWN: wm.actionMoveWorkspaceDown(); return true; case Meta.KeyBindingAction.PANEL_RUN_DIALOG: case Meta.KeyBindingAction.COMMAND_2: getRunDialog().open(); return true; case Meta.KeyBindingAction.PANEL_MAIN_MENU: overview.hide(); return true; case Meta.KeyBindingAction.SWITCH_PANELS: ctrlAltTabManager.popup(modifierState & Clutter.ModifierType.SHIFT_MASK); return true; } return false; } function _findModal(actor) { for (let i = 0; i < modalActorFocusStack.length; i++) { if (modalActorFocusStack[i].actor == actor) return i; } return -1; } /** * pushModal: * @actor: #ClutterActor which will be given keyboard focus * @timestamp: optional timestamp * * Ensure we are in a mode where all keyboard and mouse input goes to * the stage, and focus @actor. 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. * * @timestamp is optionally used to associate the call with a specific user * initiated event. If not provided then the value of * global.get_current_time() is assumed. * * Returns: true iff we successfully acquired a grab or already had one */ function pushModal(actor, timestamp) { if (timestamp == undefined) timestamp = global.get_current_time(); if (modalCount == 0) { if (!global.begin_modal(timestamp)) { log('pushModal: invocation of begin_modal failed'); return false; } } global.set_stage_input_mode(Shell.StageInputMode.FULLSCREEN); modalCount += 1; let actorDestroyId = actor.connect('destroy', function() { let index = _findModal(actor); if (index >= 0) modalActorFocusStack.splice(index, 1); }); let curFocus = global.stage.get_key_focus(); let curFocusDestroyId; if (curFocus != null) { curFocusDestroyId = curFocus.connect('destroy', function() { let index = _findModal(actor); if (index >= 0) modalActorFocusStack[index].actor = null; }); } modalActorFocusStack.push({ actor: actor, focus: curFocus, destroyId: actorDestroyId, focusDestroyId: curFocusDestroyId }); global.stage.set_key_focus(actor); return true; } /** * popModal: * @actor: #ClutterActor passed to original invocation of pushModal(). * @timestamp: optional timestamp * * 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. * * @timestamp is optionally used to associate the call with a specific user * initiated event. If not provided then the value of * global.get_current_time() is assumed. */ function popModal(actor, timestamp) { if (timestamp == undefined) timestamp = global.get_current_time(); let focusIndex = _findModal(actor); if (focusIndex < 0) { global.stage.set_key_focus(null); global.end_modal(timestamp); global.set_stage_input_mode(Shell.StageInputMode.NORMAL); throw new Error('incorrect pop'); } modalCount -= 1; let record = modalActorFocusStack[focusIndex]; record.actor.disconnect(record.destroyId); if (focusIndex == modalActorFocusStack.length - 1) { if (record.focus) record.focus.disconnect(record.focusDestroyId); global.stage.set_key_focus(record.focus); } else { let t = modalActorFocusStack[modalActorFocusStack.length - 1]; if (t.focus) t.focus.disconnect(t.focusDestroyId); // Remove from the middle, shift the focus chain up for (let i = modalActorFocusStack.length - 1; i > focusIndex; i--) { modalActorFocusStack[i].focus = modalActorFocusStack[i - 1].focus; modalActorFocusStack[i].focusDestroyId = modalActorFocusStack[i - 1].focusDestroyId; } } modalActorFocusStack.splice(focusIndex, 1); if (modalCount > 0) return; global.end_modal(timestamp); 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; } /** * activateWindow: * @window: the Meta.Window to activate * @time: (optional) current event time * @workspaceNum: (optional) window's workspace number * * Activates @window, switching to its workspace first if necessary, * and switching out of the overview if it's currently active */ function activateWindow(window, time, workspaceNum) { let activeWorkspaceNum = global.screen.get_active_workspace_index(); let windowWorkspaceNum = (workspaceNum !== undefined) ? workspaceNum : window.get_workspace().index(); if (!time) time = global.get_current_time(); if (windowWorkspaceNum != activeWorkspaceNum) { let workspace = global.screen.get_workspace_by_index(windowWorkspaceNum); workspace.activate_with_focus(window, time); } else { window.activate(time); } overview.hide(); } // TODO - replace this timeout with some system to guess when the user might // be e.g. just reading the screen and not likely to interact. const DEFERRED_TIMEOUT_SECONDS = 20; var _deferredWorkData = {}; // Work scheduled for some point in the future var _deferredWorkQueue = []; // Work we need to process before the next redraw var _beforeRedrawQueue = []; // Counter to assign work ids var _deferredWorkSequence = 0; var _deferredTimeoutId = 0; function _runDeferredWork(workId) { if (!_deferredWorkData[workId]) return; let index = _deferredWorkQueue.indexOf(workId); if (index < 0) return; _deferredWorkQueue.splice(index, 1); _deferredWorkData[workId].callback(); if (_deferredWorkQueue.length == 0 && _deferredTimeoutId > 0) { Mainloop.source_remove(_deferredTimeoutId); _deferredTimeoutId = 0; } } function _runAllDeferredWork() { while (_deferredWorkQueue.length > 0) _runDeferredWork(_deferredWorkQueue[0]); } function _runBeforeRedrawQueue() { for (let i = 0; i < _beforeRedrawQueue.length; i++) { let workId = _beforeRedrawQueue[i]; _runDeferredWork(workId); } _beforeRedrawQueue = []; } function _queueBeforeRedraw(workId) { _beforeRedrawQueue.push(workId); if (_beforeRedrawQueue.length == 1) { Meta.later_add(Meta.LaterType.BEFORE_REDRAW, function () { _runBeforeRedrawQueue(); return false; }); } } /** * initializeDeferredWork: * @actor: A #ClutterActor * @callback: Function to invoke to perform work * * This function sets up a callback to be invoked when either the * given actor is mapped, or after some period of time when the machine * is idle. This is useful if your actor isn't always visible on the * screen (for example, all actors in the overview), and you don't want * to consume resources updating if the actor isn't actually going to be * displaying to the user. * * Note that queueDeferredWork is called by default immediately on * initialization as well, under the assumption that new actors * will need it. * * Returns: A string work identifer */ function initializeDeferredWork(actor, callback, props) { // Turn into a string so we can use as an object property let workId = '' + (++_deferredWorkSequence); _deferredWorkData[workId] = { 'actor': actor, 'callback': callback }; actor.connect('notify::mapped', function () { if (!(actor.mapped && _deferredWorkQueue.indexOf(workId) >= 0)) return; _queueBeforeRedraw(workId); }); actor.connect('destroy', function() { let index = _deferredWorkQueue.indexOf(workId); if (index >= 0) _deferredWorkQueue.splice(index, 1); delete _deferredWorkData[workId]; }); queueDeferredWork(workId); return workId; } /** * queueDeferredWork: * @workId: work identifier * * Ensure that the work identified by @workId will be * run on map or timeout. You should call this function * for example when data being displayed by the actor has * changed. */ function queueDeferredWork(workId) { let data = _deferredWorkData[workId]; if (!data) { global.logError('invalid work id ', workId); return; } if (_deferredWorkQueue.indexOf(workId) < 0) _deferredWorkQueue.push(workId); if (data.actor.mapped) { _queueBeforeRedraw(workId); return; } else if (_deferredTimeoutId == 0) { _deferredTimeoutId = Mainloop.timeout_add_seconds(DEFERRED_TIMEOUT_SECONDS, function () { _runAllDeferredWork(); _deferredTimeoutId = 0; return false; }); } }