diff --git a/configure.ac b/configure.ac index 410e9930c..87278026f 100644 --- a/configure.ac +++ b/configure.ac @@ -38,7 +38,7 @@ fi AM_CONDITIONAL(BUILD_RECORDER, $build_recorder) -PKG_CHECK_MODULES(MUTTER_PLUGIN, gtk+-2.0 dbus-glib-1 metacity-plugins gjs-gi-1.0 $recorder_modules) +PKG_CHECK_MODULES(MUTTER_PLUGIN, gtk+-2.0 dbus-glib-1 metacity-plugins gjs-gi-1.0 libgnome-menu $recorder_modules) PKG_CHECK_MODULES(TIDY, clutter-0.9) PKG_CHECK_MODULES(BIG, clutter-0.9 gtk+-2.0 librsvg-2.0) PKG_CHECK_MODULES(GDMUSER, dbus-glib-1 gtk+-2.0) diff --git a/js/ui/appDisplay.js b/js/ui/appDisplay.js index 45bd9bfe3..ab2dd089c 100644 --- a/js/ui/appDisplay.js +++ b/js/ui/appDisplay.js @@ -1,13 +1,23 @@ /* -*- mode: js2; js2-basic-offset: 4; indent-tabs-mode: nil -*- */ +const Big = imports.gi.Big; const Clutter = imports.gi.Clutter; +const Pango = imports.gi.Pango; const Gio = imports.gi.Gio; const Gtk = imports.gi.Gtk; const Gdk = imports.gi.Gdk; const Shell = imports.gi.Shell; +const Lang = imports.lang; const Signals = imports.signals; const GenericDisplay = imports.ui.genericDisplay; +const GtkUtil = imports.ui.gtkutil; + +const ENTERED_MENU_COLOR = new Clutter.Color(); +ENTERED_MENU_COLOR.from_pixel(0x00ff0022); + +const MENU_ICON_SIZE = 24; +const MENU_SPACING = 15; // TODO - move this into GConf once we're not a plugin anymore // but have taken over metacity @@ -55,29 +65,11 @@ AppDisplayItem.prototype = { let description = appInfo.get_description(); - let icon = new Clutter.Texture({ width: GenericDisplay.ITEM_DISPLAY_ICON_SIZE, height: GenericDisplay.ITEM_DISPLAY_ICON_SIZE}); + let iconTheme = Gtk.IconTheme.get_default(); - this._gicon = appInfo.get_icon(); - let iconPath = null; - if (this._gicon != null) { - let iconTheme = Gtk.IconTheme.get_default(); - let iconInfo = iconTheme.lookup_by_gicon(this._gicon, GenericDisplay.ITEM_DISPLAY_ICON_SIZE, Gtk.IconLookupFlags.NO_SVG); - if (iconInfo) - iconPath = iconInfo.get_filename(); - } - - if (iconPath) { - try { - icon.set_from_file(iconPath); - icon.x = GenericDisplay.ITEM_DISPLAY_PADDING; - icon.y = GenericDisplay.ITEM_DISPLAY_PADDING; - } catch (e) { - // we can get an error here if the file path doesn't exist on the system - log('Error loading AppDisplayItem icon ' + e); - } - } - - this._setItemInfo(name, description, icon); + let gicon = appInfo.get_icon(); + let icon = GtkUtil.loadIconToTexture(gicon, GenericDisplay.ITEM_DISPLAY_ICON_SIZE, true); + this._setItemInfo(name, description, icon); }, //// Public methods //// @@ -130,6 +122,86 @@ AppDisplayItem.prototype = { } }; +const MENU_UNSELECTED = 0; +const MENU_SELECTED = 1; +const MENU_ENTERED = 2; + +function MenuItem(name, id, icon_name) { + this._init(name, id, icon_name); +} + +/** + * MenuItem: + * Shows the list of menus in the sidebar. + */ +MenuItem.prototype = { + _init: function(name, id, icon_name) { + this.id = id; + + this.actor = new Big.Box({ orientation: Big.BoxOrientation.HORIZONTAL, + spacing: 4, + corner_radius: 4, + padding_right: 4, + reactive: true }); + this.actor.connect('button-press-event', Lang.bind(this, function (a, e) { + this.setState(MENU_SELECTED); + })); + + let iconTheme = Gtk.IconTheme.get_default(); + let pixbuf = null; + this._icon = new Clutter.Texture({ width: MENU_ICON_SIZE, + height: MENU_ICON_SIZE }); + // Wine manages not to have an icon + try { + pixbuf = iconTheme.load_icon(icon_name, MENU_ICON_SIZE, 0 /* flags */); + } catch (e) { + pixbuf = iconTheme.load_icon('gtk-file', MENU_ICON_SIZE, 0); + } + if (pixbuf != null) + Shell.clutter_texture_set_from_pixbuf(this._icon, pixbuf); + this.actor.append(this._icon, 0); + this._text = new Clutter.Text({ color: GenericDisplay.ITEM_DISPLAY_NAME_COLOR, + font_name: "Sans 14px", + ellipsize: Pango.EllipsizeMode.END, + text: name }); + this.actor.append(this._text, Big.BoxPackFlags.EXPAND); + + let box = new Big.Box({ orientation: Big.BoxOrientation.HORIZONTAL, + y_align: Big.BoxAlignment.CENTER + }); + + this._arrow = new Shell.Arrow({ surface_width: MENU_ICON_SIZE/2, + surface_height: MENU_ICON_SIZE/2, + direction: Gtk.ArrowType.RIGHT, + opacity: 0 + }); + box.append(this._arrow, 0); + this.actor.append(box, 0); + }, + + getState: function() { + return this._state; + }, + + setState: function (state) { + if (state == this._state) + return; + this._state = state; + if (this._state == MENU_UNSELECTED) { + this.actor.background_color = null; + this._arrow.set_opacity(0); + } else if (this._state == MENU_ENTERED) { + this.actor.background_color = ENTERED_MENU_COLOR; + this._arrow.set_opacity(0xFF/2); + } else { + this.actor.background_color = GenericDisplay.ITEM_DISPLAY_SELECTED_BACKGROUND_COLOR; + this._arrow.set_opacity(0xFF); + } + this.emit('state-changed') + } +} +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. @@ -147,8 +219,11 @@ AppDisplay.prototype = { _init : function(width, height, numberOfColumns, columnGap) { GenericDisplay.GenericDisplay.prototype._init.call(this, width, height, numberOfColumns, columnGap); + this._menus = []; + this._menuDisplays = []; + // map - this._categories = {}; + this._appCategories = {}; let me = this; this._appMonitor = new Shell.AppMonitor(); @@ -158,12 +233,116 @@ AppDisplay.prototype = { // We still need to determine what events other than search can trigger // a change in the set of applications that are being shown while the // user in in the overlay mode, however let's redisplay just in case. - me._redisplay(false); + me._redisplay(false); + me._redisplayMenus(); }); // Load the GAppInfos now so it doesn't slow down the first // transition into the overlay this._refreshCache(); + + this._focusInMenus = true; + this._activeMenuIndex = -1; + this._activeMenu = null; + this._activeMenuApps = null; + this._menuDisplay = new Big.Box({ orientation: Big.BoxOrientation.VERTICAL, + spacing: MENU_SPACING + }); + this._redisplayMenus(); + + this.connect('expanded', Lang.bind(this, function (self) { + this._filterReset(); + })); + }, + + moveRight: function() { + if (this._expanded && this._focusInMenu) { + this._focusInMenu = false; + this._activeMenu.setState(MENU_ENTERED); + this.selectFirstItem(); + } + }, + + moveLeft: function() { + if (this._expanded && !this._focusInMenu) { + this._activeMenu.setState(MENU_SELECTED); + this.unsetSelected(); + this._focusInMenu = true; + } + }, + + // Override genericDisplay.js + getSideArea: function() { + return this._menuDisplay; + }, + + selectUp: function() { + if (!(this._expanded && this._focusInMenu)) + return GenericDisplay.GenericDisplay.prototype.selectUp.call(this); + this._selectMenuIndex(this._activeMenuIndex - 1); + return true; + }, + + selectDown: function() { + if (!(this._expanded && this._focusInMenu)) + return GenericDisplay.GenericDisplay.prototype.selectDown.call(this); + this._selectMenuIndex(this._activeMenuIndex+1); + return true; + }, + + // Protected overrides + + _filterActive: function() { + return !!this._search || this._activeMenuIndex >= 0; + }, + + _filterReset: function() { + GenericDisplay.GenericDisplay.prototype._filterReset.call(this); + if (this._activeMenu != null) + this._activeMenu.setState(MENU_UNSELECTED); + this._activeMenuIndex = -1; + this._activeMenu = null; + this._focusInMenu = true; + }, + + //// Private //// + + _emitStateChange: function() { + this.emit('state-changed'); + }, + + _selectMenuIndex: function(index) { + if (index < 0 || index >= this._menus.length) + return; + if (this._activeMenu != null) + this._activeMenu.setState(MENU_UNSELECTED); + this._activeMenu = this._menuDisplays[index]; + this._activeMenu.setState(MENU_SELECTED); + }, + + _redisplayMenus: function() { + this._menuDisplay.remove_all(); + for (let i = 0; i < this._menus.length; i++) { + let menu = this._menus[i]; + let display = new MenuItem(menu.name, menu.id, menu.icon); + this._menuDisplays.push(display); + let menuIndex = i; + display.connect('state-changed', Lang.bind(this, function (display) { + let activated = display.getState() != MENU_UNSELECTED; + if (!activated && display == this._activeMenu) { + this._activeMenuIndex = -1; + this._activeMenu = null; + } else if (activated) { + if (this._activeMenu != null) + this._activeMenu.setState(MENU_SELECTED); + this._activeMenuIndex = menuIndex; + this._activeMenu = display; + this._activeMenuApps = this._appMonitor.get_applications_for_menu(menu.id); + } + this._redisplay(); + })); + this._menuDisplay.append(display.actor, 0); + } }, //// Protected method overrides //// @@ -174,7 +353,10 @@ AppDisplay.prototype = { if (!this._appsStale) return; this._allItems = {}; - this._categories = {}; + this._appCategories = {}; + + this._menus = this._appMonitor.get_menus(); + let apps = Gio.app_info_get_all(); for (let i = 0; i < apps.length; i++) { let appInfo = apps[i]; @@ -186,7 +368,7 @@ AppDisplay.prototype = { this._allItems[appId] = appInfo; // [] is returned if we could not get the categories or the list of categories was empty let categories = Shell.get_categories_for_desktop_file(appId); - this._categories[appId] = categories; + this._appCategories[appId] = categories; } this._appsStale = false; }, @@ -217,6 +399,25 @@ AppDisplay.prototype = { // Item info is expected to be GAppInfo. // Returns a boolean flag indicating if itemInfo is a match. _isInfoMatching : function(itemInfo, search) { + // Search takes precedence; not typically useful to search within a + // menu + if (this._activeMenu == null || search != "") + return this._isInfoMatchingSearch(itemInfo, search); + else + return this._isInfoMatchingMenu(itemInfo, search); + }, + + _isInfoMatchingMenu : function(itemInfo, search) { + let id = itemInfo.get_id(); + for (let i = 0; i < this._activeMenuApps.length; i++) { + let activeId = this._activeMenuApps[i]; + if (activeId == id) + return true; + } + return false; + }, + + _isInfoMatchingSearch: function(itemInfo, search) { if (search == null || search == '') return true; @@ -239,8 +440,8 @@ AppDisplay.prototype = { return true; } - // we expect this._categories.hasOwnProperty(itemInfo.get_id()) to always be true here - let categories = this._categories[itemInfo.get_id()]; + // we expect this._appCategories.hasOwnProperty(itemInfo.get_id()) to always be true here + let categories = this._appCategories[itemInfo.get_id()]; for (let i = 0; i < categories.length; i++) { let category = categories[i].toLowerCase(); if (category.indexOf(search) >= 0) @@ -253,7 +454,7 @@ AppDisplay.prototype = { // Creates an AppDisplayItem based on itemInfo, which is expected be a GAppInfo object. _createDisplayItem: function(itemInfo) { return new AppDisplayItem(itemInfo, this._columnWidth); - } + } }; Signals.addSignalMethods(AppDisplay.prototype); diff --git a/js/ui/genericDisplay.js b/js/ui/genericDisplay.js index e5c75ec26..79215de93 100644 --- a/js/ui/genericDisplay.js +++ b/js/ui/genericDisplay.js @@ -415,6 +415,7 @@ function GenericDisplay(width, height, numberOfColumns, columnGap) { GenericDisplay.prototype = { _init : function(width, height, numberOfColumns, columnGap) { this._search = ''; + this._expanded = false; this._width = null; this._height = null; this._columnWidth = null; @@ -425,8 +426,10 @@ GenericDisplay.prototype = { this._columnGap = DEFAULT_COLUMN_GAP; this._maxItemsPerPage = null; - this._setDimensionsAndMaxItems(width, height); this._grid = new Tidy.Grid({width: this._width, height: this._height}); + + this._setDimensionsAndMaxItems(width, 0, height); + this._grid.column_major = true; this._grid.column_gap = this._columnGap; // map where Object represents the item info @@ -440,6 +443,9 @@ GenericDisplay.prototype = { this._pageDisplayed = 0; this._selectedIndex = -1; this._keepDisplayCurrent = false; + // These two are public - .actor is the normal "actor subclass" property, + // but we also expose a .displayControl actor which is separate. + // See also getSideArea. this.actor = this._grid; this.displayControl = new Big.Box({ background_color: ITEM_DISPLAY_BACKGROUND_COLOR, corner_radius: 4, @@ -540,13 +546,23 @@ GenericDisplay.prototype = { }, // Readjusts display layout and the items displayed based on the new dimensions. - updateDimensions: function(width, height, numberOfColumns) { + setExpanded: function(expanded, baseWidth, expandWidth, height, numberOfColumns) { + this._expanded = expanded; this._numberOfColumns = numberOfColumns; - this._setDimensionsAndMaxItems(width, height); + this._setDimensionsAndMaxItems(baseWidth, expandWidth, height); this._grid.width = this._width; this._grid.height = this._height; this._pageDisplayed = 0; this._displayMatchedItems(true); + let gridWidth = this._width; + let sideArea = this.getSideArea(); + if (sideArea) { + if (expanded) + sideArea.show(); + else + sideArea.hide(); + } + this.emit('expanded'); }, // Updates the displayed items and makes the display actor visible. @@ -559,10 +575,17 @@ GenericDisplay.prototype = { // Hides the display actor. hide: function() { this._grid.hide(); + this._filterReset(); this._removeAllDisplayItems(); this._keepDisplayCurrent = false; }, + // Returns an actor which acts as a sidebar; this is used for + // the applications category view + getSideArea: function () { + return null; + }, + //// Protected methods //// /* @@ -691,13 +714,24 @@ GenericDisplay.prototype = { this._removeDisplayItem(itemId); }, + // Return true if there's an active search or other constraint + // on the list + _filterActive: function() { + return this._search != ""; + }, + + // Called when we are resetting state + _filterReset: function() { + this.unsetSelected(); + }, + /* * Updates the displayed items, applying the search string if one exists. * * resetPage - indicates if the page selection should be reset when displaying the matching results. * We reset the page selection when the change in results was initiated by the user by * entering a different search criteria or by viewing the results list in a different - * size mode, but we keep the page selection the same if the results got updated on + * size mode, but we keep the page selection the same if the results got updated on * their own while the user was browsing through the result pages. */ _redisplay: function(resetPage) { @@ -705,7 +739,7 @@ GenericDisplay.prototype = { return; this._refreshCache(); - if (!this._search) + if (!this._filterActive()) this._setDefaultList(); else this._doSearchFilter(); @@ -754,18 +788,27 @@ GenericDisplay.prototype = { // Sets this._width, this._height, this._columnWidth, and this._maxItemsPerPage based on the // space available for the display, number of columns, and the number of items it can fit. - _setDimensionsAndMaxItems: function(width, height) { - this._width = width; - this._columnWidth = (this._width - this._columnGap * (this._numberOfColumns - 1)) / this._numberOfColumns; + _setDimensionsAndMaxItems: function(baseWidth, expandWidth, height) { + this._width = baseWidth + expandWidth; + let gridWidth; + let sideArea = this.getSideArea(); + if (this._expanded && sideArea) { + gridWidth = expandWidth; + sideArea.width = baseWidth; + sideArea.height = this._height; + } else { + gridWidth = this._width; + } + this._columnWidth = (gridWidth - this._columnGap * (this._numberOfColumns - 1)) / this._numberOfColumns; let maxItemsInColumn = Math.floor(height / ITEM_DISPLAY_HEIGHT); this._maxItemsPerPage = maxItemsInColumn * this._numberOfColumns; this._height = maxItemsInColumn * ITEM_DISPLAY_HEIGHT; + this._grid.width = gridWidth; + this._grid.height = this._height; }, - // Applies the search string to the list of items to find matches, and displays the matching items. - _doSearchFilter: function() { + _getSearchMatchedItems: function() { let matchedItemsForSearch = {}; - // Break the search up into terms, and search for each // individual term. Keep track of the number of terms // each item matched. @@ -783,6 +826,22 @@ GenericDisplay.prototype = { } } } + return matchedItemsForSearch; + }, + + // Applies the search string to the list of items to find matches, + // and displays the matching items. + _doSearchFilter: function() { + let matchedItemsForSearch; + + if (this._filterActive()) { + matchedItemsForSearch = this._getSearchMatchedItems(); + } else { + matchedItemsForSearch = {}; + for (let itemId in this._allItems) { + matchedItemsForSearch[itemId] = 1; + } + } this._matchedItems = []; for (itemId in matchedItemsForSearch) { diff --git a/js/ui/overlay.js b/js/ui/overlay.js index 819249c81..2a936816c 100644 --- a/js/ui/overlay.js +++ b/js/ui/overlay.js @@ -76,6 +76,11 @@ const ROWS_FOR_WORKSPACES_WIDE_SCREEN = 8; const WORKSPACES_X_FACTOR_ASIDE_MODE_WIDE_SCREEN = 5 - 0.25; const EXPANDED_SIDESHOW_COLUMNS_WIDE_SCREEN = 3; +// A multi-state; PENDING is used during animations +const STATE_ACTIVE = true; +const STATE_PENDING_INACTIVE = false; +const STATE_INACTIVE = false; + let wideScreen = false; let displayGridColumnWidth = null; let displayGridRowHeight = null; @@ -88,8 +93,8 @@ Sideshow.prototype = { _init : function() { let me = this; - this._moreAppsMode = false; - this._moreDocsMode = false; + this._moreAppsMode = STATE_INACTIVE; + this._moreDocsMode = STATE_INACTIVE; let asideXFactor = wideScreen ? WORKSPACES_X_FACTOR_ASIDE_MODE_WIDE_SCREEN : WORKSPACES_X_FACTOR_ASIDE_MODE_REGULAR_SCREEN; this._expandedSideshowColumns = wideScreen ? EXPANDED_SIDESHOW_COLUMNS_WIDE_SCREEN : EXPANDED_SIDESHOW_COLUMNS_REGULAR_SCREEN; @@ -181,7 +186,11 @@ Sideshow.prototype = { // 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._appDisplay.hasSelected() && !me._appDisplay.selectUp() && me._docDisplay.hasItems()) { + if (me._moreAppsMode) + me._appDisplay.selectUp(); + else if (me._moreDocsMode) + me._docDisplay.selectUp(); + else if (me._appDisplay.hasSelected() && !me._appDisplay.selectUp() && me._docDisplay.hasItems()) { me._appDisplay.unsetSelected(); me._docDisplay.selectLastItem(); } else if (me._docDisplay.hasSelected() && !me._docDisplay.selectUp() && me._appDisplay.hasItems()) { @@ -190,7 +199,11 @@ Sideshow.prototype = { } return true; } else if (symbol == Clutter.Down) { - if (me._appDisplay.hasSelected() && !me._appDisplay.selectDown() && me._docDisplay.hasItems()) { + if (me._moreAppsMode) + me._appDisplay.selectDown(); + else if (me._moreDocsMode) + me._docDisplay.selectDown(); + else if (me._appDisplay.hasSelected() && !me._appDisplay.selectDown() && me._docDisplay.hasItems()) { me._appDisplay.unsetSelected(); me._docDisplay.selectFirstItem(); } else if (me._docDisplay.hasSelected() && !me._docDisplay.selectDown() && me._appDisplay.hasItems()) { @@ -198,6 +211,20 @@ Sideshow.prototype = { me._appDisplay.selectFirstItem(); } return true; + } else if (me._moreAppsMode && me._searchEntry.text == '') { + if (symbol == Clutter.Right) { + me._appDisplay.moveRight(); + return true; + } else if (symbol == Clutter.Left) { + me._appDisplay.moveLeft(); + return true; + } + return false; + } else if (symbol == Clutter.Right && me._searchEntry.text == '') { + if (me._appDisplay.hasSelected()) + me._setMoreAppsMode(); + else + me._setMoreDocsMode(); } return false; }); @@ -219,8 +246,13 @@ Sideshow.prototype = { this._itemDisplayHeight = global.screen_height - this._appsSection.y - SIDESHOW_SECTION_MISC_HEIGHT * 2 - bottomHeight; + this._appsContent = new Big.Box({ orientation: Big.BoxOrientation.HORIZONTAL }); + this._appsSection.append(this._appsContent, Big.BoxPackFlags.EXPAND); this._appDisplay = new AppDisplay.AppDisplay(this._width, this._itemDisplayHeight / 2, SIDESHOW_COLUMNS, SIDESHOW_PAD); - this._appsSection.append(this._appDisplay.actor, Big.BoxPackFlags.EXPAND); + let sideArea = this._appDisplay.getSideArea(); + sideArea.hide(); + this._appsContent.append(sideArea, Big.BoxPackFlags.NONE); + this._appsContent.append(this._appDisplay.actor, Big.BoxPackFlags.EXPAND); let moreAppsBox = new Big.Box({x_align: Big.BoxAlignment.END}); this._moreAppsLink = new Link.Link({ color: SIDESHOW_TEXT_COLOR, @@ -249,7 +281,7 @@ Sideshow.prototype = { height: LABEL_HEIGHT}); this._docsSection.append(this._docsText, Big.BoxPackFlags.EXPAND); - this._docDisplay = new DocDisplay.DocDisplay(this._width, this._itemDisplayHeight - this._appDisplay.actor.height, SIDESHOW_COLUMNS, SIDESHOW_PAD); + this._docDisplay = new DocDisplay.DocDisplay(this._width, this._itemDisplayHeight - this._appsContent.height, SIDESHOW_COLUMNS, SIDESHOW_PAD); this._docsSection.append(this._docDisplay.actor, Big.BoxPackFlags.EXPAND); let moreDocsBox = new Big.Box({x_align: Big.BoxAlignment.END}); @@ -328,7 +360,8 @@ Sideshow.prototype = { show: function() { let global = Shell.Global.get(); - this._appDisplay.show(); + this._appDisplay.show(); + this._appsContent.show(); this._docDisplay.show(); this._appDisplay.selectFirstItem(); if (!this._appDisplay.hasSelected()) @@ -339,7 +372,7 @@ Sideshow.prototype = { }, hide: function() { - this._appDisplay.hide(); + this._appsContent.hide(); this._docDisplay.hide(); this._searchEntry.text = ''; this._unsetMoreAppsMode(); @@ -368,7 +401,9 @@ Sideshow.prototype = { if (this._moreAppsMode) return; - this._moreAppsMode = true; + // No corresponding STATE_PENDING_ACTIVE, because we call updateAppsSection + // immediately below. + this._moreAppsMode = STATE_ACTIVE; this._docsSection.set_clip(0, 0, this._docsSection.width, this._docsSection.height); @@ -377,9 +412,9 @@ Sideshow.prototype = { // Move the selection to the applications section if it was in the docs section. this._docDisplay.unsetSelected(); - if (!this._appDisplay.hasSelected()) - this._appDisplay.selectFirstItem(); - + // Because we have menus in applications, we want to reset the selection for applications + // as well. The default is no menu. + this._appDisplay.unsetSelected(); this._updateAppsSection(); Tweener.addTween(this._docsSection, @@ -389,7 +424,7 @@ Sideshow.prototype = { transition: "easeOutQuad" }); - // We need to be expandig the clip on the applications section so that the first additional + // We need to expand the clip on the applications section so that the first additional // application to be displayed does not appear abruptly. Tweener.addTween(this._appsSection, { clipHeightBottom: this._itemDisplayHeight + SIDESHOW_SECTION_MISC_HEIGHT * 2 - LABEL_HEIGHT - SIDESHOW_SECTION_SPACING, @@ -407,7 +442,7 @@ Sideshow.prototype = { if (!this._moreAppsMode) return; - this._moreAppsMode = false; + this._moreAppsMode = STATE_PENDING_INACTIVE; this._moreAppsLink.actor.hide(); @@ -447,8 +482,11 @@ Sideshow.prototype = { // Removes the clip from the applications section to reveal the 'More...' text. // Removes the clip from the documents section, so that the clip does not limit the size of // the section if it is expanded later. - _onAppsSectionReduced: function() { - this._updateAppsSection(); + _onAppsSectionReduced: function() { + if (this._moreAppsMode != STATE_PENDING_INACTIVE) + return; + this._moreAppsMode = STATE_INACTIVE; + this._updateAppsSection(); if (!this._appDisplay.hasItems()) this._docDisplay.selectFirstItem(); this._appsSection.remove_clip(); @@ -460,15 +498,18 @@ Sideshow.prototype = { // changed, which is ensured by _setMoreAppsMode() and _unsetMoreAppsMode() functions. _updateAppsSection: function() { if (this._moreAppsMode) { - this._appDisplay.updateDimensions(this._width + this._additionalWidth, - this._itemDisplayHeight + SIDESHOW_SECTION_MISC_HEIGHT, - this._expandedSideshowColumns); + // Subtract one from columns since we are displaying menus + this._appDisplay.setExpanded(true, this._width, this._additionalWidth, + this._itemDisplayHeight + SIDESHOW_SECTION_MISC_HEIGHT, + this._expandedSideshowColumns - 1); this._moreAppsLink.setText("Less..."); - this._appsSection.insert_after(this._appsDisplayControlBox, this._appDisplay.actor, Big.BoxPackFlags.NONE); + this._appsSection.insert_after(this._appsDisplayControlBox, this._appsContent, Big.BoxPackFlags.NONE); this.actor.add_actor(this._details); this._details.append(this._appDisplay.selectedItemDetails, Big.BoxPackFlags.NONE); } else { - this._appDisplay.updateDimensions(this._width, this._appsSectionDefaultHeight - SIDESHOW_SECTION_MISC_HEIGHT, SIDESHOW_COLUMNS); + this._appDisplay.setExpanded(false, this._width, 0, + this._appsSectionDefaultHeight - SIDESHOW_SECTION_MISC_HEIGHT, + SIDESHOW_COLUMNS); this._moreAppsLink.setText("More..."); this._appsSection.remove_actor(this._appsDisplayControlBox); this.actor.remove_actor(this._details); @@ -532,7 +573,7 @@ Sideshow.prototype = { this._docsSection.set_clip(0, 0, this._docsSection.width, this._docsSection.height); - this._appDisplay.show(); + this._appsContent.show(); Tweener.addTween(this._docsSection, { y: this._docsSection.y + this._appsSectionDefaultHeight, @@ -555,7 +596,7 @@ Sideshow.prototype = { // Hides the applications section so that it doesn't get updated on new searches. _onDocsSectionExpanded: function() { this._docsSection.remove_clip(); - this._appDisplay.hide(); + this._appsContent.hide(); }, // Updates the documents section to contain fewer items. Selects the first item in the @@ -576,15 +617,17 @@ Sideshow.prototype = { // changed, which is ensured by _setMoreDocsMode() and _unsetMoreDocsMode() functions. _updateDocsSection: function() { if (this._moreDocsMode) { - this._docDisplay.updateDimensions(this._width + this._additionalWidth, - this._itemDisplayHeight + SIDESHOW_SECTION_MISC_HEIGHT, - this._expandedSideshowColumns); + this._docDisplay.setExpanded(true, this._width + this._additionalWidth, + this._itemDisplayHeight + SIDESHOW_SECTION_MISC_HEIGHT, + this._expandedSideshowColumns); this._moreDocsLink.setText("Less..."); this._docsSection.insert_after(this._docsDisplayControlBox, this._docDisplay.actor, Big.BoxPackFlags.NONE); this.actor.add_actor(this._details); this._details.append(this._docDisplay.selectedItemDetails, Big.BoxPackFlags.NONE); } else { - this._docDisplay.updateDimensions(this._width, this._docsSectionDefaultHeight - SIDESHOW_SECTION_MISC_HEIGHT, SIDESHOW_COLUMNS); + this._docDisplay.setExpanded(false, this._width, + this._docsSectionDefaultHeight - SIDESHOW_SECTION_MISC_HEIGHT, + SIDESHOW_COLUMNS); this._moreDocsLink.setText("More..."); this._docsSection.remove_actor(this._docsDisplayControlBox); this.actor.remove_actor(this._details); diff --git a/src/Makefile.am b/src/Makefile.am index 9b2da575f..688f2798d 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -51,6 +51,8 @@ libgnome_shell_la_SOURCES = \ gnome-shell-plugin.c \ shell-app-monitor.c \ shell-app-monitor.h \ + shell-arrow.c \ + shell-arrow.h \ shell-gtkwindow-actor.c \ shell-gtkwindow-actor.h \ shell-process.c \ diff --git a/src/shell-app-monitor.c b/src/shell-app-monitor.c index 3a0ea9fa9..570829019 100644 --- a/src/shell-app-monitor.c +++ b/src/shell-app-monitor.c @@ -4,6 +4,9 @@ #include +#define GMENU_I_KNOW_THIS_IS_UNSTABLE +#include + enum { PROP_0, @@ -17,16 +20,41 @@ enum { static guint signals[LAST_SIGNAL] = { 0 }; struct _ShellAppMonitorPrivate { - GList *desktop_dir_monitors; + GMenuTree *tree; + GMenuTreeDirectory *trunk; + + GList *cached_menus; }; static void shell_app_monitor_finalize (GObject *object); -static void on_monitor_changed (GFileMonitor *monitor, GFile *file, - GFile *other_file, GFileMonitorEvent event_type, - gpointer user_data); +static void on_tree_changed (GMenuTree *tree, gpointer user_data); +static void reread_menus (ShellAppMonitor *self); G_DEFINE_TYPE(ShellAppMonitor, shell_app_monitor, G_TYPE_OBJECT); +static gpointer +shell_app_menu_entry_copy (gpointer entryp) +{ + ShellAppMenuEntry *entry; + ShellAppMenuEntry *copy; + entry = entryp; + copy = g_new0 (ShellAppMenuEntry, 1); + copy->name = g_strdup (entry->name); + copy->id = g_strdup (entry->id); + copy->icon = g_strdup (entry->icon); + return copy; +} + +static void +shell_app_menu_entry_free (gpointer entryp) +{ + ShellAppMenuEntry *entry = entryp; + g_free (entry->name); + g_free (entry->id); + g_free (entry->icon); + g_free (entry); +} + static void shell_app_monitor_class_init(ShellAppMonitorClass *klass) { GObjectClass *gobject_class = (GObjectClass *)klass; @@ -48,33 +76,18 @@ static void shell_app_monitor_class_init(ShellAppMonitorClass *klass) static void shell_app_monitor_init (ShellAppMonitor *self) { - const gchar *const *iter; - self->priv = G_TYPE_INSTANCE_GET_PRIVATE (self, - SHELL_TYPE_APP_MONITOR, - ShellAppMonitorPrivate); - for (iter = g_get_system_data_dirs (); *iter; iter++) - { - char *app_path; - GFile *dir; - GFileMonitor *monitor; - GError *error = NULL; + ShellAppMonitorPrivate *priv; + self->priv = priv = G_TYPE_INSTANCE_GET_PRIVATE (self, + SHELL_TYPE_APP_MONITOR, + ShellAppMonitorPrivate); - app_path = g_build_filename (*iter, "applications", NULL); + priv->tree = gmenu_tree_lookup ("applications.menu", GMENU_TREE_FLAGS_NONE); - dir = g_file_new_for_path (app_path); - g_free (app_path); - monitor = g_file_monitor_directory (dir, 0, NULL, &error); - if (!monitor) { - g_warning ("failed to monitor %s", error->message); - g_clear_error (&error); - continue; - } - g_signal_connect (monitor, "changed", G_CALLBACK (on_monitor_changed), self); - self->priv->desktop_dir_monitors - = g_list_prepend (self->priv->desktop_dir_monitors, - monitor); - g_object_unref (dir); - } + priv->trunk = gmenu_tree_get_root_directory (priv->tree); + + gmenu_tree_add_monitor (priv->tree, on_tree_changed, self); + + reread_menus (self); } static void @@ -82,18 +95,112 @@ shell_app_monitor_finalize (GObject *object) { ShellAppMonitor *self = SHELL_APP_MONITOR (object); - g_list_foreach (self->priv->desktop_dir_monitors, (GFunc) g_object_unref, NULL); - g_list_free (self->priv->desktop_dir_monitors); - G_OBJECT_CLASS (shell_app_monitor_parent_class)->finalize(object); } static void -on_monitor_changed (GFileMonitor *monitor, GFile *file, - GFile *other_file, GFileMonitorEvent event_type, - gpointer user_data) +reread_menus (ShellAppMonitor *self) +{ + GSList *entries = gmenu_tree_directory_get_contents (self->priv->trunk); + GSList *iter; + ShellAppMonitorPrivate *priv = self->priv; + + g_list_foreach (self->priv->cached_menus, (GFunc)shell_app_menu_entry_free, NULL); + g_list_free (self->priv->cached_menus); + self->priv->cached_menus = NULL; + + for (iter = entries; iter; iter = iter->next) + { + GMenuTreeEntry *entry = iter->data; + ShellAppMenuEntry *shell_entry = g_new0 (ShellAppMenuEntry, 1); + + shell_entry->name = g_strdup (gmenu_tree_entry_get_name (entry)); + shell_entry->id = g_strdup (gmenu_tree_entry_get_desktop_file_id (entry)); + shell_entry->icon = g_strdup (gmenu_tree_entry_get_icon (entry)); + + priv->cached_menus = g_list_prepend (priv->cached_menus, shell_entry); + + gmenu_tree_item_unref (entry); + } + priv->cached_menus = g_list_reverse (priv->cached_menus); + + g_slist_free (entries); +} + +static void +on_tree_changed (GMenuTree *monitor, gpointer user_data) { ShellAppMonitor *self = SHELL_APP_MONITOR (user_data); g_signal_emit (self, signals[CHANGED], 0); + + reread_menus (self); +} + +GType +shell_app_menu_entry_get_type (void) +{ + static GType gtype = G_TYPE_INVALID; + if (gtype == G_TYPE_INVALID) + { + gtype = g_boxed_type_register_static ("ShellAppMenuEntry", + shell_app_menu_entry_copy, + shell_app_menu_entry_free); + } + return gtype; +} + +/** + * shell_app_monitor_get_applications_for_menu: + * + * Return value: (transfer full) (element-type utf8): List of desktop file ids + */ +GList * +shell_app_monitor_get_applications_for_menu (ShellAppMonitor *monitor, + const char *menu) +{ + GList *ret = NULL; + GSList *contents; + GSList *iter; + char *path; + GMenuTreeDirectory *menu_entry; + + path = g_strdup_printf ("/%s", menu); + menu_entry = gmenu_tree_get_directory_from_path (monitor->priv->tree, path); + g_free (path); + g_assert (menu_entry != NULL); + + contents = gmenu_tree_directory_get_contents (menu_entry); + + for (iter = contents; iter; iter = iter->next) + { + GMenuTreeItem *item = iter->data; + switch (gmenu_tree_item_get_type (item)) + { + case GMENU_TREE_ITEM_ENTRY: + { + GMenuTreeEntry *entry = (GMenuTreeEntry *)item; + const char *id = gmenu_tree_entry_get_desktop_file_id (entry); + ret = g_list_prepend (ret, g_strdup (id)); + } + break; + default: + break; + } + gmenu_tree_item_unref (item); + } + g_slist_free (contents); + + return ret; +} + +/** + * shell_app_monitor_get_menus: + * + * Return value: (transfer none) (element-type AppMenuEntry): List of toplevel menus + */ +GList * +shell_app_monitor_get_menus (ShellAppMonitor *monitor) +{ + return monitor->priv->cached_menus; } diff --git a/src/shell-app-monitor.h b/src/shell-app-monitor.h index e5d8923cb..9a777428e 100644 --- a/src/shell-app-monitor.h +++ b/src/shell-app-monitor.h @@ -31,4 +31,18 @@ struct _ShellAppMonitorClass GType shell_app_monitor_get_type (void) G_GNUC_CONST; ShellAppMonitor* shell_app_monitor_new(void); +GList *shell_app_monitor_get_applications_for_menu (ShellAppMonitor *monitor, const char *menu); + +typedef struct _ShellAppMenuEntry ShellAppMenuEntry; + +struct _ShellAppMenuEntry { + char *name; + char *id; + char *icon; +}; + +GType shell_app_menu_entry_get_type (void); + +GList *shell_app_monitor_get_menus (ShellAppMonitor *monitor); + #endif /* __SHELL_APP_MONITOR_H__ */ diff --git a/src/shell-arrow.c b/src/shell-arrow.c new file mode 100644 index 000000000..1d961f3b4 --- /dev/null +++ b/src/shell-arrow.c @@ -0,0 +1,141 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ + +#include "shell-arrow.h" + +#include +#include +#include + +enum { + PROP_0, + + PROP_DIRECTION +}; + +G_DEFINE_TYPE(ShellArrow, shell_arrow, CLUTTER_TYPE_CAIRO_TEXTURE); + +struct _ShellArrowPrivate { + GtkArrowType direction; +}; + +static void shell_arrow_redraw (ShellArrow *self); + +static void +shell_arrow_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + ShellArrow *self = SHELL_ARROW (object); + + switch (prop_id) + { + case PROP_DIRECTION: + self->priv->direction = g_value_get_enum (value); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } + + shell_arrow_redraw (self); +} + +static void +shell_arrow_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + ShellArrow *self = SHELL_ARROW (object); + + switch (prop_id) + { + case PROP_DIRECTION: + g_value_set_enum (value, self->priv->direction); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +shell_arrow_redraw (ShellArrow *self) +{ + cairo_t *cr; + guint width, height; + + g_object_get (G_OBJECT (self), "surface-width", &width, + "surface-height", &height, + NULL); + + if (width == 0) + return; + + cr = clutter_cairo_texture_create (CLUTTER_CAIRO_TEXTURE (self)); + + cairo_set_source_rgb (cr, 1, 1, 1); + + switch (self->priv->direction) + { + case GTK_ARROW_RIGHT: + cairo_move_to (cr, 0, 0); + cairo_line_to (cr, width, height*0.5); + cairo_line_to (cr, 0, height); + break; + case GTK_ARROW_LEFT: + cairo_move_to (cr, width, 0); + cairo_line_to (cr, 0, height*0.5); + cairo_line_to (cr, width, height); + break; + case GTK_ARROW_UP: + cairo_move_to (cr, 0, height); + cairo_line_to (cr, width*0.5, 0); + cairo_line_to (cr, width, height); + break; + case GTK_ARROW_DOWN: + cairo_move_to (cr, 0, 0); + cairo_line_to (cr, width*0.5, height); + cairo_line_to (cr, width, height); + break; + case GTK_ARROW_NONE: + default: + break; + } + + cairo_close_path (cr); + cairo_fill (cr); + + cairo_destroy (cr); +} + +static void +shell_arrow_class_init (ShellArrowClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + g_type_class_add_private (klass, sizeof (ShellArrowPrivate)); + + object_class->get_property = shell_arrow_get_property; + object_class->set_property = shell_arrow_set_property; + + g_object_class_install_property (object_class, + PROP_DIRECTION, + g_param_spec_enum ("direction", + "Direction", + "Direction", + GTK_TYPE_ARROW_TYPE, + GTK_ARROW_NONE, + G_PARAM_READWRITE)); +} + +static void +shell_arrow_init (ShellArrow *actor) +{ + actor->priv = G_TYPE_INSTANCE_GET_PRIVATE (actor, SHELL_TYPE_ARROW, + ShellArrowPrivate); + g_signal_connect (actor, "notify::surface-width", G_CALLBACK (shell_arrow_redraw), NULL); +} diff --git a/src/shell-arrow.h b/src/shell-arrow.h new file mode 100644 index 000000000..500d201ed --- /dev/null +++ b/src/shell-arrow.h @@ -0,0 +1,33 @@ +#ifndef __SHELL_ARROW_H__ +#define __SHELL_ARROW_H__ + +#include +#include + +#define SHELL_TYPE_ARROW (shell_arrow_get_type ()) +#define SHELL_ARROW(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), SHELL_TYPE_ARROW, ShellArrow)) +#define SHELL_ARROW_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), SHELL_TYPE_ARROW, ShellArrowClass)) +#define SHELL_IS_ARROW(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), SHELL_TYPE_ARROW)) +#define SHELL_IS_ARROW_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), SHELL_TYPE_ARROW)) +#define SHELL_ARROW_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), SHELL_TYPE_ARROW, ShellArrowClass)) + +typedef struct _ShellArrow ShellArrow; +typedef struct _ShellArrowClass ShellArrowClass; + +typedef struct _ShellArrowPrivate ShellArrowPrivate; + +struct _ShellArrow +{ + ClutterCairoTexture parent; + + ShellArrowPrivate *priv; +}; + +struct _ShellArrowClass +{ + ClutterCairoTextureClass parent_class; +}; + +GType shell_arrow_get_type (void) G_GNUC_CONST; + +#endif /* __SHELL_ARROW_H__ */