diff --git a/js/ui/Makefile.am b/js/ui/Makefile.am index bb12365e5..ebc7f53d3 100644 --- a/js/ui/Makefile.am +++ b/js/ui/Makefile.am @@ -19,7 +19,7 @@ dist_jsui_DATA = \ main.js \ overview.js \ panel.js \ - places.js \ + placeDisplay.js \ runDialog.js \ shellDBus.js \ sidebar.js \ diff --git a/js/ui/dash.js b/js/ui/dash.js index 5c05340cd..da4f8e887 100644 --- a/js/ui/dash.js +++ b/js/ui/dash.js @@ -14,7 +14,7 @@ const _ = Gettext.gettext; const AppDisplay = imports.ui.appDisplay; const DocDisplay = imports.ui.docDisplay; -const Places = imports.ui.places; +const PlaceDisplay = imports.ui.placeDisplay; const GenericDisplay = imports.ui.genericDisplay; const Button = imports.ui.button; const Main = imports.ui.main; @@ -60,6 +60,7 @@ PANE_BACKGROUND_COLOR.from_pixel(0x000000f4); const APPS = "apps"; const PREFS = "prefs"; const DOCS = "docs"; +const PLACES = "places"; /* * Returns the index in an array of a given length that is obtained @@ -81,7 +82,8 @@ function _createDisplay(displayType) { return new AppDisplay.AppDisplay(true); else if (displayType == DOCS) return new DocDisplay.DocDisplay(); - + else if (displayType == PLACES) + return new PlaceDisplay.PlaceDisplay(); return null; } @@ -729,7 +731,7 @@ Dash.prototype = { /* Translators: This is in the sense of locations for documents, network locations, etc. */ this._placesSection = new Section(_("PLACES"), true); - let placesDisplay = new Places.Places(); + let placesDisplay = new PlaceDisplay.DashPlaceDisplay(); this._placesSection.content.append(placesDisplay.actor, Big.BoxPackFlags.EXPAND); this.sectionArea.append(this._placesSection.actor, Big.BoxPackFlags.NONE); @@ -783,6 +785,11 @@ Dash.prototype = { title: _("RECENT DOCUMENTS"), header: null, resultArea: null + }, + { type: PLACES, + title: _("PLACES"), + header: null, + resultArea: null } ]; diff --git a/js/ui/main.js b/js/ui/main.js index aac26720b..5a0ad4cc9 100644 --- a/js/ui/main.js +++ b/js/ui/main.js @@ -16,6 +16,7 @@ const Chrome = imports.ui.chrome; const Environment = imports.ui.environment; const Overview = imports.ui.overview; const Panel = imports.ui.panel; +const PlaceDisplay = imports.ui.placeDisplay; const RunDialog = imports.ui.runDialog; const LookingGlass = imports.ui.lookingGlass; const ShellDBus = imports.ui.shellDBus; @@ -28,6 +29,7 @@ DEFAULT_BACKGROUND_COLOR.from_pixel(0x2266bbff); let chrome = null; let panel = null; let sidebar = null; +let placesManager = null; let overview = null; let runDialog = null; let lookingGlass = null; @@ -96,6 +98,7 @@ function start() { getRunDialog().open(); }); + placesManager = new PlaceDisplay.PlacesManager(); overview = new Overview.Overview(); chrome = new Chrome.Chrome(); panel = new Panel.Panel(); diff --git a/js/ui/places.js b/js/ui/placeDisplay.js similarity index 50% rename from js/ui/places.js rename to js/ui/placeDisplay.js index 9aa604de5..acee53e55 100644 --- a/js/ui/places.js +++ b/js/ui/placeDisplay.js @@ -23,120 +23,93 @@ const PLACES_VSPACING = 8; const PLACES_ICON_SIZE = 16; /** - * An entry in the places menu. + * Represents a place object, which is most normally a bookmark entry, + * a mount/volume, or a special place like the Home Folder, Computer, and Network. + * * @name: String title - * @iconFactory: A JavaScript callback which will create an icon texture - * @onActivate: A JavaScript callback to launch the entry + * @iconFactory: A JavaScript callback which will create an icon texture given a size parameter + * @launch: A JavaScript callback to launch the entry */ -function PlaceDisplay(name, iconFactory, onActivate) { - this._init(name, iconFactory, onActivate); +function PlaceInfo(name, iconFactory, launch) { + this._init(name, iconFactory, launch); } -PlaceDisplay.prototype = { - _init : function(name, iconFactory, onActivate) { - this.actor = new Big.Box({ orientation: Big.BoxOrientation.HORIZONTAL, - reactive: true, - spacing: 4 }); - this.actor.connect('button-release-event', Lang.bind(this, function (b, e) { - onActivate(this); - Main.overview.hide(); - })); - let text = new Clutter.Text({ font_name: "Sans 14px", - ellipsize: Pango.EllipsizeMode.END, - color: GenericDisplay.ITEM_DISPLAY_NAME_COLOR, - text: name }); - let iconBox = new Big.Box({ y_align: Big.BoxAlignment.CENTER }); - this._icon = iconFactory(); - iconBox.append(this._icon, Big.BoxPackFlags.NONE); - this.actor.append(iconBox, Big.BoxPackFlags.NONE); - this.actor.append(text, Big.BoxPackFlags.EXPAND); - - this._iconFactory = iconFactory; - this._onActivate = onActivate; - - this.actor._delegate = this; - let draggable = DND.makeDraggable(this.actor); - }, - - getDragActorSource: function() { - return this._icon; - }, - - getDragActor: function(stageX, stageY) { - return this._iconFactory(); - }, - - //// Drag and drop methods //// - - shellWorkspaceLaunch : function() { - this._onActivate(); +PlaceInfo.prototype = { + _init: function(name, iconFactory, launch) { + this.name = name; + this.iconFactory = iconFactory; + this.launch = launch; + this.id = null; } -}; -Signals.addSignalMethods(PlaceDisplay.prototype); +} -function Places() { + +function PlacesManager() { this._init(); } -Places.prototype = { - _init : function() { - // Places is divided semi-arbitrarily into left and right; a grid would - // look better in that there would be an even number of items left+right, - // but it seems like we want some sort of differentiation between actions - // like "Connect to server..." and regular folders - this.actor = new Big.Box({ orientation: Big.BoxOrientation.HORIZONTAL, - spacing: 4 }); - this._leftBox = new Big.Box({ orientation: Big.BoxOrientation.VERTICAL }); - this.actor.append(this._leftBox, Big.BoxPackFlags.EXPAND); - this._rightBox = new Big.Box({ orientation: Big.BoxOrientation.VERTICAL }); - this.actor.append(this._rightBox, Big.BoxPackFlags.EXPAND); - - // Subdivide left into actions and devices - this._actionsBox = new Big.Box({ orientation: Big.BoxOrientation.VERTICAL, - spacing: PLACES_VSPACING }); - this._leftBox.append(this._actionsBox, Big.BoxPackFlags.NONE); - this._devBox = new Big.Box({ orientation: Big.BoxOrientation.VERTICAL, - spacing: PLACES_VSPACING, - padding_top: 6 }); - this._leftBox.append(this._devBox, Big.BoxPackFlags.NONE); - - // Right is bookmarks - this._dirsBox = new Big.Box({ orientation: Big.BoxOrientation.VERTICAL, - spacing: PLACES_VSPACING }); - this._rightBox.append(this._dirsBox, Big.BoxPackFlags.NONE); - +PlacesManager.prototype = { + _init: function() { let gconf = Shell.GConf.get_default(); gconf.watch_directory(NAUTILUS_PREFS_DIR); + this._mounts = []; + this._bookmarks = []; + this._isDesktopHome = false; + let homeFile = Gio.file_new_for_path (GLib.get_home_dir()); let homeUri = homeFile.get_uri(); let homeLabel = Shell.util_get_label_for_uri (homeUri); let homeIcon = Shell.util_get_icon_for_uri (homeUri); - let home = new PlaceDisplay(homeLabel, - function() { - return Shell.TextureCache.get_default().load_gicon(homeIcon, PLACES_ICON_SIZE); + this._home = new PlaceInfo(homeLabel, + function(size) { + return Shell.TextureCache.get_default().load_gicon(homeIcon, size); }, function() { Gio.app_info_launch_default_for_uri(homeUri, Main.createAppLaunchContext()); }); - this._actionsBox.append(home.actor, Big.BoxPackFlags.NONE); - let desktopPath = GLib.get_user_special_dir(GLib.UserDirectory.DIRECTORY_DESKTOP); let desktopFile = Gio.file_new_for_path (desktopPath); let desktopUri = desktopFile.get_uri(); let desktopLabel = Shell.util_get_label_for_uri (desktopUri); let desktopIcon = Shell.util_get_icon_for_uri (desktopUri); - this._desktopMenu = new PlaceDisplay(desktopLabel, - function() { - return Shell.TextureCache.get_default().load_gicon(desktopIcon, PLACES_ICON_SIZE); + this._desktopMenu = new PlaceInfo(desktopLabel, + function(size) { + return Shell.TextureCache.get_default().load_gicon(desktopIcon, size); }, function() { Gio.app_info_launch_default_for_uri(desktopUri, Main.createAppLaunchContext()); }); - this._actionsBox.append(this._desktopMenu.actor, Big.BoxPackFlags.NONE); - this._updateDesktopMenuVisibility(); - gconf.connect('changed::' + DESKTOP_IS_HOME_KEY, Lang.bind(this, this._updateDesktopMenuVisibility)); + + this._connect = new PlaceInfo(_("Connect to..."), + function (size) { + return Shell.TextureCache.get_default().load_icon_name("applications-internet", size); + }, + function () { + new Shell.Process({ args: ['nautilus-connect-server'] }).run(); + }); + + let networkApp = null; + try { + networkApp = Shell.AppSystem.get_default().load_from_desktop_file('gnome-network-scheme.desktop'); + } catch(e) { + try { + networkApp = Shell.AppSystem.get_default().load_from_desktop_file('network-scheme.desktop'); + } catch(e) { + log("Cannot create \"Network\" item, .desktop file not found or corrupt."); + } + } + + if (networkApp != null) { + this._network = new PlaceInfo(networkApp.get_name(), + function(size) { + return networkApp.create_icon_texture(size); + }, + function () { + networkApp.launch(); + }); + } /* * Show devices, code more or less ported from nautilus-places-sidebar.c @@ -153,37 +126,6 @@ Places.prototype = { this._volumeMonitor.connect('drive-changed', Lang.bind(this, this._updateDevices)); this._updateDevices(); - let networkApp = null; - try { - networkApp = Shell.AppSystem.get_default().load_from_desktop_file('gnome-network-scheme.desktop'); - } catch(e) { - try { - networkApp = Shell.AppSystem.get_default().load_from_desktop_file('network-scheme.desktop'); - } catch(e) { - log("Cannot create \"Network\" item, .desktop file not found or corrupt."); - } - } - - if (networkApp != null) { - let network = new PlaceDisplay(networkApp.get_name(), - function() { - return networkApp.create_icon_texture(PLACES_ICON_SIZE); - }, - function () { - networkApp.launch(); - }); - this._actionsBox.append(network.actor, Big.BoxPackFlags.NONE); - } - - let connect = new PlaceDisplay(_("Connect to..."), - function () { - return Shell.TextureCache.get_default().load_icon_name("applications-internet", PLACES_ICON_SIZE); - }, - function () { - new Shell.Process({ args: ['nautilus-connect-server'] }).run(); - }); - this._actionsBox.append(connect.actor, Big.BoxPackFlags.NONE); - this._bookmarksPath = GLib.build_filenamev([GLib.get_home_dir(), ".gtk-bookmarks"]); this._bookmarksFile = Gio.file_new_for_path(this._bookmarksPath); let monitor = this._bookmarksFile.monitor_file(Gio.FileMonitorFlags.NONE, null); @@ -200,14 +142,68 @@ Places.prototype = { })); this._reloadBookmarks(); + this._updateDesktopMenuVisibility(); + + gconf.connect('changed::' + DESKTOP_IS_HOME_KEY, Lang.bind(this, this._updateDesktopMenuVisibility)); + + }, + + _updateDevices: function() { + this._mounts = []; + + /* first go through all connected drives */ + let drives = this._volumeMonitor.get_connected_drives(); + for (let i = 0; i < drives.length; i++) { + let volumes = drives[i].get_volumes(); + for(let j = 0; j < volumes.length; j++) { + let mount = volumes[j].get_mount(); + if(mount != null) { + this._addMount(mount); + } + } + } + + /* add all volumes that is not associated with a drive */ + let volumes = this._volumeMonitor.get_volumes(); + for(let i = 0; i < volumes.length; i++) { + if(volumes[i].get_drive() != null) + continue; + + let mount = volumes[i].get_mount(); + if(mount != null) { + this._addMount(mount); + } + } + + /* add mounts that have no volume (/etc/mtab mounts, ftp, sftp,...) */ + let mounts = this._volumeMonitor.get_mounts(); + for(let i = 0; i < mounts.length; i++) { + if(mounts[i].is_shadowed()) + continue; + + if(mounts[i].get_volume()) + continue; + + this._addMount(mounts[i]); + } + + /* We emit two signals, one for a generic 'all places' update + * and the other for one specific to mounts. We do this because + * clients like PlaceDisplay may only care about places in general + * being updated while clients like DashPlaceDisplay care which + * specific type of place got updated. + */ + this.emit('mounts-updated'); + this.emit('places-updated'); + }, _reloadBookmarks: function() { - this._dirsBox.remove_all(); + this._bookmarks = []; if (!GLib.file_test(this._bookmarksPath, GLib.FileTest.EXISTS)) - return; + return; let [success, bookmarksContent, len] = GLib.file_get_contents(this._bookmarksPath); @@ -243,55 +239,28 @@ Places.prototype = { continue; let icon = Shell.util_get_icon_for_uri(bookmark); - let item = new PlaceDisplay(label, - function() { - return Shell.TextureCache.get_default().load_gicon(icon, PLACES_ICON_SIZE); + let item = new PlaceInfo(label, + function(size) { + return Shell.TextureCache.get_default().load_gicon(icon, size); }, function() { Gio.app_info_launch_default_for_uri(bookmark, Main.createAppLaunchContext()); }); - this._dirsBox.append(item.actor, Big.BoxPackFlags.NONE); + this._bookmarks.push(item); } + + /* See comment in _updateDevices for explanation why there are two signals. */ + this.emit('bookmarks-updated'); + this.emit('places-updated'); }, - _updateDevices: function() { - this._devBox.remove_all(); + _updateDesktopMenuVisibility: function() { + let gconf = Shell.GConf.get_default(); + this._isDesktopHome = gconf.get_boolean(DESKTOP_IS_HOME_KEY); - /* first go through all connected drives */ - let drives = this._volumeMonitor.get_connected_drives(); - for (let i = 0; i < drives.length; i++) { - let volumes = drives[i].get_volumes(); - for(let j = 0; j < volumes.length; j++) { - let mount = volumes[j].get_mount(); - if(mount != null) { - this._addMount(mount); - } - } - } - - /* add all volumes that is not associated with a drive */ - let volumes = this._volumeMonitor.get_volumes(); - for(let i = 0; i < volumes.length; i++) { - if(volumes[i].get_drive() != null) - continue; - - let mount = volumes[i].get_mount(); - if(mount != null) { - this._addMount(mount); - } - } - - /* add mounts that have no volume (/etc/mtab mounts, ftp, sftp,...) */ - let mounts = this._volumeMonitor.get_mounts(); - for(let i = 0; i < mounts.length; i++) { - if(mounts[i].is_shadowed()) - continue; - - if(mounts[i].get_volume()) - continue; - - this._addMount(mounts[i]); - } + /* See comment in _updateDevices for explanation why there are two signals. */ + this.emit('defaults-updated'); + this.emit('places-updated'); }, _addMount: function(mount) { @@ -299,23 +268,282 @@ Places.prototype = { let mountIcon = mount.get_icon(); let root = mount.get_root(); let mountUri = root.get_uri(); - let devItem = new PlaceDisplay(mountLabel, - function() { - return Shell.TextureCache.get_default().load_gicon(mountIcon, PLACES_ICON_SIZE); + let devItem = new PlaceInfo(mountLabel, + function(size) { + return Shell.TextureCache.get_default().load_gicon(mountIcon, size); }, function() { Gio.app_info_launch_default_for_uri(mountUri, Main.createAppLaunchContext()); }); - this._devBox.append(devItem.actor, Big.BoxPackFlags.NONE); + this._mounts.push(devItem); }, - _updateDesktopMenuVisibility: function() { - let gconf = Shell.GConf.get_default(); - let desktopIsHome = gconf.get_boolean(DESKTOP_IS_HOME_KEY); - if (desktopIsHome) - this._desktopMenu.actor.hide(); - else - this._desktopMenu.actor.show(); + getAllPlaces: function () { + return this.getDefaultPlaces().concat(this.getBookmarks(), this.getMounts()); + }, + + getDefaultPlaces: function () { + let places = [this._home]; + + if (this._isDesktopHome) + places.push(this._desktopMenu); + + if (this._network) + places.push(this._network); + + places.push(this._connect); + return places; + }, + + getBookmarks: function () { + return this._bookmarks; + }, + + getMounts: function () { + return this._mounts; + } +}; + +Signals.addSignalMethods(PlacesManager.prototype); + +/** + * An entry in the places menu. + * @info The corresponding PlaceInfo to populate this entry. + */ +function DashPlaceDisplayItem(info) { + this._init(info); +} + +DashPlaceDisplayItem.prototype = { + _init: function(info) { + this.name = info.name; + this._info = info; + this._icon = info.iconFactory(PLACES_ICON_SIZE); + this.actor = new Big.Box({ orientation: Big.BoxOrientation.HORIZONTAL, + reactive: true, + spacing: 4 }); + this.actor.connect('button-release-event', Lang.bind(this, function (b, e) { + this._info.launch(); + Main.overview.hide(); + })); + let text = new Clutter.Text({ font_name: 'Sans 14px', + ellipsize: Pango.EllipsizeMode.END, + color: GenericDisplay.ITEM_DISPLAY_NAME_COLOR, + text: this.name }); + let iconBox = new Big.Box({ y_align: Big.BoxAlignment.CENTER }); + iconBox.append(this._icon, Big.BoxPackFlags.NONE); + this.actor.append(iconBox, Big.BoxPackFlags.NONE); + this.actor.append(text, Big.BoxPackFlags.EXPAND); + + this.actor._delegate = this; + let draggable = DND.makeDraggable(this.actor); + }, + + getDragActorSource: function() { + return this._icon; + }, + + getDragActor: function(stageX, stageY) { + return this._info.iconFactory(PLACES_ICON_SIZE); + }, + + //// Drag and drop methods //// + + shellWorkspaceLaunch: function() { + this._info.launch(); + } +}; + +function DashPlaceDisplay() { + this._init(); +} + +DashPlaceDisplay.prototype = { + _init: function() { + + // Places is divided semi-arbitrarily into left and right; a grid would + // look better in that there would be an even number of items left+right, + // but it seems like we want some sort of differentiation between actions + // like "Connect to server..." and regular folders + this.actor = new Big.Box({ orientation: Big.BoxOrientation.HORIZONTAL, + spacing: 4 }); + this._leftBox = new Big.Box({ orientation: Big.BoxOrientation.VERTICAL }); + this.actor.append(this._leftBox, Big.BoxPackFlags.EXPAND); + this._rightBox = new Big.Box({ orientation: Big.BoxOrientation.VERTICAL }); + this.actor.append(this._rightBox, Big.BoxPackFlags.EXPAND); + + // Subdivide left into actions and devices + this._actionsBox = new Big.Box({ orientation: Big.BoxOrientation.VERTICAL, + spacing: PLACES_VSPACING }); + + this._devBox = new Big.Box({ orientation: Big.BoxOrientation.VERTICAL, + spacing: PLACES_VSPACING, + padding_top: 6 }); + + this._dirsBox = new Big.Box({ orientation: Big.BoxOrientation.VERTICAL, + spacing: PLACES_VSPACING }); + + this._leftBox.append(this._actionsBox, Big.BoxPackFlags.NONE); + this._leftBox.append(this._devBox, Big.BoxPackFlags.NONE); + + this._rightBox.append(this._dirsBox, Big.BoxPackFlags.NONE); + + Main.placesManager.connect('defaults-updated', Lang.bind(this, this._updateDefaults)); + Main.placesManager.connect('bookmarks-updated', Lang.bind(this, this._updateBookmarks)); + Main.placesManager.connect('mounts-updated', Lang.bind(this, this._updateMounts)); + + this._updateDefaults(); + this._updateMounts(); + this._updateBookmarks(); + }, + + _updateDefaults: function() { + this._actionsBox.get_children().forEach(function (child) { + child.destroy(); + }); + + let places = Main.placesManager.getDefaultPlaces(); + for (let i = 0; i < places.length; i++) + this._actionsBox.append(new DashPlaceDisplayItem(places[i]).actor, Big.BoxPackFlags.NONE); + }, + + _updateMounts: function() { + this._devBox.get_children().forEach(function (child) { + child.destroy(); + }); + + let places = Main.placesManager.getMounts(); + for (let i = 0; i < places.length; i++) + this._devBox.append(new DashPlaceDisplayItem(places[i]).actor, Big.BoxPackFlags.NONE); + }, + + _updateBookmarks: function() { + this._dirsBox.get_children().forEach(function (child) { + child.destroy(); + }); + + let places = Main.placesManager.getBookmarks(); + for (let i = 0; i < places.length; i ++) + this._dirsBox.append(new DashPlaceDisplayItem(places[i]).actor, Big.BoxPackFlags.NONE); + } +}; + +Signals.addSignalMethods(DashPlaceDisplay.prototype); + + +function PlaceDisplayItem(placeInfo) { + this._init(placeInfo); +} + +PlaceDisplayItem.prototype = { + __proto__: GenericDisplay.GenericDisplayItem.prototype, + + _init : function(placeInfo) { + GenericDisplay.GenericDisplayItem.prototype._init.call(this); + this._info = placeInfo; + + this._setItemInfo(placeInfo.name, ''); + }, + + //// Public method overrides //// + + // Opens an application represented by this display item. + launch : function() { + this._info.launch(); + }, + + shellWorkspaceLaunch: function() { + this._info.launch(); + }, + + //// Protected method overrides //// + + // Returns an icon for the item. + _createIcon: function() { + return this._info.iconFactory(GenericDisplay.ITEM_DISPLAY_ICON_SIZE); + }, + + // Returns a preview icon for the item. + _createPreviewIcon: function() { + return this._info.iconFactory(GenericDisplay.PREVIEW_ICON_SIZE); + } + +}; + +function PlaceDisplay() { + this._init(); +} + +PlaceDisplay.prototype = { + __proto__: GenericDisplay.GenericDisplay.prototype, + + _init: function() { + GenericDisplay.GenericDisplay.prototype._init.call(this); + this._stale = true; + Main.placesManager.connect('places-updated', Lang.bind(this, function (e) { + this._stale = true; + })); + }, + + //// Protected method overrides //// + _refreshCache: function () { + if (!this._stale) + return true; + this._allItems = {}; + let array = Main.placesManager.getAllPlaces(); + for (let i = 0; i < array.length; i ++) { + // We are using an array id as placeInfo id because placeInfo doesn't have any + // other information piece that can be used as a unique id. There are different + // types of placeInfo, such as devices and directories that would result in differently + // structured ids. Also the home directory can show up in both the default places and in + // bookmarks which means its URI can't be used as a unique id. (This does mean it can + // appear twice in search results, though that doesn't happen at the moment because we + // name it "Home Folder" in default places and it's named with the user's system name + // if it appears as a bookmark.) + let placeInfo = array[i]; + placeInfo.id = i; + this._allItems[i] = placeInfo; + } + this._stale = false; + return false; + }, + + // Sets the list of the displayed items. + _setDefaultList: function() { + this._matchedItems = {}; + this._matchedItemKeys = []; + for (id in this._allItems) { + this._matchedItems[id] = 1; + this._matchedItemKeys.push(id); + } + this._matchedItemKeys.sort(Lang.bind(this, this._compareItems)); + }, + + // Checks if the item info can be a match for the search string by checking + // the name of the place. Item info is expected to be PlaceInfo. + // Returns a boolean flag indicating if itemInfo is a match. + _isInfoMatching: function(itemInfo, search) { + if (search == null || search == '') + return true; + + let name = itemInfo.name.toLowerCase(); + if (name.indexOf(search) >= 0) + return true; + + return false; + }, + + // Compares items associated with the item ids based on the alphabetical order + // of the item names. + // Returns an integer value indicating the result of the comparison. + _compareItems: function(itemIdA, itemIdB) { + let placeA = this._allItems[itemIdA]; + let placeB = this._allItems[itemIdB]; + return placeA.name.localeCompare(placeB.name); + }, + + // Creates a PlaceDisplayItem based on itemInfo, which is expected to be a PlaceInfo object. + _createDisplayItem: function(itemInfo) { + return new PlaceDisplayItem(itemInfo); } }; -Signals.addSignalMethods(Places.prototype);