diff --git a/js/ui/appDisplay.js b/js/ui/appDisplay.js index 9a4c2a7c0..e5437bd65 100644 --- a/js/ui/appDisplay.js +++ b/js/ui/appDisplay.js @@ -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, diff --git a/js/ui/dash.js b/js/ui/dash.js new file mode 100644 index 000000000..c09307c17 --- /dev/null +++ b/js/ui/dash.js @@ -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); diff --git a/js/ui/docDisplay.js b/js/ui/docDisplay.js index fac319fb5..fa9ad3c1e 100644 --- a/js/ui/docDisplay.js +++ b/js/ui/docDisplay.js @@ -18,19 +18,18 @@ 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, ""); this._timeoutTime = -1; @@ -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; }, diff --git a/js/ui/genericDisplay.js b/js/ui/genericDisplay.js index 3cec3491f..2f4a9e41b 100644 --- a/js/ui/genericDisplay.js +++ b/js/ui/genericDisplay.js @@ -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,20 +29,19 @@ 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_CORNER_RADIUS = 10; +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; const PREVIEW_DETAILS_MIN_WIDTH = PREVIEW_ICON_SIZE * 2; @@ -49,25 +49,25 @@ const PREVIEW_DETAILS_MIN_WIDTH = PREVIEW_ICON_SIZE * 2; 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 + * 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; + _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 = new Clutter.Group({ reactive: true, - width: availableWidth, - height: ITEM_DISPLAY_HEIGHT }); this.actor._delegate = this; - this.actor.connect('button-release-event', - Lang.bind(this, + this.actor.connect('button-release-event', + Lang.bind(this, function() { // Activates the item by launching it this.emit('activate'); @@ -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,10 +95,14 @@ 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, + // 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 // the dragging, which then prevents us from getting the button-release-event for the button. this._informationButton.actor.connect('button-press-event', @@ -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,18 +177,17 @@ GenericDisplayItem.prototype = { color = ITEM_DISPLAY_BACKGROUND_COLOR; this._informationButton.forceShow(false); } - this._bg.background_color = color; + this.actor.background_color = color; }, /* - * Returns an actor containing item details. In the future details can have more information than what + * Returns an actor containing item details. In the future details can have more information than what * 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, width: availableWidth }); @@ -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,29 +272,24 @@ 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 - // in the details actors that have been created for the item. + // in the details actors that have been created for the item. _setDescriptionText: function(text) { this._description.text = text; for (let i = 0; i < this._detailsDescriptions.length; i++) { @@ -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 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,11 +559,11 @@ 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(); - } + } displayItem.destroy(); @@ -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'); } }; diff --git a/js/ui/overlay.js b/js/ui/overlay.js index 235b0ee0f..894d3c6ba 100644 --- a/js/ui/overlay.js +++ b/js/ui/overlay.js @@ -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,10 +88,68 @@ 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 - // different visual components. + // different visual components. if (wideScreen) { displayGridColumnWidth = global.screen_width / COLUMNS_WIDE_SCREEN; displayGridRowHeight = global.screen_height / ROWS_WIDE_SCREEN; @@ -740,87 +157,79 @@ 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; - }); - } - }); - this._dash.connect('panes-removed', function(dash) { - if (me._buttonEventHandlerId != null) { - me._transparentBackground.lower_bottom(); - me._transparentBackground.disconnect(me._buttonEventHandlerId); - me._buttonEventHandlerId = null; + })); + } else if (pane == this._activeDisplayPane) { + this._activeDisplayPane = null; + if (backgroundEventId != null) { + this._transparentBackground.disconnect(backgroundEventId); + backgroundEventId = 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; } - + return false; }, @@ -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();