Rewrite Dash, remove hardcoded width/height from GenericDisplay

This patch is a near-total rewrite of the Dash.  First, the dash
code moves into a separate file, dash.js.

Inside dash.js, the components are more broken up into separate
classes; in particular there's now a Pane class and a MoreLink
class.  Instead of each section of the dash, when activated,
attempting to close all N-1 other sections, instead there
is the concept of a single "active pane", and when e.g. activating
the More link for documents, if we know there's an active pane
which happens to be the apps, close it.

Many redundant containers were removed from the dash, and all
manual width, height and x/y offsets are entirely gone.  We move
the visual apperance closer to the design by using the view-more.svg,
etc.

To complete the removal of height/width calculations from the dash,
we also had to do the same for GenericDisplay.  Also clean up
the positioning inside overlay.js so calculation of children's
positioning is inside a single function that flows from screen.width
and screen.height, so in the future we can stop passing the width
into the Dash constructor and call this once and work on screen
resizing.
This commit is contained in:
Colin Walters 2009-07-31 22:12:01 -04:00
parent 2726fdb831
commit 85b4b97b7b
5 changed files with 730 additions and 843 deletions

View File

@ -35,17 +35,16 @@ const MAX_ITEMS = 30;
/* This class represents a single display item containing information about an application.
*
* appInfo - AppInfo object containing information about the application
* availableWidth - total width available for the item
*/
function AppDisplayItem(appInfo, availableWidth) {
this._init(appInfo, availableWidth);
function AppDisplayItem(appInfo) {
this._init(appInfo);
}
AppDisplayItem.prototype = {
__proto__: GenericDisplay.GenericDisplayItem.prototype,
_init : function(appInfo, availableWidth) {
GenericDisplay.GenericDisplayItem.prototype._init.call(this, availableWidth);
_init : function(appInfo) {
GenericDisplay.GenericDisplayItem.prototype._init.call(this);
this._appInfo = appInfo;
this._setItemInfo(appInfo.get_name(), appInfo.get_description());
@ -115,7 +114,6 @@ MenuItem.prototype = {
this.actor.append(this._icon, Big.BoxPackFlags.NONE);
this._text = new Clutter.Text({ color: GenericDisplay.ITEM_DISPLAY_NAME_COLOR,
font_name: "Sans 14px",
ellipsize: Pango.EllipsizeMode.END,
text: name });
// We use individual boxes for the label and the arrow to ensure that they
@ -164,18 +162,16 @@ Signals.addSignalMethods(MenuItem.prototype);
/* This class represents a display containing a collection of application items.
* The applications are sorted based on their popularity by default, and based on
* their name if some search filter is applied.
*
* width - width available for the display
*/
function AppDisplay(width) {
this._init(width);
function AppDisplay() {
this._init();
}
AppDisplay.prototype = {
__proto__: GenericDisplay.GenericDisplay.prototype,
_init : function(width) {
GenericDisplay.GenericDisplay.prototype._init.call(this, width);
_init : function() {
GenericDisplay.GenericDisplay.prototype._init.call(this);
this._menus = [];
this._menuDisplays = [];
@ -433,8 +429,8 @@ AppDisplay.prototype = {
},
// Creates an AppDisplayItem based on itemInfo, which is expected be an Shell.AppInfo object.
_createDisplayItem: function(itemInfo, width) {
return new AppDisplayItem(itemInfo, width);
_createDisplayItem: function(itemInfo) {
return new AppDisplayItem(itemInfo);
}
};
@ -488,8 +484,6 @@ WellDisplayItem.prototype = {
x_align: Big.BoxAlignment.CENTER });
this._nameBox = nameBox;
this._wordWidth = Shell.Global.get().get_max_word_width(this.actor, appInfo.get_name(),
"Sans 12px");
this._name = new Clutter.Text({ color: GenericDisplay.ITEM_DISPLAY_NAME_COLOR,
font_name: "Sans 12px",
line_alignment: Pango.Alignment.CENTER,

505
js/ui/dash.js Normal file
View File

@ -0,0 +1,505 @@
/* -*- mode: js2; js2-basic-offset: 4; indent-tabs-mode: nil -*- */
const Big = imports.gi.Big;
const Clutter = imports.gi.Clutter;
const Gio = imports.gi.Gio;
const Gtk = imports.gi.Gtk;
const Mainloop = imports.mainloop;
const Shell = imports.gi.Shell;
const Signals = imports.signals;
const Lang = imports.lang;
const AppDisplay = imports.ui.appDisplay;
const DocDisplay = imports.ui.docDisplay;
const GenericDisplay = imports.ui.genericDisplay;
const Button = imports.ui.button;
const Main = imports.ui.main;
const DEFAULT_PADDING = 4;
const DASH_SECTION_PADDING = 6;
const DASH_SECTION_SPACING = 12;
const DASH_CORNER_RADIUS = 5;
const DASH_SEARCH_BG_COLOR = new Clutter.Color();
DASH_SEARCH_BG_COLOR.from_pixel(0xffffffff);
const DASH_SECTION_COLOR = new Clutter.Color();
DASH_SECTION_COLOR.from_pixel(0x846c3dff);
const DASH_TEXT_COLOR = new Clutter.Color();
DASH_TEXT_COLOR.from_pixel(0xffffffff);
const PANE_BORDER_COLOR = new Clutter.Color();
PANE_BORDER_COLOR.from_pixel(0x213b5dfa);
const PANE_BORDER_WIDTH = 2;
const PANE_BACKGROUND_COLOR = new Clutter.Color();
PANE_BACKGROUND_COLOR.from_pixel(0x0d131ff4);
function Pane() {
this._init();
}
Pane.prototype = {
_init: function () {
this._open = false;
this.actor = new Big.Box({ orientation: Big.BoxOrientation.VERTICAL,
background_color: PANE_BACKGROUND_COLOR,
border: PANE_BORDER_WIDTH,
border_color: PANE_BORDER_COLOR,
padding: DEFAULT_PADDING,
reactive: true });
this.actor.connect('button-press-event', Lang.bind(this, function (a, e) {
// Eat button press events so they don't go through and close the pane
return true;
}));
let chromeTop = new Big.Box({ orientation: Big.BoxOrientation.HORIZONTAL,
spacing: 6 });
let global = Shell.Global.get();
let closeIconUri = "file://" + global.imagedir + "close.svg";
let closeIcon = Shell.TextureCache.get_default().load_uri_sync(Shell.TextureCachePolicy.FOREVER,
closeIconUri,
16,
16);
closeIcon.reactive = true;
closeIcon.connect('button-press-event', Lang.bind(this, function (b, e) {
this.close();
return true;
}));
chromeTop.append(closeIcon, Big.BoxPackFlags.END);
this.actor.append(chromeTop, Big.BoxPackFlags.NONE);
this.content = new Big.Box({ orientation: Big.BoxOrientation.VERTICAL,
spacing: DEFAULT_PADDING });
this.actor.append(this.content, Big.BoxPackFlags.EXPAND);
// Hidden by default
this.actor.hide();
},
open: function () {
if (this._open)
return;
this._open = true;
this.actor.show();
this.emit('open-state-changed', this._open);
},
close: function () {
if (!this._open)
return;
this._open = false;
this.actor.hide();
this.emit('open-state-changed', this._open);
},
destroyContent: function() {
let children = this.content.get_children();
for (let i = 0; i < children.length; i++) {
children[i].destroy();
}
},
toggle: function () {
if (this._open)
this.close();
else
this.open();
}
}
Signals.addSignalMethods(Pane.prototype);
function ResultArea(displayClass, enableNavigation) {
this._init(displayClass, enableNavigation);
}
ResultArea.prototype = {
_init : function(displayClass, enableNavigation) {
this.actor = new Big.Box({ orientation: Big.BoxOrientation.VERTICAL });
this.resultsContainer = new Big.Box({ orientation: Big.BoxOrientation.HORIZONTAL,
spacing: DEFAULT_PADDING
});
this.actor.append(this.resultsContainer, Big.BoxPackFlags.EXPAND);
this.navContainer = new Big.Box({ orientation: Big.BoxOrientation.VERTICAL });
this.resultsContainer.append(this.navContainer, Big.BoxPackFlags.NONE);
this.display = new displayClass();
this.navArea = this.display.getNavigationArea();
if (enableNavigation && this.navArea)
this.navContainer.append(this.navArea, Big.BoxPackFlags.EXPAND);
this.resultsContainer.append(this.display.actor, Big.BoxPackFlags.EXPAND);
this.controlBox = new Big.Box({ x_align: Big.BoxAlignment.CENTER });
this.controlBox.append(this.display.displayControl, Big.BoxPackFlags.NONE);
this.actor.append(this.controlBox, Big.BoxPackFlags.EXPAND);
this.display.load();
}
}
// Utility function shared between ResultPane and the DocDisplay in the main dash.
// Connects to the detail signal of the display, and on-demand creates a new
// pane.
function createPaneForDetails(dash, display, detailsWidth) {
let detailPane = null;
display.connect('show-details', Lang.bind(this, function(display, index) {
if (detailPane == null) {
detailPane = new Pane();
detailPane.connect('open-state-changed', Lang.bind(this, function (pane, isOpen) {
if (!isOpen) {
/* Ensure we don't keep around large preview textures */
detailPane.destroyContent();
}
}));
dash._addPane(detailPane);
}
if (index >= 0) {
detailPane.destroyContent();
let details = display.createDetailsForIndex(index, detailsWidth, -1);
detailPane.content.append(details, Big.BoxPackFlags.EXPAND);
detailPane.open();
} else {
detailPane.close();
}
}));
return null;
}
function ResultPane(dash, detailsWidth) {
this._init(dash, detailsWidth);
}
ResultPane.prototype = {
__proto__: Pane.prototype,
_init: function(dash, detailsWidth) {
Pane.prototype._init.call(this);
this._dash = dash;
this._detailsWidth = detailsWidth;
},
// Create an instance of displayClass and pack it into this pane's
// content area. Return the displayClass instance.
packResults: function(displayClass, enableNavigation) {
let resultArea = new ResultArea(displayClass, enableNavigation);
createPaneForDetails(this._dash, resultArea.display, this._detailsWidth);
this.content.append(resultArea.actor, Big.BoxPackFlags.EXPAND);
this.connect('open-state-changed', Lang.bind(this, function(pane, isOpen) {
resultArea.display.resetState();
}));
return resultArea.display;
}
}
function SearchEntry() {
this._init();
}
SearchEntry.prototype = {
_init : function() {
this.actor = new Big.Box({ orientation: Big.BoxOrientation.HORIZONTAL,
y_align: Big.BoxAlignment.CENTER,
background_color: DASH_SEARCH_BG_COLOR,
corner_radius: 4,
spacing: DEFAULT_PADDING,
padding: DEFAULT_PADDING
});
let icon = new Gio.ThemedIcon({ name: 'gtk-find' });
let searchIconTexture = Shell.TextureCache.get_default().load_gicon(icon, 16);
this.actor.append(searchIconTexture, Big.BoxPackFlags.NONE);
this.pane = null;
// We need to initialize the text for the entry to have the cursor displayed
// in it. See http://bugzilla.openedhand.com/show_bug.cgi?id=1365
this.entry = new Clutter.Text({ font_name: "Sans 14px",
editable: true,
activatable: true,
singleLineMode: true,
text: ""
});
this.actor.append(this.entry, Big.BoxPackFlags.EXPAND);
},
setPane: function (pane) {
this._pane = pane;
}
};
Signals.addSignalMethods(SearchEntry.prototype);
function MoreLink() {
this._init();
}
MoreLink.prototype = {
_init : function () {
this.actor = new Big.Box({ orientation: Big.BoxOrientation.HORIZONTAL,
padding_left: DEFAULT_PADDING,
padding_right: DEFAULT_PADDING });
let global = Shell.Global.get();
let inactiveUri = "file://" + global.imagedir + "view-more.svg";
let activeUri = "file://" + global.imagedir + "view-more-activated.svg";
this._inactiveIcon = Shell.TextureCache.get_default().load_uri_sync(Shell.TextureCachePolicy.FOREVER,
inactiveUri, 29, 18);
this._activeIcon = Shell.TextureCache.get_default().load_uri_sync(Shell.TextureCachePolicy.FOREVER,
activeUri, 29, 18);
this._iconBox = new Big.Box({ reactive: true });
this._iconBox.append(this._inactiveIcon, Big.BoxPackFlags.NONE);
this.actor.append(this._iconBox, Big.BoxPackFlags.END);
this.pane = null;
this._iconBox.connect('button-press-event', Lang.bind(this, function (b, e) {
if (this.pane == null) {
// Ensure the pane is created; the activated handler will call setPane
this.emit('activated');
}
this._pane.toggle();
return true;
}));
},
setPane: function (pane) {
this._pane = pane;
this._pane.connect('open-state-changed', Lang.bind(this, function(pane, isOpen) {
this._iconBox.remove_all();
this._iconBox.append(isOpen ? this._activeIcon : this._inactiveIcon,
Big.BoxPackFlags.NONE);
}));
}
}
Signals.addSignalMethods(MoreLink.prototype);
function SectionHeader(title) {
this._init(title);
}
SectionHeader.prototype = {
_init : function (title) {
this.actor = new Big.Box({ orientation: Big.BoxOrientation.HORIZONTAL });
let text = new Clutter.Text({ color: DASH_SECTION_COLOR,
font_name: "Sans Bold 10px",
text: title });
this.moreLink = new MoreLink();
this.actor.append(text, Big.BoxPackFlags.EXPAND);
this.actor.append(this.moreLink.actor, Big.BoxPackFlags.END);
}
}
function Dash(displayGridColumnWidth) {
this._init(displayGridColumnWidth);
}
Dash.prototype = {
_init : function(displayGridColumnWidth) {
this._width = displayGridColumnWidth;
this._detailsWidth = displayGridColumnWidth * 2;
let global = Shell.Global.get();
// dash and the popup panes need to be reactive so that the clicks in unoccupied places on them
// are not passed to the transparent background underneath them. This background is used for the workspaces area when
// the additional dash panes are being shown and it handles clicks by closing the additional panes, so that the user
// can interact with the workspaces. However, this behavior is not desirable when the click is actually over a pane.
//
// We have to make the individual panes reactive instead of making the whole dash actor reactive because the width
// of the Group actor ends up including the width of its hidden children, so we were getting a reactive object as
// wide as the details pane that was blocking the clicks to the workspaces underneath it even when the details pane
// was actually hidden.
this.actor = new Big.Box({ orientation: Big.BoxOrientation.HORIZONTAL,
width: this._width,
padding: DEFAULT_PADDING,
reactive: true });
this.dashContainer = new Big.Box({ orientation: Big.BoxOrientation.VERTICAL,
spacing: DASH_SECTION_SPACING });
this.actor.append(this.dashContainer, Big.BoxPackFlags.EXPAND);
// The currently active popup display
this._activePane = null;
/***** Search *****/
this._searchPane = null;
this._searchActive = false;
this._searchEntry = new SearchEntry();
this.dashContainer.append(this._searchEntry.actor, Big.BoxPackFlags.NONE);
this._searchAreaApps = null;
this._searchAreaDocs = null;
this._searchQueued = false;
this._searchEntry.entry.connect('text-changed', Lang.bind(this, function (se, prop) {
this._searchActive = this._searchEntry.text != '';
if (this._searchQueued)
return;
if (this._searchPane == null) {
this._searchPane = new ResultPane(this, this._detailsWidth);
this._searchPane.content.append(new Clutter.Text({ color: DASH_SECTION_COLOR,
font_name: 'Sans Bold 10px',
text: "APPLICATIONS" }),
Big.BoxPackFlags.NONE);
this._searchAreaApps = this._searchPane.packResults(AppDisplay.AppDisplay, false);
this._searchPane.content.append(new Clutter.Text({ color: DASH_SECTION_COLOR,
font_name: 'Sans Bold 10px',
text: "RECENT DOCUMENTS" }),
Big.BoxPackFlags.NONE);
this._searchAreaDocs = this._searchPane.packResults(DocDisplay.DocDisplay, false);
this._addPane(this._searchPane);
this._searchEntry.setPane(this._searchPane);
}
this._searchQueued = true;
Mainloop.timeout_add(250, Lang.bind(this, function() {
// Strip leading and trailing whitespace
let text = this._searchEntry.entry.text.replace(/^\s+/g, "").replace(/\s+$/g, "");
this._searchQueued = false;
this._searchAreaApps.setSearch(text);
this._searchAreaDocs.setSearch(text);
if (text == '')
this._searchPane.close();
else
this._searchPane.open();
return false;
}));
}));
this._searchEntry.entry.connect('activate', Lang.bind(this, function (se) {
// only one of the displays will have an item selected, so it's ok to
// call activateSelected() on all of them
this._searchAreaApps.activateSelected();
this._searchAreaDocs.activateSelected();
return true;
}));
this._searchEntry.entry.connect('key-press-event', Lang.bind(this, function (se, e) {
let symbol = Shell.get_event_key_symbol(e);
if (symbol == Clutter.Escape) {
// Escape will keep clearing things back to the desktop. First, if
// we have active text, we remove it.
if (this._searchEntry.entry.text != '')
this._searchEntry.entry.text = '';
// Next, if we're in one of the "more" modes or showing the details pane, close them
else if (this._activePane != null)
this._activePane.close();
// Finally, just close the overlay entirely
else
Main.overlay.hide();
return true;
} else if (symbol == Clutter.Up) {
if (!this._searchActive)
return true;
// selectUp and selectDown wrap around in their respective displays
// too, but there doesn't seem to be any flickering if we first select
// something in one display, but then unset the selection, and move
// it to the other display, so it's ok to do that.
if (this._searchAreaDocs.hasSelected())
this._searchAreaDocs.selectUp();
else if (this._searchAreaApps.hasItems())
this._searchAreaApps.selectUp();
else
this._searchAreaDocs.selectUp();
return true;
} else if (symbol == Clutter.Down) {
if (!this._searchActive)
return true;
if (this._searchAreaDocs.hasSelected())
this._searchAreaDocs.selectDown();
else if (this._searchAreaApps.hasItems())
this._searchAreaApps.selectDown();
else
this._searchAreaDocs.selectDown();
return true;
}
return false;
}));
/***** Applications *****/
let appsHeader = new SectionHeader("APPLICATIONS");
this._appsSection = new Big.Box({ spacing: DEFAULT_PADDING });
this._appsSection.append(appsHeader.actor, Big.BoxPackFlags.NONE);
this._appsContent = new Big.Box({ orientation: Big.BoxOrientation.HORIZONTAL });
this._appsSection.append(this._appsContent, Big.BoxPackFlags.EXPAND);
this._appWell = new AppDisplay.AppWell(this._width);
this._appsContent.append(this._appWell.actor, Big.BoxPackFlags.EXPAND);
this._moreAppsPane = null;
appsHeader.moreLink.connect('activated', Lang.bind(this, function (link) {
if (this._moreAppsPane == null) {
this._moreAppsPane = new ResultPane(this, this._detailsWidth);
this._moreAppsPane.packResults(AppDisplay.AppDisplay, true);
this._addPane(this._moreAppsPane);
link.setPane(this._moreAppsPane);
}
}));
this.dashContainer.append(this._appsSection, Big.BoxPackFlags.NONE);
/***** Documents *****/
this._docsSection = new Big.Box({ orientation: Big.BoxOrientation.VERTICAL,
spacing: DEFAULT_PADDING });
this._moreDocsPane = null;
let docsHeader = new SectionHeader("RECENT DOCUMENTS");
this._docsSection.append(docsHeader.actor, Big.BoxPackFlags.NONE);
this._docDisplay = new DocDisplay.DocDisplay();
this._docDisplay.load();
this._docsSection.append(this._docDisplay.actor, Big.BoxPackFlags.EXPAND);
createPaneForDetails(this, this._docDisplay, this._detailsWidth);
docsHeader.moreLink.connect('activated', Lang.bind(this, function (link) {
if (this._moreDocsPane == null) {
this._moreDocsPane = new ResultPane(this, this._detailsWidth);
this._moreDocsPane.packResults(DocDisplay.DocDisplay, true);
this._addPane(this._moreDocsPane);
link.setPane(this._moreDocsPane);
}
}));
this.dashContainer.append(this._docsSection, Big.BoxPackFlags.EXPAND);
},
show: function() {
let global = Shell.Global.get();
global.stage.set_key_focus(this._searchEntry.entry);
},
hide: function() {
this._firstSelectAfterOverlayShow = true;
if (this._searchEntry.entry.text != '')
this._searchEntry.entry.text = '';
if (this._activePane != null)
this._activePane.close();
},
closePanes: function () {
if (this._activePane != null)
this._activePane.close();
},
_addPane: function(pane) {
pane.connect('open-state-changed', Lang.bind(this, function (pane, isOpen) {
if (isOpen) {
if (pane != this._activePane && this._activePane != null) {
this._activePane.close();
}
this._activePane = pane;
} else if (pane == this._activePane) {
this._activePane = null;
}
}));
Main.overlay.addPane(pane);
}
};
Signals.addSignalMethods(Dash.prototype);

View File

@ -18,17 +18,16 @@ const Main = imports.ui.main;
*
* docInfo - DocInfo object containing information about the document
* currentSeconds - current number of seconds since the epoch
* availableWidth - total width available for the item
*/
function DocDisplayItem(docInfo, currentSecs, availableWidth) {
this._init(docInfo, currentSecs, availableWidth);
function DocDisplayItem(docInfo, currentSecs) {
this._init(docInfo, currentSecs);
}
DocDisplayItem.prototype = {
__proto__: GenericDisplay.GenericDisplayItem.prototype,
_init : function(docInfo, currentSecs, availableWidth) {
GenericDisplay.GenericDisplayItem.prototype._init.call(this, availableWidth);
_init : function(docInfo, currentSecs) {
GenericDisplay.GenericDisplayItem.prototype._init.call(this);
this._docInfo = docInfo;
this._setItemInfo(docInfo.name, "");
@ -91,18 +90,16 @@ DocDisplayItem.prototype = {
/* This class represents a display containing a collection of document items.
* The documents are sorted by how recently they were last visited.
*
* width - width available for the display
*/
function DocDisplay(width) {
this._init(width);
function DocDisplay() {
this._init();
}
DocDisplay.prototype = {
__proto__: GenericDisplay.GenericDisplay.prototype,
_init : function(width) {
GenericDisplay.GenericDisplay.prototype._init.call(this, width);
_init : function() {
GenericDisplay.GenericDisplay.prototype._init.call(this);
let me = this;
// We keep a single timeout callback for updating last visited times
@ -203,9 +200,9 @@ DocDisplay.prototype = {
},
// Creates a DocDisplayItem based on itemInfo, which is expected to be a DocInfo object.
_createDisplayItem: function(itemInfo, width) {
_createDisplayItem: function(itemInfo) {
let currentSecs = new Date().getTime() / 1000;
let docDisplayItem = new DocDisplayItem(itemInfo, currentSecs, width);
let docDisplayItem = new DocDisplayItem(itemInfo, currentSecs);
this._updateTimeoutCallback(docDisplayItem, currentSecs);
return docDisplayItem;
},

View File

@ -15,6 +15,7 @@ const Tidy = imports.gi.Tidy;
const Button = imports.ui.button;
const DND = imports.ui.dnd;
const Link = imports.ui.link;
const Main = imports.ui.main;
const ITEM_DISPLAY_NAME_COLOR = new Clutter.Color();
ITEM_DISPLAY_NAME_COLOR.from_pixel(0xffffffff);
@ -28,19 +29,18 @@ const DISPLAY_CONTROL_SELECTED_COLOR = new Clutter.Color();
DISPLAY_CONTROL_SELECTED_COLOR.from_pixel(0x112288ff);
const PREVIEW_BOX_BACKGROUND_COLOR = new Clutter.Color();
PREVIEW_BOX_BACKGROUND_COLOR.from_pixel(0xADADADf0);
const HOT_PINK_DEBUG = new Clutter.Color();
HOT_PINK_DEBUG.from_pixel(0xFF8888FF);
const DEFAULT_PADDING = 4;
const ITEM_DISPLAY_HEIGHT = 50;
const ITEM_DISPLAY_ICON_SIZE = 48;
const ITEM_DISPLAY_PADDING_TOP = 1;
const ITEM_DISPLAY_PADDING = 1;
const ITEM_DISPLAY_PADDING_RIGHT = 2;
const DEFAULT_COLUMN_GAP = 6;
const LABEL_HEIGHT = 16;
const PREVIEW_ICON_SIZE = 96;
const PREVIEW_BOX_PADDING = 6;
const PREVIEW_BOX_SPACING = 4;
const PREVIEW_BOX_SPACING = DEFAULT_PADDING;
const PREVIEW_BOX_CORNER_RADIUS = 10;
// how far relative to the full item width the preview box should be placed
const PREVIEW_PLACING = 3/4;
@ -51,20 +51,20 @@ const INFORMATION_BUTTON_SIZE = 16;
/* This is a virtual class that represents a single display item containing
* a name, a description, and an icon. It allows selecting an item and represents
* it by highlighting it with a different background color than the default.
*
* availableWidth - total width available for the item
*/
function GenericDisplayItem(availableWidth) {
this._init(availableWidth);
function GenericDisplayItem() {
this._init();
}
GenericDisplayItem.prototype = {
_init: function(availableWidth) {
this._availableWidth = availableWidth;
this.actor = new Clutter.Group({ reactive: true,
width: availableWidth,
_init: function() {
this.actor = new Big.Box({ orientation: Big.BoxOrientation.HORIZONTAL,
spacing: ITEM_DISPLAY_PADDING,
reactive: true,
background_color: ITEM_DISPLAY_BACKGROUND_COLOR,
corner_radius: 4,
height: ITEM_DISPLAY_HEIGHT });
this.actor._delegate = this;
this.actor.connect('button-release-event',
Lang.bind(this,
@ -77,11 +77,16 @@ GenericDisplayItem.prototype = {
let draggable = DND.makeDraggable(this.actor);
draggable.connect('drag-begin', Lang.bind(this, this._onDragBegin));
this._bg = new Big.Box({ background_color: ITEM_DISPLAY_BACKGROUND_COLOR,
corner_radius: 4,
x: 0, y: 0,
width: availableWidth, height: ITEM_DISPLAY_HEIGHT });
this.actor.add_actor(this._bg);
this._infoContent = new Big.Box({ orientation: Big.BoxOrientation.HORIZONTAL,
spacing: DEFAULT_PADDING });
this.actor.append(this._infoContent, Big.BoxPackFlags.EXPAND);
this._iconBox = new Big.Box();
this._infoContent.append(this._iconBox, Big.BoxPackFlags.NONE);
this._infoText = new Big.Box({ orientation: Big.BoxOrientation.VERTICAL,
spacing: DEFAULT_PADDING });
this._infoContent.append(this._infoText, Big.BoxPackFlags.EXPAND);
let global = Shell.Global.get();
let infoIconUri = "file://" + global.imagedir + "info.svg";
@ -90,8 +95,12 @@ GenericDisplayItem.prototype = {
INFORMATION_BUTTON_SIZE,
INFORMATION_BUTTON_SIZE);
this._informationButton = new Button.iconButton(this.actor, INFORMATION_BUTTON_SIZE, infoIcon);
this._informationButton.actor.x = availableWidth - ITEM_DISPLAY_PADDING_RIGHT - INFORMATION_BUTTON_SIZE;
this._informationButton.actor.y = ITEM_DISPLAY_HEIGHT / 2 - INFORMATION_BUTTON_SIZE / 2;
let buttonBox = new Big.Box({ width: INFORMATION_BUTTON_SIZE + 2 * DEFAULT_PADDING,
height: INFORMATION_BUTTON_SIZE,
padding_left: DEFAULT_PADDING, padding_right: DEFAULT_PADDING,
y_align: Big.BoxAlignment.CENTER });
buttonBox.append(this._informationButton.actor, Big.BoxPackFlags.NONE);
this.actor.append(buttonBox, Big.BoxPackFlags.END);
// Connecting to the button-press-event for the information button ensures that the actor,
// which is a draggable actor, does not get the button-press-event and doesn't initiate
@ -105,11 +114,9 @@ GenericDisplayItem.prototype = {
Lang.bind(this,
function() {
// Selects the item by highlighting it and displaying its details
this.emit('select');
this.emit('show-details');
return true;
}));
this.actor.add_actor(this._informationButton.actor);
this._informationButton.actor.lower_bottom();
this._name = null;
this._description = null;
@ -170,7 +177,7 @@ GenericDisplayItem.prototype = {
color = ITEM_DISPLAY_BACKGROUND_COLOR;
this._informationButton.forceShow(false);
}
this._bg.background_color = color;
this.actor.background_color = color;
},
/*
@ -178,9 +185,8 @@ GenericDisplayItem.prototype = {
* the preview pop-up has and be item-type specific.
*
* availableWidth - width available for displaying details
* availableHeight - height available for displaying details
*/
createDetailsActor: function(availableWidth, availableHeight) {
createDetailsActor: function(availableWidth) {
let details = new Big.Box({ orientation: Big.BoxOrientation.VERTICAL,
spacing: PREVIEW_BOX_SPACING,
@ -196,7 +202,7 @@ GenericDisplayItem.prototype = {
let detailsName = new Clutter.Text({ color: ITEM_DISPLAY_NAME_COLOR,
font_name: "Sans bold 14px",
line_wrap: true,
text: this._name.text});
text: this._name.text });
textDetails.append(detailsName, Big.BoxPackFlags.NONE);
let detailsDescription = new Clutter.Text({ color: ITEM_DISPLAY_NAME_COLOR,
@ -210,7 +216,7 @@ GenericDisplayItem.prototype = {
mainDetails.append(textDetails, Big.BoxPackFlags.EXPAND);
let previewIcon = this._createPreviewIcon();
let largePreviewIcon = this._createLargePreviewIcon(availableWidth, Math.max(0, availableHeight - mainDetails.height - PREVIEW_BOX_SPACING));
let largePreviewIcon = this._createLargePreviewIcon(availableWidth, -1);
if (previewIcon != null && largePreviewIcon == null) {
mainDetails.prepend(previewIcon, Big.BoxPackFlags.NONE);
@ -266,25 +272,20 @@ GenericDisplayItem.prototype = {
}
this._icon = this._createIcon();
this.actor.add_actor(this._icon);
this._iconBox.append(this._icon, Big.BoxPackFlags.NONE);
let textWidth = this._availableWidth - (ITEM_DISPLAY_ICON_SIZE + 4) - INFORMATION_BUTTON_SIZE - ITEM_DISPLAY_PADDING_RIGHT;
this._name = new Clutter.Text({ color: ITEM_DISPLAY_NAME_COLOR,
font_name: "Sans 14px",
width: textWidth,
ellipsize: Pango.EllipsizeMode.END,
text: nameText,
x: ITEM_DISPLAY_ICON_SIZE + 4,
y: ITEM_DISPLAY_PADDING_TOP });
this.actor.add_actor(this._name);
text: nameText });
this._infoText.append(this._name, Big.BoxPackFlags.EXPAND);
this._description = new Clutter.Text({ color: ITEM_DISPLAY_DESCRIPTION_COLOR,
font_name: "Sans 12px",
width: textWidth,
ellipsize: Pango.EllipsizeMode.END,
text: descriptionText ? descriptionText : "",
x: this._name.x,
y: this._name.height + 4 });
this.actor.add_actor(this._description);
text: descriptionText ? descriptionText : ""
});
this._infoText.append(this._description, Big.BoxPackFlags.EXPAND);
},
// Sets the description text for the item, including the description text
@ -332,22 +333,18 @@ Signals.addSignalMethods(GenericDisplayItem.prototype);
/* This is a virtual class that represents a display containing a collection of items
* that can be filtered with a search string.
*
* width - width available for the display
*/
function GenericDisplay(width) {
this._init(width);
function GenericDisplay() {
this._init();
}
GenericDisplay.prototype = {
_init : function(width) {
_init : function() {
this._search = '';
this._expanded = false;
this._width = width;
this._maxItemsPerPage = null;
this._list = new Shell.OverflowList({ width: this._width,
spacing: 6.0,
this._list = new Shell.OverflowList({ spacing: 6.0,
item_height: ITEM_DISPLAY_HEIGHT });
this._list.connect('notify::n-pages', Lang.bind(this, function (grid, alloc) {
@ -364,6 +361,7 @@ GenericDisplay.prototype = {
this._matchedItems = [];
// map<itemId, GenericDisplayItem>
this._displayedItems = {};
this._openDetailIndex = -1;
this._selectedIndex = -1;
// These two are public - .actor is the normal "actor subclass" property,
// but we also expose a .displayControl actor which is separate.
@ -372,24 +370,10 @@ GenericDisplay.prototype = {
this.displayControl = new Big.Box({ background_color: ITEM_DISPLAY_BACKGROUND_COLOR,
spacing: 12,
orientation: Big.BoxOrientation.HORIZONTAL});
this._availableWidthForItemDetails = width;
this.selectedItemDetails = new Big.Box({});
},
//// Public methods ////
// Sets dimensions available for the item details display.
setAvailableDimensionsForItemDetails: function(availableWidth, availableHeight) {
this._availableWidthForItemDetails = availableWidth;
this._availableHeightForItemDetails = availableHeight;
},
// Returns dimensions available for the item details display.
getAvailableDimensionsForItemDetails: function() {
return [this._availableWidthForItemDetails, this._availableHeightForItemDetails];
},
// Sets the search string and displays the matching items.
setSearch: function(text) {
this._search = text.toLowerCase();
@ -400,8 +384,9 @@ GenericDisplay.prototype = {
activateSelected: function() {
if (this._selectedIndex != -1) {
let selected = this._findDisplayedByIndex(this._selectedIndex);
selected.launch()
this.emit('activated');
selected.launch();
this.unsetSelected();
Main.overlay.hide();
}
},
@ -463,17 +448,15 @@ GenericDisplay.prototype = {
return this._list.displayedCount > 0;
},
// Updates the displayed items and makes the display actor visible.
show: function() {
this._list.show();
// Load the initial state
load: function() {
this._redisplay(true);
},
// Hides the display actor.
hide: function() {
this._list.hide();
// Should be called when the display is closed
resetState: function() {
this._filterReset();
this._removeAllDisplayItems();
this._openDetailIndex = -1;
},
// Returns an actor which acts as a sidebar; this is used for
@ -482,6 +465,11 @@ GenericDisplay.prototype = {
return null;
},
createDetailsForIndex: function(index, width, height) {
let item = this._findDisplayedByIndex(index);
return item.createDetailsActor(width, height);
},
//// Protected methods ////
/*
@ -538,7 +526,7 @@ GenericDisplay.prototype = {
}
let itemInfo = this._allItems[itemId];
let displayItem = this._createDisplayItem(itemInfo, this._width);
let displayItem = this._createDisplayItem(itemInfo);
displayItem.connect('activate',
Lang.bind(this,
@ -548,11 +536,18 @@ GenericDisplay.prototype = {
this.activateSelected();
}));
displayItem.connect('select',
displayItem.connect('show-details',
Lang.bind(this,
function() {
// update the selection
this._selectIndex(this._getIndexOfDisplayedActor(displayItem.actor));
let index = this._getIndexOfDisplayedActor(displayItem.actor);
/* Close the details pane if already open */
if (index == this._openDetailIndex) {
this._openDetailIndex = -1;
this.emit('show-details', -1);
} else {
this._openDetailIndex = index;
this.emit('show-details', index);
}
}));
this._list.add_actor(displayItem.actor);
this._displayedItems[itemId] = displayItem;
@ -564,7 +559,7 @@ GenericDisplay.prototype = {
let displayItem = this._displayedItems[itemId];
let displayItemIndex = this._getIndexOfDisplayedActor(displayItem.actor);
if (this.hasSelected() && (count == 1 || !this._list.visible)) {
if (this.hasSelected() && count == 1) {
this.unsetSelected();
} else if (this.hasSelected() && displayItemIndex < this._selectedIndex) {
this.selectUp();
@ -603,9 +598,6 @@ GenericDisplay.prototype = {
* their own while the user was browsing through the result pages.
*/
_redisplay: function(resetPage) {
if (!this._list.visible)
return;
this._refreshCache();
if (!this._filterActive())
this._setDefaultList();
@ -729,7 +721,6 @@ GenericDisplay.prototype = {
let pageControl = new Link.Link({ color: (i == pageNumber) ? DISPLAY_CONTROL_SELECTED_COLOR : ITEM_DISPLAY_DESCRIPTION_COLOR,
font_name: "Sans Bold 16px",
text: (i+1) + "",
height: LABEL_HEIGHT,
reactive: (i == pageNumber) ? false : true});
this.displayControl.append(pageControl.actor, Big.BoxPackFlags.NONE);
@ -794,22 +785,13 @@ GenericDisplay.prototype = {
// If the item is already selected, all we do is toggling the details pane.
if (this._selectedIndex == index && index >= 0) {
this.emit('toggle-details');
this.emit('details', index);
return;
}
// Cleanup from the previous item
if (this._selectedIndex >= 0) {
this._findDisplayedByIndex(this._selectedIndex).markSelected(false);
// Calling destroy() gets large image previews released as quickly as
// possible, if we just removed them, they might hang around for a while
// until the actor was garbage collected.
let children = this.selectedItemDetails.get_children();
for (let i = 0; i < children.length; i++)
children[i].destroy();
this.selectedItemDetails.remove_all();
}
this._selectedIndex = index;
@ -819,8 +801,6 @@ GenericDisplay.prototype = {
// Mark the new item as selected and create its details pane
let item = this._findDisplayedByIndex(index);
item.markSelected(true);
this.selectedItemDetails.append(item.createDetailsActor(this._availableWidthForItemDetails,
this._availableHeightForItemDetails), Big.BoxPackFlags.NONE);
this.emit('selected');
}
};

View File

@ -15,6 +15,7 @@ const GenericDisplay = imports.ui.genericDisplay;
const Link = imports.ui.link;
const Main = imports.ui.main;
const Panel = imports.ui.panel;
const Dash = imports.ui.dash;
const Tweener = imports.ui.tweener;
const Workspaces = imports.ui.workspaces;
@ -25,19 +26,6 @@ ROOT_OVERLAY_COLOR.from_pixel(0x000000bb);
// than 3/2, because the rule of thirds is used for positioning (see below).
const BACKGROUND_SCALE = 2;
const LABEL_HEIGHT = 16;
const DASH_MIN_WIDTH = 250;
const DASH_OUTER_PADDING = 4;
const DASH_SECTION_PADDING = 6;
const DASH_SECTION_SPACING = 6;
const DASH_CORNER_RADIUS = 5;
// This is the height of section components other than the item display.
const DASH_SECTION_MISC_HEIGHT = (LABEL_HEIGHT + DASH_SECTION_SPACING) * 2 + DASH_SECTION_PADDING;
const DASH_SEARCH_BG_COLOR = new Clutter.Color();
DASH_SEARCH_BG_COLOR.from_pixel(0xffffffff);
const DASH_TEXT_COLOR = new Clutter.Color();
DASH_TEXT_COLOR.from_pixel(0xffffffff);
// Time for initial animation going into overlay mode
const ANIMATION_TIME = 0.25;
@ -61,6 +49,8 @@ const ROWS_REGULAR_SCREEN = 8;
const COLUMNS_WIDE_SCREEN = 5;
const ROWS_WIDE_SCREEN = 10;
const DEFAULT_PADDING = 4;
// Padding around workspace grid / Spacing between Dash and Workspaces
const WORKSPACE_GRID_PADDING = 12;
@ -75,27 +65,6 @@ const STATE_ACTIVE = true;
const STATE_PENDING_INACTIVE = false;
const STATE_INACTIVE = false;
// The dash has a slightly transparent blue background with a gradient.
const DASH_LEFT_COLOR = new Clutter.Color();
DASH_LEFT_COLOR.from_pixel(0x0d131fbb);
const DASH_MIDDLE_COLOR = new Clutter.Color();
DASH_MIDDLE_COLOR.from_pixel(0x0d131faa);
const DASH_RIGHT_COLOR = new Clutter.Color();
DASH_RIGHT_COLOR.from_pixel(0x0d131fcc);
const DASH_BORDER_COLOR = new Clutter.Color();
DASH_BORDER_COLOR.from_pixel(0x213b5dfa);
const DASH_BORDER_WIDTH = 2;
// The results and details panes have a somewhat transparent blue background with a gradient.
const PANE_LEFT_COLOR = new Clutter.Color();
PANE_LEFT_COLOR.from_pixel(0x0d131ff4);
const PANE_MIDDLE_COLOR = new Clutter.Color();
PANE_MIDDLE_COLOR.from_pixel(0x0d131ffa);
const PANE_RIGHT_COLOR = new Clutter.Color();
PANE_RIGHT_COLOR.from_pixel(0x0d131ff4);
const SHADOW_COLOR = new Clutter.Color();
SHADOW_COLOR.from_pixel(0x00000033);
const TRANSPARENT_COLOR = new Clutter.Color();
@ -109,616 +78,6 @@ let wideScreen = false;
let displayGridColumnWidth = null;
let displayGridRowHeight = null;
function SearchEntry(width) {
this._init(width);
}
SearchEntry.prototype = {
_init : function() {
this.actor = new Big.Box({ orientation: Big.BoxOrientation.HORIZONTAL,
y_align: Big.BoxAlignment.CENTER,
background_color: DASH_SEARCH_BG_COLOR,
corner_radius: 4,
spacing: 4,
padding_left: 4,
padding_right: 4,
height: 24
});
let icontheme = Gtk.IconTheme.get_default();
let searchIconTexture = new Clutter.Texture({});
let searchIconPath = icontheme.lookup_icon('gtk-find', 16, 0).get_filename();
searchIconTexture.set_from_file(searchIconPath);
this.actor.append(searchIconTexture, 0);
// We need to initialize the text for the entry to have the cursor displayed
// in it. See http://bugzilla.openedhand.com/show_bug.cgi?id=1365
this.entry = new Clutter.Text({ font_name: "Sans 14px",
editable: true,
activatable: true,
singleLineMode: true,
text: ""
});
this.entry.connect('text-changed', Lang.bind(this, function (e) {
let text = this.entry.text;
}));
this.actor.append(this.entry, Big.BoxPackFlags.EXPAND);
}
};
function ItemResults(resultsWidth, resultsHeight, displayClass, labelText) {
this._init(resultsWidth, resultsHeight, displayClass, labelText);
}
ItemResults.prototype = {
_init: function(resultsWidth, resultsHeight, displayClass, labelText) {
this._resultsWidth = resultsWidth;
this._resultsHeight = resultsHeight;
this.actor = new Big.Box({ orientation: Big.BoxOrientation.VERTICAL,
height: resultsHeight,
padding: DASH_SECTION_PADDING + DASH_BORDER_WIDTH,
spacing: DASH_SECTION_SPACING });
this._resultsText = new Clutter.Text({ color: DASH_TEXT_COLOR,
font_name: "Sans Bold 14px",
text: labelText });
this.resultsContainer = new Big.Box({ orientation: Big.BoxOrientation.HORIZONTAL,
spacing: 4 });
this.navContainer = new Big.Box({ orientation: Big.BoxOrientation.VERTICAL });
this.resultsContainer.append(this.navContainer, Big.BoxPackFlags.NONE);
// LABEL_HEIGHT is the height of this._resultsText and GenericDisplay.LABEL_HEIGHT is the height
// of the display controls.
this._displayHeight = resultsHeight - LABEL_HEIGHT - GenericDisplay.LABEL_HEIGHT - DASH_SECTION_SPACING * 2;
this.display = new displayClass(resultsWidth);
this.navArea = this.display.getNavigationArea();
if (this.navArea)
this.navContainer.append(this.navArea, Big.BoxPackFlags.EXPAND);
this.resultsContainer.append(this.display.actor, Big.BoxPackFlags.EXPAND);
this.controlBox = new Big.Box({ x_align: Big.BoxAlignment.CENTER });
this.controlBox.append(this.display.displayControl, Big.BoxPackFlags.NONE);
this._unsetSearchMode();
},
_setSearchMode: function() {
if (this.navArea)
this.navArea.hide();
this.actor.height = this._resultsHeight / NUMBER_OF_SECTIONS_IN_SEARCH;
let displayHeight = this._displayHeight - this._resultsHeight * (NUMBER_OF_SECTIONS_IN_SEARCH - 1) / NUMBER_OF_SECTIONS_IN_SEARCH;
this.actor.remove_all();
this.actor.append(this._resultsText, Big.BoxPackFlags.NONE);
this.actor.append(this.resultsContainer, Big.BoxPackFlags.EXPAND);
this.actor.append(this.controlBox, Big.BoxPackFlags.END);
},
_unsetSearchMode: function() {
if (this.navArea)
this.navArea.show();
this.actor.height = this._resultsHeight;
this.actor.remove_all();
this.actor.append(this._resultsText, Big.BoxPackFlags.NONE);
this.actor.append(this.resultsContainer, Big.BoxPackFlags.EXPAND);
this.actor.append(this.controlBox, Big.BoxPackFlags.END);
}
}
function Dash() {
this._init();
}
Dash.prototype = {
_init : function() {
let me = this;
this._moreAppsMode = false;
this._moreDocsMode = false;
this._width = displayGridColumnWidth;
this._displayWidth = displayGridColumnWidth - DASH_SECTION_PADDING * 2;
this._resultsWidth = displayGridColumnWidth;
this._detailsWidth = displayGridColumnWidth * 2;
let global = Shell.Global.get();
let dashHeight = global.screen_height - Panel.PANEL_HEIGHT;
let resultsHeight = global.screen_height - Panel.PANEL_HEIGHT;
let detailsHeight = global.screen_height - Panel.PANEL_HEIGHT;
// Size the actor to 0x0 so as not to interfere with DND
this.actor = new Clutter.Group({ width: 0, height: 0 });
this.actor.height = global.screen_height;
// dashPane, as well as results and details panes need to be reactive so that the clicks in unoccupied places on them
// are not passed to the transparent background underneath them. This background is used for the workspaces area when
// the additional dash panes are being shown and it handles clicks by closing the additional panes, so that the user
// can interact with the workspaces. However, this behavior is not desirable when the click is actually over a pane.
//
// We have to make the individual panes reactive instead of making the whole dash actor reactive because the width
// of the Group actor ends up including the width of its hidden children, so we were getting a reactive object as
// wide as the details pane that was blocking the clicks to the workspaces underneath it even when the details pane
// was actually hidden.
let dashPane = new Big.Box({ orientation: Big.BoxOrientation.HORIZONTAL,
x: 0,
y: Panel.PANEL_HEIGHT,
width: this._width + SHADOW_WIDTH,
height: dashHeight,
reactive: true});
let dashBackground = new Big.Box({ orientation: Big.BoxOrientation.HORIZONTAL,
width: this._width,
height: dashHeight });
dashPane.append(dashBackground, Big.BoxPackFlags.EXPAND);
let dashLeft = Shell.create_horizontal_gradient(DASH_LEFT_COLOR,
DASH_MIDDLE_COLOR);
let dashRight = Shell.create_horizontal_gradient(DASH_MIDDLE_COLOR,
DASH_RIGHT_COLOR);
let dashShadow = Shell.create_horizontal_gradient(SHADOW_COLOR,
TRANSPARENT_COLOR);
dashShadow.set_width(SHADOW_WIDTH);
dashBackground.append(dashLeft, Big.BoxPackFlags.EXPAND);
dashBackground.append(dashRight, Big.BoxPackFlags.EXPAND);
dashPane.append(dashShadow, Big.BoxPackFlags.NONE);
this.actor.add_actor(dashPane);
this.dashOuterContainer = new Big.Box({ orientation: Big.BoxOrientation.VERTICAL,
x: 0,
y: dashPane.y,
width: this._width,
height: dashHeight,
padding: DASH_OUTER_PADDING
});
this.actor.add_actor(this.dashOuterContainer);
this.dashContainer = new Big.Box({ orientation: Big.BoxOrientation.VERTICAL });
this.dashOuterContainer.append(this.dashContainer, Big.BoxPackFlags.EXPAND);
this._searchEntry = new SearchEntry();
this.dashContainer.append(this._searchEntry.actor, Big.BoxPackFlags.NONE);
this._searchQueued = false;
this._searchEntry.entry.connect('text-changed', function (se, prop) {
if (me._searchQueued)
return;
me._searchQueued = true;
Mainloop.timeout_add(250, function() {
// Strip leading and trailing whitespace
let text = me._searchEntry.entry.text.replace(/^\s+/g, "").replace(/\s+$/g, "");
me._searchQueued = false;
me._resultsAppsSection.display.setSearch(text);
me._resultsDocsSection.display.setSearch(text);
if (text == '')
me._unsetSearchMode();
else
me._setSearchMode();
return false;
});
});
this._searchEntry.entry.connect('activate', function (se) {
// only one of the displays will have an item selected, so it's ok to
// call activateSelected() on all of them
me._docDisplay.activateSelected();
me._resultsAppsSection.display.activateSelected();
me._resultsDocsSection.display.activateSelected();
return true;
});
this._searchEntry.entry.connect('key-press-event', function (se, e) {
let symbol = Shell.get_event_key_symbol(e);
if (symbol == Clutter.Escape) {
// Escape will keep clearing things back to the desktop. First, if
// we have active text, we remove it.
if (me._searchEntry.entry.text != '')
me._searchEntry.entry.text = '';
// Next, if we're in one of the "more" modes or showing the details pane, close them
else if (me._resultsShowing())
me.unsetMoreMode();
// Finally, just close the overlay entirely
else
me.emit('activated');
return true;
} else if (symbol == Clutter.Up) {
if (!me._resultsShowing())
return true;
// selectUp and selectDown wrap around in their respective displays
// too, but there doesn't seem to be any flickering if we first select
// something in one display, but then unset the selection, and move
// it to the other display, so it's ok to do that.
if (me._resultsDocsSection.display.hasSelected())
me._resultsDocsSection.display.selectUp();
else if (me._resultsAppsSection.display.hasItems())
me._resultsAppsSection.display.selectUp();
else
me._resultsDocsSection.display.selectUp();
return true;
} else if (symbol == Clutter.Down) {
if (!me._resultsShowing())
return true;
if (me._resultsDocsSection.display.hasSelected())
me._resultsDocsSection.display.selectDown();
else if (me._resultsAppsSection.display.hasItems())
me._resultsAppsSection.display.selectDown();
else
me._resultsDocsSection.display.selectDown();
return true;
}
return false;
});
this._appsText = new Clutter.Text({ color: DASH_TEXT_COLOR,
font_name: "Sans Bold 14px",
text: "Applications",
height: LABEL_HEIGHT});
this._appsSection = new Big.Box({ padding_top: DASH_SECTION_PADDING,
spacing: DASH_SECTION_SPACING});
this._appsSection.append(this._appsText, Big.BoxPackFlags.NONE);
this._itemDisplayHeight = global.screen_height - this._appsSection.y - DASH_SECTION_MISC_HEIGHT * 2;
this._appsContent = new Big.Box({ orientation: Big.BoxOrientation.HORIZONTAL });
this._appsSection.append(this._appsContent, Big.BoxPackFlags.EXPAND);
this._appWell = new AppDisplay.AppWell(this._displayWidth);
this._appsContent.append(this._appWell.actor, Big.BoxPackFlags.EXPAND);
let moreAppsBox = new Big.Box({x_align: Big.BoxAlignment.END});
this._moreAppsLink = new Link.Link({ color: DASH_TEXT_COLOR,
font_name: "Sans Bold 14px",
text: "More...",
height: LABEL_HEIGHT });
moreAppsBox.append(this._moreAppsLink.actor, Big.BoxPackFlags.EXPAND);
this._appsSection.append(moreAppsBox, Big.BoxPackFlags.NONE);
this.dashContainer.append(this._appsSection, Big.BoxPackFlags.NONE);
this._appsSectionDefaultHeight = this._appsSection.height;
this._docsSection = new Big.Box({ padding_top: DASH_SECTION_PADDING,
padding_bottom: DASH_SECTION_PADDING,
spacing: DASH_SECTION_SPACING});
this._docsText = new Clutter.Text({ color: DASH_TEXT_COLOR,
font_name: "Sans Bold 14px",
text: "Recent Documents",
height: LABEL_HEIGHT});
this._docsSection.append(this._docsText, Big.BoxPackFlags.NONE);
this._docDisplay = new DocDisplay.DocDisplay(this._displayWidth);
this._docsSection.append(this._docDisplay.actor, Big.BoxPackFlags.EXPAND);
let moreDocsBox = new Big.Box({x_align: Big.BoxAlignment.END});
this._moreDocsLink = new Link.Link({ color: DASH_TEXT_COLOR,
font_name: "Sans Bold 14px",
text: "More...",
height: LABEL_HEIGHT });
moreDocsBox.append(this._moreDocsLink.actor, Big.BoxPackFlags.EXPAND);
this._docsSection.append(moreDocsBox, Big.BoxPackFlags.NONE);
this.dashContainer.append(this._docsSection, Big.BoxPackFlags.EXPAND);
this._docsSectionDefaultHeight = this._docsSection.height;
// The "More" or search results area
this._resultsAppsSection = new ItemResults(this._resultsWidth, resultsHeight, AppDisplay.AppDisplay, "Applications");
this._resultsDocsSection = new ItemResults(this._resultsWidth, resultsHeight, DocDisplay.DocDisplay, "Documents");
this._resultsPane = new Big.Box({ orientation: Big.BoxOrientation.VERTICAL,
x: this._width,
y: Panel.PANEL_HEIGHT,
height: resultsHeight,
reactive: true });
let resultsBackground = new Big.Box({ orientation: Big.BoxOrientation.HORIZONTAL,
height: resultsHeight,
corner_radius: DASH_CORNER_RADIUS,
border: DASH_BORDER_WIDTH,
border_color: DASH_BORDER_COLOR });
this._resultsPane.add_actor(resultsBackground);
let resultsLeft = Shell.create_horizontal_gradient(PANE_LEFT_COLOR,
PANE_MIDDLE_COLOR);
let resultsRight = Shell.create_horizontal_gradient(PANE_MIDDLE_COLOR,
PANE_RIGHT_COLOR);
let resultsShadow = Shell.create_horizontal_gradient(SHADOW_COLOR,
TRANSPARENT_COLOR);
resultsShadow.set_width(SHADOW_WIDTH);
resultsBackground.append(resultsLeft, Big.BoxPackFlags.EXPAND);
resultsBackground.append(resultsRight, Big.BoxPackFlags.EXPAND);
this._resultsPane.add_actor(resultsShadow);
this._resultsPane.connect('notify::allocation', Lang.bind(this, function (b, a) {
let width = this._resultsPane.width;
resultsBackground.width = width;
resultsShadow.width = width;
}));
this.actor.add_actor(this._resultsPane);
this._resultsPane.hide();
this._detailsPane = new Big.Box({ orientation: Big.BoxOrientation.HORIZONTAL,
y: Panel.PANEL_HEIGHT,
width: this._detailsWidth + SHADOW_WIDTH,
height: detailsHeight,
reactive: true });
this._firstSelectAfterOverlayShow = true;
let detailsBackground = new Big.Box({ orientation: Big.BoxOrientation.HORIZONTAL,
width: this._detailsWidth,
height: detailsHeight,
corner_radius: DASH_CORNER_RADIUS,
border: DASH_BORDER_WIDTH,
border_color: DASH_BORDER_COLOR });
this._detailsPane.append(detailsBackground, Big.BoxPackFlags.EXPAND);
let detailsLeft = Shell.create_horizontal_gradient(PANE_LEFT_COLOR,
PANE_MIDDLE_COLOR);
let detailsRight = Shell.create_horizontal_gradient(PANE_MIDDLE_COLOR,
PANE_RIGHT_COLOR);
let detailsShadow = Shell.create_horizontal_gradient(SHADOW_COLOR,
TRANSPARENT_COLOR);
detailsShadow.set_width(SHADOW_WIDTH);
detailsBackground.append(detailsLeft, Big.BoxPackFlags.EXPAND);
detailsBackground.append(detailsRight, Big.BoxPackFlags.EXPAND);
this._detailsPane.append(detailsShadow, Big.BoxPackFlags.NONE);
this._detailsContent = new Big.Box({ padding: DASH_SECTION_PADDING + DASH_BORDER_WIDTH });
this._detailsPane.add_actor(this._detailsContent);
this.actor.add_actor(this._detailsPane);
this._detailsPane.hide();
let itemDetailsAvailableWidth = this._detailsWidth - DASH_SECTION_PADDING * 2 - DASH_BORDER_WIDTH * 2;
let itemDetailsAvailableHeight = detailsHeight - DASH_SECTION_PADDING * 2 - DASH_BORDER_WIDTH * 2;
this._docDisplay.setAvailableDimensionsForItemDetails(itemDetailsAvailableWidth, itemDetailsAvailableHeight);
this._resultsAppsSection.display.setAvailableDimensionsForItemDetails(itemDetailsAvailableWidth, itemDetailsAvailableHeight);
this._resultsDocsSection.display.setAvailableDimensionsForItemDetails(itemDetailsAvailableWidth, itemDetailsAvailableHeight);
/* Proxy the activated signals */
this._appWell.connect('activated', function(well) {
me.emit('activated');
});
this._docDisplay.connect('activated', function(docDisplay) {
me.emit('activated');
});
this._resultsAppsSection.display.connect('activated', function(resultsAppsDisplay) {
me.emit('activated');
});
this._resultsDocsSection.display.connect('activated', function(resultsDocsDisplay) {
me.emit('activated');
});
this._docDisplay.connect('selected', Lang.bind(this, function(docDisplay) {
this._resultsDocsSection.display.unsetSelected();
this._resultsAppsSection.display.unsetSelected();
this._showDetails();
this._detailsContent.remove_all();
this._detailsContent.append(this._docDisplay.selectedItemDetails, Big.BoxPackFlags.NONE);
}));
this._docDisplay.connect('toggle-details', Lang.bind(this, function(docDisplay) {
this._toggleDetails();
}));
this._resultsDocsSection.display.connect('selected', Lang.bind(this, function(resultsDocDisplay) {
this._docDisplay.unsetSelected();
this._resultsAppsSection.display.unsetSelected();
this._showDetails();
this._detailsContent.remove_all();
this._detailsContent.append(this._resultsDocsSection.display.selectedItemDetails, Big.BoxPackFlags.NONE);
}));
this._resultsDocsSection.display.connect('toggle-details', Lang.bind(this, function(resultsDocDisplay) {
this._toggleDetails();
}));
this._resultsAppsSection.display.connect('selected', Lang.bind(this, function(resultsAppDisplay) {
this._docDisplay.unsetSelected();
this._resultsDocsSection.display.unsetSelected();
this._showDetails();
this._detailsContent.remove_all();
this._detailsContent.append(this._resultsAppsSection.display.selectedItemDetails, Big.BoxPackFlags.NONE);
}));
this._moreAppsLink.connect('clicked',
function(o, event) {
if (me._moreAppsMode) {
me._unsetMoreAppsMode();
} else {
me._setMoreAppsMode();
}
});
this._moreDocsLink.connect('clicked',
function(o, event) {
if (me._moreDocsMode) {
me._unsetMoreDocsMode();
} else {
me._setMoreDocsMode();
}
});
},
show: function() {
let global = Shell.Global.get();
this._appsContent.show();
this._docDisplay.show();
global.stage.set_key_focus(this._searchEntry.entry);
},
hide: function() {
this._firstSelectAfterOverlayShow = true;
this._appsContent.hide();
this._docDisplay.hide();
this._searchEntry.entry.text = '';
this.unsetMoreMode();
},
unsetMoreMode: function() {
this._unsetMoreAppsMode();
this._unsetMoreDocsMode();
if (this._detailsShowing()) {
this._detailsPane.hide();
this.emit('panes-removed');
}
this._unsetSearchMode();
},
// Sets the 'More' mode for browsing applications.
_setMoreAppsMode: function() {
if (this._moreAppsMode)
return;
this._unsetMoreDocsMode();
this._unsetSearchMode();
this._moreAppsMode = true;
this._resultsAppsSection.display.show();
this._resultsPane.append(this._resultsAppsSection.actor, Big.BoxPackFlags.EXPAND);
this._resultsPane.show();
this._repositionDetails();
this._moreAppsLink.setText("Less...");
this.emit('panes-displayed');
},
// Unsets the 'More' mode for browsing applications.
_unsetMoreAppsMode: function() {
if (!this._moreAppsMode)
return;
this._moreAppsMode = false;
this._resultsPane.remove_actor(this._resultsAppsSection.actor);
this._resultsAppsSection.display.hide();
this._resultsPane.hide();
this._moreAppsLink.setText("More...");
this._hideDetails();
},
// Sets the 'More' mode for browsing documents.
_setMoreDocsMode: function() {
if (this._moreDocsMode)
return;
this._unsetMoreAppsMode();
this._unsetSearchMode();
this._moreDocsMode = true;
this._resultsDocsSection.display.show();
this._resultsPane.append(this._resultsDocsSection.actor, Big.BoxPackFlags.EXPAND);
this._resultsPane.show();
this._repositionDetails();
this._moreDocsLink.setText("Less...");
this.emit('panes-displayed');
},
// Unsets the 'More' mode for browsing documents.
_unsetMoreDocsMode: function() {
if (!this._moreDocsMode)
return;
this._moreDocsMode = false;
this._resultsPane.hide();
this._resultsPane.remove_actor(this._resultsDocsSection.actor);
this._resultsDocsSection.display.hide();
this._moreDocsLink.setText("More...");
this._hideDetails();
},
_setSearchMode: function() {
if (this._resultsShowing())
return;
this._resultsAppsSection._setSearchMode();
this._resultsAppsSection.display.show();
this._resultsPane.append(this._resultsAppsSection.actor, Big.BoxPackFlags.EXPAND);
this._resultsDocsSection._setSearchMode();
this._resultsDocsSection.display.show();
this._resultsPane.append(this._resultsDocsSection.actor, Big.BoxPackFlags.EXPAND);
this._resultsPane.show();
this._repositionDetails();
this.emit('panes-displayed');
},
_unsetSearchMode: function() {
if (this._moreDocsMode || this._moreAppsMode || !this._resultsShowing())
return;
this._resultsPane.hide();
this._repositionDetails();
this._resultsPane.remove_actor(this._resultsAppsSection.actor);
this._resultsAppsSection.display.hide();
this._resultsAppsSection._unsetSearchMode();
this._resultsPane.remove_actor(this._resultsDocsSection.actor);
this._resultsDocsSection.display.hide();
this._resultsDocsSection._unsetSearchMode();
this._resultsDocsSection.actor.set_y(0);
this._hideDetails();
},
_repositionDetails: function () {
let x;
if (this._resultsPane.visible)
x = this._resultsPane.x + this._resultsPane.width;
else
x = this._width;
this._detailsPane.x = x;
},
_showDetails: function () {
this._detailsPane.show();
this._repositionDetails();
this.emit('panes-displayed');
},
_hideDetails: function() {
if (!this._detailsShowing)
return;
this._detailsPane.hide();
this.emit('panes-removed');
},
_toggleDetails: function() {
if (this._detailsShowing())
this._hideDetails();
else
this._showDetails();
},
_detailsShowing: function() {
return this._detailsPane.visible;
},
_resultsShowing: function() {
return this._resultsPane.visible;
}
};
Signals.addSignalMethods(Dash.prototype);
function Overlay() {
this._init();
}
@ -729,6 +88,64 @@ Overlay.prototype = {
let global = Shell.Global.get();
this._group = new Clutter.Group();
this._group._delegate = this;
this.visible = false;
this._hideInProgress = false;
this._recalculateGridSizes();
// A scaled root pixmap actor is used as a background. It is zoomed in
// to the lower right intersection of the lines that divide the image
// evenly in a 3x3 grid. This is based on the rule of thirds, a
// compositional rule of thumb in visual arts. The choice for the
// lower right point is based on a quick survey of GNOME wallpapers.
this._background = global.create_root_pixmap_actor();
this._group.add_actor(this._background);
this._activeDisplayPane = null;
// Used to catch any clicks when we have an active pane; see the comments
// in addPane below.
this._transparentBackground = new Clutter.Rectangle({ opacity: 0,
reactive: true });
this._group.add_actor(this._transparentBackground);
// Draw a semitransparent rectangle over the background for readability.
this._backOver = new Clutter.Rectangle({ color: ROOT_OVERLAY_COLOR });
this._group.add_actor(this._backOver);
this._group.hide();
global.overlay_group.add_actor(this._group);
// TODO - recalculate everything when desktop size changes
this._dash = new Dash.Dash(displayGridColumnWidth);
this._group.add_actor(this._dash.actor);
// Container to hold popup pane chrome.
this._paneContainer = new Big.Box({ orientation: Big.BoxOrientation.HORIZONTAL,
spacing: 6
});
// Note here we explicitly don't set the paneContainer to be reactive yet; that's done
// inside the notify::visible handler on panes.
this._paneContainer.connect('button-release-event', Lang.bind(this, function(background) {
this._activeDisplayPane.close();
return true;
}));
this._group.add_actor(this._paneContainer);
this._transparentBackground.lower_bottom();
this._paneContainer.lower_bottom();
this._repositionChildren();
this._workspaces = null;
},
_recalculateGridSizes: function () {
let global = Shell.Global.get();
wideScreen = (global.screen_width/global.screen_height > WIDE_SCREEN_CUT_OFF_RATIO);
// We divide the screen into an imaginary grid which helps us determine the layout of
@ -740,84 +157,76 @@ Overlay.prototype = {
displayGridColumnWidth = global.screen_width / COLUMNS_REGULAR_SCREEN;
displayGridRowHeight = global.screen_height / ROWS_REGULAR_SCREEN;
}
},
this._group = new Clutter.Group();
this._group._delegate = this;
_repositionChildren: function () {
let global = Shell.Global.get();
this.visible = false;
this._hideInProgress = false;
let contentHeight = global.screen_height - Panel.PANEL_HEIGHT;
// A scaled root pixmap actor is used as a background. It is zoomed in
// to the lower right intersection of the lines that divide the image
// evenly in a 3x3 grid. This is based on the rule of thirds, a
// compositional rule of thumb in visual arts. The choice for the
// lower right point is based on a quick survey of GNOME wallpapers.
let background = global.create_root_pixmap_actor();
background.width = global.screen_width * BACKGROUND_SCALE;
background.height = global.screen_height * BACKGROUND_SCALE;
background.x = -global.screen_width * (4 * BACKGROUND_SCALE - 3) / 6;
background.y = -global.screen_height * (4 * BACKGROUND_SCALE - 3) / 6;
this._group.add_actor(background);
this._dash.actor.set_position(0, Panel.PANEL_HEIGHT);
this._dash.actor.set_size(displayGridColumnWidth, contentHeight);
// Transparent background is used to catch clicks outside of the dash panes when the panes
// are being displayed and the workspaces area should not be reactive. Catching such a
// click results in the panes being closed and the workspaces area becoming reactive again.
this._transparentBackground = new Clutter.Rectangle({ opacity: 0,
width: global.screen_width,
height: global.screen_height - Panel.PANEL_HEIGHT,
y: Panel.PANEL_HEIGHT,
reactive: true });
this._group.add_actor(this._transparentBackground);
this._backOver.set_position(0, Panel.PANEL_HEIGHT);
this._backOver.set_size(global.screen_width, contentHeight);
// Draw a semitransparent rectangle over the background for readability.
let backOver = new Clutter.Rectangle({ color: ROOT_OVERLAY_COLOR,
width: global.screen_width,
height: global.screen_height - Panel.PANEL_HEIGHT,
y: Panel.PANEL_HEIGHT });
this._group.add_actor(backOver);
let bgPositionFactor = (4 * BACKGROUND_SCALE - 3) / 6;
this._background.set_size(global.screen_width * BACKGROUND_SCALE,
global.screen_height * BACKGROUND_SCALE);
this._background.set_position(-global.screen_width * bgPositionFactor,
-global.screen_height * bgPositionFactor);
this._group.hide();
global.overlay_group.add_actor(this._group);
this._paneContainer.set_position(this._dash.actor.x + this._dash.actor.width + DEFAULT_PADDING,
Panel.PANEL_HEIGHT);
// Dynamic width
this._paneContainer.height = contentHeight;
// TODO - recalculate everything when desktop size changes
this._dash = new Dash();
this._group.add_actor(this._dash.actor);
this._workspaces = null;
this._buttonEventHandlerId = null;
this._dash.connect('activated', function(dash) {
// TODO - have some sort of animation/effect while
// transitioning to the new app. We definitely need
// startup-notification integration at least.
me.hide();
});
this._dash.connect('panes-displayed', function(dash) {
if (me._buttonEventHandlerId == null) {
me._transparentBackground.raise_top();
me._dash.actor.raise_top();
me._buttonEventHandlerId = me._transparentBackground.connect('button-release-event', function(background) {
me._dash.unsetMoreMode();
this._transparentBackground.set_position(this._paneContainer.x, this._paneContainer.y);
this._transparentBackground.set_size(global.screen_width - this._paneContainer.x,
this._paneContainer.height);
},
addPane: function (pane) {
pane.actor.width = displayGridColumnWidth * 2;
this._paneContainer.append(pane.actor, Big.BoxPackFlags.NONE);
// When a pane is displayed, we raise the transparent background to the top
// and connect to button-release-event on it, then raise the pane above that.
// The idea here is that clicking anywhere outside the pane should close it.
// When the active pane is closed, undo the effect.
let backgroundEventId = null;
pane.connect('open-state-changed', Lang.bind(this, function (pane, isOpen) {
if (isOpen) {
this._activeDisplayPane = pane;
this._transparentBackground.raise_top();
this._paneContainer.raise_top();
if (backgroundEventId != null)
this._transparentBackground.disconnect(backgroundEventId);
backgroundEventId = this._transparentBackground.connect('button-release-event', Lang.bind(this, function () {
this._activeDisplayPane.close();
return true;
});
}));
} else if (pane == this._activeDisplayPane) {
this._activeDisplayPane = null;
if (backgroundEventId != null) {
this._transparentBackground.disconnect(backgroundEventId);
backgroundEventId = null;
}
});
this._dash.connect('panes-removed', function(dash) {
if (me._buttonEventHandlerId != null) {
me._transparentBackground.lower_bottom();
me._transparentBackground.disconnect(me._buttonEventHandlerId);
me._buttonEventHandlerId = null;
this._transparentBackground.lower_bottom();
this._paneContainer.lower_bottom();
}
});
}));
},
//// Draggable target interface ////
// Unsets the expanded display mode if a GenericDisplayItem is being
// Closes any active panes if a GenericDisplayItem is being
// dragged over the overlay, i.e. as soon as it starts being dragged.
// This closes the additional panes and allows the user to place
// the item on any workspace.
// This allows the user to place the item on any workspace.
handleDragOver : function(source, actor, x, y, time) {
if (source instanceof GenericDisplay.GenericDisplayItem) {
this._dash.unsetMoreMode();
if (source instanceof GenericDisplay.GenericDisplayItem
|| source instanceof AppDisplay.WellDisplayItem) {
if (this._activeDisplayPane != null)
this._activeDisplayPane.close();
return true;
}
@ -901,7 +310,9 @@ Overlay.prototype = {
let global = Shell.Global.get();
this._hideInProgress = true;
// lower the Dash, so that workspaces display is on top and covers the Dash while it is sliding out
if (this._activeDisplayPane != null)
this._activeDisplayPane.close();
// lower the panes, so that workspaces display is on top while sliding out
this._dash.actor.lower(this._workspaces.actor);
this._workspaces.hide();