From bc558306a409fc9d80ea1b1d4a4887ae63d53e0c Mon Sep 17 00:00:00 2001 From: Dan Winship Date: Wed, 6 May 2009 14:36:50 -0400 Subject: [PATCH] Improve the framework for managing shell UI Adds an explicit "chrome" layer for the panel (and later the sidebar), managing the input region and struts for them, and hiding them when fullscreen windows are present or the user enters the overlay. http://bugzilla.gnome.org/show_bug.cgi?id=581771 --- js/ui/Makefile.am | 1 + js/ui/chrome.js | 347 ++++++++++++++++++++++++++++++++++++++++++++++ js/ui/main.js | 91 +----------- js/ui/panel.js | 42 +----- 4 files changed, 353 insertions(+), 128 deletions(-) create mode 100644 js/ui/chrome.js diff --git a/js/ui/Makefile.am b/js/ui/Makefile.am index f9d0f9b8c..8465b9e84 100644 --- a/js/ui/Makefile.am +++ b/js/ui/Makefile.am @@ -4,6 +4,7 @@ dist_jsui_DATA = \ altTab.js \ appDisplay.js \ button.js \ + chrome.js \ dnd.js \ docDisplay.js \ genericDisplay.js \ diff --git a/js/ui/chrome.js b/js/ui/chrome.js new file mode 100644 index 000000000..a49842b9a --- /dev/null +++ b/js/ui/chrome.js @@ -0,0 +1,347 @@ +/* -*- mode: js2; js2-basic-offset: 4; indent-tabs-mode: nil -*- */ + +const Clutter = imports.gi.Clutter; +const Lang = imports.lang; +const Mainloop = imports.mainloop; +const Meta = imports.gi.Meta; +const Shell = imports.gi.Shell; + +const Main = imports.ui.main; + +// This manages the shell "chrome"; the UI that's visible in the +// normal mode (ie, outside the overlay), that surrounds the main +// workspace content. + +function Chrome() { + this._init(); +} + +Chrome.prototype = { + _init: function() { + let global = Shell.Global.get(); + + this.actor = new Clutter.Group(); + global.stage.add_actor(this.actor); + this.nonOverlayActor = new Clutter.Group(); + this.actor.add_actor(this.nonOverlayActor); + + this._obscuredByFullscreen = false; + + this._trackedActors = []; + + global.screen.connect('restacked', + Lang.bind(this, this._windowsRestacked)); + + // Need to update struts on new workspaces when they are added + global.screen.connect('notify::n-workspaces', + Lang.bind(this, this._queueUpdateRegions)); + + Main.overlay.connect('showing', + Lang.bind(this, this._overlayShowing)); + Main.overlay.connect('hidden', + Lang.bind(this, this._overlayHidden)); + + this._queueUpdateRegions(); + }, + + _verifyAncestry: function(actor, ancestor) { + while (actor) { + if (actor == ancestor) + return true; + actor = actor.get_parent(); + } + return false; + }, + + // addActor: + // @actor: an actor to add to the chrome layer + // @shapeActor: optional "shape actor". + // + // Adds @actor to the chrome layer and extends the input region + // and window manager struts to include it. (Window manager struts + // will only be affected if @actor is touching a screen edge.) + // Changes in @actor's size and position will automatically result + // in appropriate changes to the input region and struts. Changes + // in its visibility will affect the input region, but NOT the + // struts. + // + // If @shapeActor is provided, it will be used instead of @actor + // for the input region/strut shape. (This lets you have things like + // drop shadows in @actor that don't affect the struts.) It must + // be a child of @actor. Alternatively, you can pass %null for + // @shapeActor to indicate that @actor should not affect the input + // region or struts at all. + addActor: function(actor, shapeActor) { + if (shapeActor === undefined) + shapeActor = actor; + else if (shapeActor && !this._verifyAncestry(shapeActor, actor)) + throw new Error('shapeActor is not a descendent of actor'); + + this.nonOverlayActor.add_actor(actor); + + if (shapeActor) + this._trackActor(shapeActor, true, true); + }, + + // setVisibleInOverlay: + // @actor: an actor in the chrome layer + // @visible: overlay visibility + // + // By default, actors in the chrome layer are automatically hidden + // when the overlay is shown. This can be used to override that + // behavior + setVisibleInOverlay: function(actor, visible) { + if (!this._verifyAncestry(actor, this.actor)) + throw new Error('actor is not a descendent of the chrome layer'); + + if (visible) + actor.reparent(this.actor); + else + actor.reparent(this.nonOverlayActor); + }, + + // addInputRegionActor: + // @actor: an actor to add to the stage input region + // + // Adds @actor to the stage input region, as with addActor(), but + // for actors that are already descendants of the chrome layer. + addInputRegionActor: function(actor) { + if (!this._verifyAncestry(actor, this.actor)) + throw new Error('actor is not a descendent of the chrome layer'); + + this._trackActor(actor, true, false); + }, + + // removeInputRegionActor: + // @actor: an actor previously added to the stage input region + // + // Undoes the effect of addInputRegionActor() + removeInputRegionActor: function(actor) { + this._untrackActor(actor, true, false); + }, + + // removeActor: + // @actor: a child of the chrome layer + // + // Removes @actor from the chrome layer + removeActor: function(actor) { + this.actor.remove_actor(actor); + // We don't have to do anything else; the parent-set handlers + // will do the rest. + }, + + _findActor: function(actor) { + for (let i = 0; i < this._trackedActors.length; i++) { + let actorData = this._trackedActors[i]; + if (actorData.actor == actor) + return i; + } + return -1; + }, + + _trackActor: function(actor, inputRegion, strut) { + let actorData; + let i = this._findActor(actor); + + if (i != -1) { + actorData = this._trackedActors[i]; + if (inputRegion) + actorData.inputRegion++; + if (strut) + actorData.strut++; + if (!inputRegion && !strut) + actorData.children++; + return; + } + + actorData = { actor: actor, + inputRegion: inputRegion ? 1 : 0, + strut: strut ? 1 : 0, + children: 0 }; + + actorData.visibleId = actor.connect('notify::visible', + Lang.bind(this, this._queueUpdateRegions)); + actorData.allocationId = actor.connect('notify::allocation', + Lang.bind(this, this._queueUpdateRegions)); + actorData.parentSetId = actor.connect('parent-set', + Lang.bind(this, this._actorReparented)); + + this._trackedActors.push(actorData); + + actor = actor.get_parent(); + if (actor != this.actor) + this._trackActor(actor, false, false); + + if (inputRegion || strut) + this._queueUpdateRegions(); + }, + + _untrackActor: function(actor, inputRegion, strut) { + let i = this._findActor(actor); + + if (i == -1) + return; + let actorData = this._trackedActors[i]; + + if (inputRegion) + actorData.inputRegion--; + if (strut) + actorData.strut--; + if (!inputRegion && !strut) + actorData.children--; + + if (actorData.inputRegion <= 0 && actorData.strut <= 0 && actorData.children <= 0) { + this._trackedActors.splice(i, 1); + actor.disconnect(actorData.visibleId); + actor.disconnect(actorData.allocationId); + actor.disconnect(actorData.parentSetId); + + actor = actor.get_parent(); + if (actor && actor != this.actor) + this._untrackActor(actor, false, false); + } + + if (inputRegion || strut) + this._queueUpdateRegions(); + }, + + _actorReparented: function(actor, oldParent) { + if (this._verifyAncestry(actor, this.actor)) { + let newParent = actor.get_parent(); + if (newParent != this.actor) + this._trackActor(newParent, false, false); + } + if (oldParent != this.actor) + this._untrackActor(oldParent, false, false); + }, + + _overlayShowing: function() { + this.actor.show(); + this.nonOverlayActor.hide(); + this._queueUpdateRegions(); + }, + + _overlayHidden: function() { + if (this._obscuredByFullscreen) + this.actor.hide(); + this.nonOverlayActor.show(); + this._queueUpdateRegions(); + }, + + _queueUpdateRegions: function() { + if (!this._updateRegionIdle) + this._updateRegionIdle = Mainloop.idle_add(Lang.bind(this, this._updateRegions)); + }, + + _windowsRestacked: function() { + let global = Shell.Global.get(); + let windows = global.get_windows(); + + // The chrome layer should be visible unless there is a window + // with layer FULLSCREEN, or a window with layer + // OVERRIDE_REDIRECT that covers the whole screen. + // ("override_redirect" is not actually a layer above all + // other windows, but this seems to be how mutter treats it + // currently...) If we wanted to be extra clever, we could + // figure out when an OVERRIDE_REDIRECT window was trying to + // partially overlap us, and then adjust the input region and + // our clip region accordingly... + + // @windows is sorted bottom to top. + + this._obscuredByFullscreen = false; + for (let i = windows.length - 1; i > -1; i--) { + let layer = windows[i].get_meta_window().get_layer(); + + if (layer == Meta.StackLayer.OVERRIDE_REDIRECT) { + if (windows[i].x <= 0 && + windows[i].x + windows[i].width >= global.screen_width && + windows[i].y <= 0 && + windows[i].y + windows[i].height >= global.screen_height) { + this._obscuredByFullscreen = true; + break; + } + } else if (layer == Meta.StackLayer.FULLSCREEN) { + this._obscuredByFullscreen = true; + break; + } else + break; + } + + let shouldBeVisible = !this._obscuredByFullscreen || Main.overlay.visible; + if (this.actor.visible != shouldBeVisible) { + this.actor.visible = shouldBeVisible; + this._queueUpdateRegions(); + } + }, + + _updateRegions: function() { + let global = Shell.Global.get(); + let rects = [], struts = [], i; + + delete this._updateRegionIdle; + + for (i = 0; i < this._trackedActors.length; i++) { + let actorData = this._trackedActors[i]; + if (!actorData.inputRegion && !actorData.strut) + continue; + + let [x, y] = actorData.actor.get_transformed_position(); + let [w, h] = actorData.actor.get_transformed_size(); + let rect = new Meta.Rectangle({ x: x, y: y, width: w, height: h}); + + if (actorData.inputRegion && actorData.actor.get_paint_visibility()) + rects.push(rect); + + if (!actorData.strut) + continue; + + // Metacity wants to know what side of the screen the + // strut is considered to be attached to. If the actor is + // only touching one edge, or is touching the entire + // width/height of one edge, then it's obvious which side + // to call it. If it's in a corner, we pick a side + // arbitrarily. If it doesn't touch any edges, or it spans + // the width/height across the middle of the screen, then + // we don't create a strut for it at all. + let side; + if (w >= global.screen_width) { + if (y <= 0) + side = Meta.Side.TOP; + else if (y + h >= global.screen_height) + side = Meta.Side.BOTTOM; + else + continue; + } else if (h >= global.screen_height) { + if (x <= 0) + side = Meta.Side.LEFT; + else if (x + w >= global.screen_width) + side = Meta.Side.RIGHT; + else + continue; + } else if (x <= 0) + side = Meta.Side.LEFT; + else if (y <= 0) + side = Meta.Side.TOP; + else if (x + w >= global.screen_width) + side = Meta.Side.RIGHT; + else if (y + h >= global.screen_height) + side = Meta.Side.BOTTOM; + else + continue; + + let strut = new Meta.Strut({ rect: rect, side: side }); + struts.push(strut); + } + + global.set_stage_input_region(rects); + + let screen = global.screen; + for (let w = 0; w < screen.n_workspaces; w++) { + let workspace = screen.get_workspace_by_index(w); + workspace.set_builtin_struts(struts); + } + + return false; + } +}; diff --git a/js/ui/main.js b/js/ui/main.js index 531ac3861..6886f8aae 100644 --- a/js/ui/main.js +++ b/js/ui/main.js @@ -9,6 +9,7 @@ const Meta = imports.gi.Meta; const Shell = imports.gi.Shell; const Signals = imports.signals; +const Chrome = imports.ui.chrome; const Overlay = imports.ui.overlay; const Panel = imports.ui.panel; const RunDialog = imports.ui.runDialog; @@ -18,6 +19,7 @@ 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 overlay = null; let runDialog = null; @@ -62,8 +64,8 @@ function start() { }); overlay = new Overlay.Overlay(); + chrome = new Chrome.Chrome(); panel = new Panel.Panel(); - wm = new WindowManager.WindowManager(); global.screen.connect('toggle-recording', function() { @@ -87,9 +89,6 @@ function start() { display.connect('overlay-key', Lang.bind(overlay, overlay.toggle)); global.connect('panel-main-menu', Lang.bind(overlay, overlay.toggle)); - // Need to update struts on new workspaces when they are added - global.screen.connect('notify::n-workspaces', _setStageArea); - Mainloop.idle_add(_removeUnusedWorkspaces); } @@ -160,87 +159,3 @@ function createAppLaunchContext() { return context; } - -let _shellActors = []; - -// For adding an actor that is part of the shell in the normal desktop view -function addShellActor(actor) { - let global = Shell.Global.get(); - - _shellActors.push(actor); - - actor.connect('notify::visible', _setStageArea); - actor.connect('destroy', function(actor) { - let i = _shellActors.indexOf(actor); - if (i != -1) - _shellActors.splice(i, 1); - _setStageArea(); - }); - - while (actor != global.stage) { - actor.connect('notify::allocation', _setStageArea); - actor = actor.get_parent(); - } - - _setStageArea(); -} - -function _setStageArea() { - let global = Shell.Global.get(); - let rects = [], struts = []; - - for (let i = 0; i < _shellActors.length; i++) { - if (!_shellActors[i].visible) - continue; - - let [x, y] = _shellActors[i].get_transformed_position(); - let [w, h] = _shellActors[i].get_transformed_size(); - - let rect = new Meta.Rectangle({ x: x, y: y, width: w, height: h}); - rects.push(rect); - - // Metacity wants to know what side of the screen the strut is - // considered to be attached to. If the actor is only touching - // one edge, or is touching the entire width/height of one - // edge, then it's obvious which side to call it. If it's in a - // corner, we pick a side arbitrarily. If it doesn't touch any - // edges, or it spans the width/height across the middle of - // the screen, then we don't create a strut for it at all. - let side; - if (w >= global.screen_width) { - if (y <= 0) - side = Meta.Side.TOP; - else if (y + h >= global.screen_height) - side = Meta.Side.BOTTOM; - else - continue; - } else if (h >= global.screen_height) { - if (x <= 0) - side = Meta.Side.LEFT; - else if (x + w >= global.screen_width) - side = Meta.Side.RIGHT; - else - continue; - } else if (x <= 0) - side = Meta.Side.LEFT; - else if (y <= 0) - side = Meta.Side.TOP; - else if (x + w >= global.screen_width) - side = Meta.Side.RIGHT; - else if (y + h >= global.screen_height) - side = Meta.Side.BOTTOM; - else - continue; - - let strut = new Meta.Strut({ rect: rect, side: side }); - struts.push(strut); - } - - let screen = global.screen; - for (let w = 0; w < screen.n_workspaces; w++) { - let workspace = screen.get_workspace_by_index(w); - workspace.set_builtin_struts(struts); - } - - global.set_stage_input_region(rects); -} diff --git a/js/ui/panel.js b/js/ui/panel.js index cd8d43515..aadae9af2 100644 --- a/js/ui/panel.js +++ b/js/ui/panel.js @@ -159,51 +159,13 @@ Panel.prototype = { this.actor.add_actor(box); - global.stage.add_actor(this.actor); - // Declare just "box" (ie, not the drop shadow) as a shell actor - Main.addShellActor(box); - - global.screen.connect('restacked', Lang.bind(this, this._restacked)); - this._restacked(); + Main.chrome.addActor(this.actor, box); + Main.chrome.setVisibleInOverlay(this.actor, true); // Start the clock this._updateClock(); }, - _restacked: function() { - let global = Shell.Global.get(); - let windows = global.get_windows(); - let i; - - // We want to be visible unless there is a window with layer - // FULLSCREEN, or a window with layer OVERRIDE_REDIRECT that - // completely covers us. (We can't set a non-rectangular - // stage_input_area, so we don't let windows overlap us - // partially.). "override_redirect" is not actually a layer - // above all other windows, but this seems to be how mutter - // treats it currently... - // - // @windows is sorted bottom to top. - this.actor.show(); - for (i = windows.length - 1; i > -1; i--) { - let layer = windows[i].get_meta_window().get_layer(); - - if (layer == Meta.StackLayer.OVERRIDE_REDIRECT) { - if (windows[i].x <= this.actor.x && - windows[i].x + windows[i].width >= this.actor.x + this.actor.width && - windows[i].y <= this.actor.y && - windows[i].y + windows[i].height >= this.actor.y + PANEL_HEIGHT) { - this.actor.hide(); - break; - } - } else if (layer == Meta.StackLayer.FULLSCREEN) { - this.actor.hide(); - break; - } else - break; - } - }, - _updateClock: function() { let displayDate = new Date(); let msecRemaining = 60000 - (1000 * displayDate.getSeconds() +