0a9226f7f2
ShellAppMonitor now depends on gmenu to load menus. Use the menu data from ShellAppMonitor to show a menu list. GenericDisplay implementations can now have a sidebar area. We handle keystrokes such as left/right explicitly. Some internal API changes to account for the fact that a display can have another filter in addition to the search.
962 lines
39 KiB
JavaScript
962 lines
39 KiB
JavaScript
/* -*- 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 Gdk = imports.gi.Gdk;
|
|
const Gtk = imports.gi.Gtk;
|
|
const Lang = imports.lang;
|
|
const Mainloop = imports.mainloop;
|
|
const Pango = imports.gi.Pango;
|
|
const Signals = imports.signals;
|
|
const Shell = imports.gi.Shell;
|
|
const Tidy = imports.gi.Tidy;
|
|
|
|
const DND = imports.ui.dnd;
|
|
const Link = imports.ui.link;
|
|
|
|
const ITEM_DISPLAY_NAME_COLOR = new Clutter.Color();
|
|
ITEM_DISPLAY_NAME_COLOR.from_pixel(0xffffffff);
|
|
const ITEM_DISPLAY_DESCRIPTION_COLOR = new Clutter.Color();
|
|
ITEM_DISPLAY_DESCRIPTION_COLOR.from_pixel(0xffffffbb);
|
|
const ITEM_DISPLAY_BACKGROUND_COLOR = new Clutter.Color();
|
|
ITEM_DISPLAY_BACKGROUND_COLOR.from_pixel(0x00000000);
|
|
const ITEM_DISPLAY_SELECTED_BACKGROUND_COLOR = new Clutter.Color();
|
|
ITEM_DISPLAY_SELECTED_BACKGROUND_COLOR.from_pixel(0x00ff0055);
|
|
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 ITEM_DISPLAY_HEIGHT = 50;
|
|
const ITEM_DISPLAY_ICON_SIZE = 48;
|
|
const ITEM_DISPLAY_PADDING = 1;
|
|
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;
|
|
// 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;
|
|
|
|
/* This is a virtual class that represents a single display item containing
|
|
* a name, a description, and an icon. It allows selecting an item and represents
|
|
* it by highlighting it with a different background color than the default.
|
|
*
|
|
* availableWidth - total width available for the item
|
|
*/
|
|
function GenericDisplayItem(availableWidth) {
|
|
this._init(availableWidth);
|
|
}
|
|
|
|
GenericDisplayItem.prototype = {
|
|
_init: function(availableWidth) {
|
|
this._availableWidth = availableWidth;
|
|
this._showPreview = false;
|
|
this._havePointer = false;
|
|
this._previewEventSourceId = null;
|
|
|
|
this.actor = new Clutter.Group({ reactive: true,
|
|
width: availableWidth,
|
|
height: ITEM_DISPLAY_HEIGHT });
|
|
this.actor._delegate = this;
|
|
this.actor.connect('button-press-event',
|
|
Lang.bind(this,
|
|
function(actor, e) {
|
|
let clickCount = Shell.get_button_event_click_count(e);
|
|
if (clickCount == 1)
|
|
this.select();
|
|
else if (clickCount == 2)
|
|
this.activate();
|
|
}));
|
|
|
|
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._name = null;
|
|
this._description = null;
|
|
this._icon = null;
|
|
this._preview = null;
|
|
this._previewIcon = null;
|
|
|
|
this.dragActor = null;
|
|
|
|
this.actor.connect('enter-event', Lang.bind(this, this._onEnter));
|
|
this.actor.connect('leave-event', Lang.bind(this, this._onLeave));
|
|
},
|
|
|
|
//// Draggable object interface ////
|
|
|
|
// Returns a cloned texture of the item's icon to represent the item as it
|
|
// is being dragged.
|
|
getDragActor: function(stageX, stageY) {
|
|
this.dragActor = new Clutter.Clone({ source: this._icon });
|
|
[this.dragActor.width, this.dragActor.height] = this._icon.get_transformed_size();
|
|
|
|
// If the user dragged from the icon itself, then position
|
|
// the dragActor over the original icon. Otherwise center it
|
|
// around the pointer
|
|
let [iconX, iconY] = this._icon.get_transformed_position();
|
|
let [iconWidth, iconHeight] = this._icon.get_transformed_size();
|
|
if (stageX > iconX && stageX <= iconX + iconWidth &&
|
|
stageY > iconY && stageY <= iconY + iconHeight)
|
|
this.dragActor.set_position(iconX, iconY);
|
|
else
|
|
this.dragActor.set_position(stageX - this.dragActor.width / 2, stageY - this.dragActor.height / 2);
|
|
return this.dragActor;
|
|
},
|
|
|
|
// Returns the original icon that is being used as a source for the cloned texture
|
|
// that represents the item as it is being dragged.
|
|
getDragActorSource: function() {
|
|
return this._icon;
|
|
},
|
|
|
|
//// Public methods ////
|
|
|
|
// Sets a boolean value that indicates whether the item should display a pop-up preview on mouse over.
|
|
setShowPreview: function(showPreview) {
|
|
this._showPreview = showPreview;
|
|
},
|
|
|
|
// Returns a boolean value that indicates whether the item displays a pop-up preview on mouse over.
|
|
getShowPreview: function() {
|
|
return this._showPreview;
|
|
},
|
|
|
|
// Displays the preview for the item.
|
|
showPreview: function() {
|
|
if(!this._showPreview)
|
|
return;
|
|
|
|
this._ensurePreviewCreated();
|
|
|
|
let [x, y] = this.actor.get_transformed_position();
|
|
let global = Shell.Global.get();
|
|
let previewX = Math.min(x + this._availableWidth * PREVIEW_PLACING, global.screen_width - this._preview.width);
|
|
let previewY = Math.min(y, global.screen_height - this._preview.height);
|
|
this._preview.set_position(previewX, previewY);
|
|
|
|
this._preview.show();
|
|
},
|
|
|
|
// Hides the preview for the item and removes the preview event source so that
|
|
// there is no preview scheduled to show up.
|
|
hidePreview: function() {
|
|
if (this._previewEventSourceId) {
|
|
Mainloop.source_remove(this._previewEventSourceId);
|
|
this._previewEventSourceId = null;
|
|
}
|
|
|
|
if (this._preview)
|
|
this._preview.hide();
|
|
},
|
|
|
|
// Shows a preview when the item was drawn under the mouse pointer.
|
|
onDrawnUnderPointer: function() {
|
|
this._havePointer = true;
|
|
// This code is usually triggered when we just had a different preview showing on the same spot
|
|
// and having a delay before showing a new preview looks bad. So we just show it right away.
|
|
this.showPreview();
|
|
},
|
|
|
|
// Highlights the item by setting a different background color than the default
|
|
// if isSelected is true, removes the highlighting otherwise.
|
|
markSelected: function(isSelected) {
|
|
let color;
|
|
if (isSelected)
|
|
color = ITEM_DISPLAY_SELECTED_BACKGROUND_COLOR;
|
|
else
|
|
color = ITEM_DISPLAY_BACKGROUND_COLOR;
|
|
this._bg.background_color = color;
|
|
},
|
|
|
|
// Activates the item, as though it was launched
|
|
activate: function() {
|
|
this.hidePreview();
|
|
this.emit('activate');
|
|
},
|
|
|
|
// Selects the item, as though it was clicked
|
|
select: function() {
|
|
this.emit('select');
|
|
},
|
|
|
|
/*
|
|
* 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
|
|
*/
|
|
createDetailsActor: function(availableWidth) {
|
|
let details = new Big.Box({ orientation: Big.BoxOrientation.HORIZONTAL,
|
|
spacing: PREVIEW_BOX_SPACING,
|
|
width: availableWidth });
|
|
|
|
this._ensurePreviewIconCreated();
|
|
|
|
if (this._previewIcon != null) {
|
|
let previewIconClone = new Clutter.Clone({ source: this._previewIcon });
|
|
details.append(previewIconClone, Big.BoxPackFlags.NONE);
|
|
}
|
|
|
|
// Inner box with name and description
|
|
let textDetails = new Big.Box({ orientation: Big.BoxOrientation.VERTICAL,
|
|
spacing: PREVIEW_BOX_SPACING });
|
|
let detailsName = new Clutter.Text({ color: ITEM_DISPLAY_NAME_COLOR,
|
|
font_name: "Sans bold 14px",
|
|
line_wrap: true,
|
|
text: this._name.text});
|
|
textDetails.append(detailsName, Big.BoxPackFlags.NONE);
|
|
|
|
let detailsDescription = new Clutter.Text({ color: ITEM_DISPLAY_NAME_COLOR,
|
|
font_name: "Sans 14px",
|
|
line_wrap: true,
|
|
text: this._description.text });
|
|
textDetails.append(detailsDescription, Big.BoxPackFlags.NONE);
|
|
|
|
details.append(textDetails, Big.BoxPackFlags.EXPAND);
|
|
|
|
// We hide the preview pop-up if the details are shown elsewhere.
|
|
details.connect("show",
|
|
Lang.bind(this,
|
|
function() {
|
|
// Right now "show" signal is emitted when an actor is added to a parent that
|
|
// has not been added to anything and "visible" property is also set to true
|
|
// at this point, so checking if the parent that the actor has been added to
|
|
// has a parent of its own is a temporary workaround. That other actor is
|
|
// presumed to be displayed, which is a limitation of this workaround, but is
|
|
// the case with our usage of the details actor now.
|
|
// http://bugzilla.openedhand.com/show_bug.cgi?id=1138
|
|
if (details.get_parent() != null && details.get_parent().get_parent() != null)
|
|
this.hidePreview();
|
|
}));
|
|
return details;
|
|
},
|
|
|
|
// Destoys the item, as well as a preview for the item if it exists.
|
|
destroy: function() {
|
|
this.actor.destroy();
|
|
if (this._preview != null)
|
|
this._preview.destroy();
|
|
},
|
|
|
|
//// Pure virtual public methods ////
|
|
|
|
// Performes an action associated with launching this item, such as opening a file or an application.
|
|
launch: function() {
|
|
throw new Error("Not implemented");
|
|
},
|
|
|
|
//// Protected methods ////
|
|
|
|
/*
|
|
* Creates the graphical elements for the item based on the item information.
|
|
*
|
|
* nameText - name of the item
|
|
* descriptionText - short description of the item
|
|
* iconActor - ClutterTexture containing the icon image which should be ITEM_DISPLAY_ICON_SIZE size
|
|
*/
|
|
_setItemInfo: function(nameText, descriptionText, iconActor) {
|
|
if (this._name != null) {
|
|
// this also removes this._name from the parent container,
|
|
// so we don't need to call this.actor.remove_actor(this._name) directly
|
|
this._name.destroy();
|
|
this._name = null;
|
|
}
|
|
if (this._description != null) {
|
|
this._description.destroy();
|
|
this._description = null;
|
|
}
|
|
if (this._icon != null) {
|
|
// though we get the icon from elsewhere, we assume its ownership here,
|
|
// and therefore should be responsible for distroying it
|
|
this._icon.destroy();
|
|
this._icon = null;
|
|
}
|
|
// This ensures we'll create a new preview and previewIcon next time we need a preview
|
|
if (this._preview != null) {
|
|
this._preview.destroy();
|
|
this._preview = null;
|
|
}
|
|
if (this._previewIcon != null) {
|
|
this._previewIcon.destroy();
|
|
this._previewIcon = null;
|
|
}
|
|
|
|
this._icon = iconActor;
|
|
this.actor.add_actor(this._icon);
|
|
|
|
let textWidth = this._availableWidth - (ITEM_DISPLAY_ICON_SIZE + 4);
|
|
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 });
|
|
this.actor.add_actor(this._name);
|
|
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);
|
|
},
|
|
|
|
//// Pure virtual protected methods ////
|
|
|
|
// Ensures the preview icon is created.
|
|
_ensurePreviewIconCreated: function() {
|
|
throw new Error("Not implemented");
|
|
},
|
|
|
|
//// Private methods ////
|
|
|
|
// Ensures the preview actor is created.
|
|
_ensurePreviewCreated: function() {
|
|
if (!this._showPreview || this._preview)
|
|
return;
|
|
|
|
this._preview = new Big.Box({ background_color: PREVIEW_BOX_BACKGROUND_COLOR,
|
|
orientation: Big.BoxOrientation.HORIZONTAL,
|
|
corner_radius: PREVIEW_BOX_CORNER_RADIUS,
|
|
padding: PREVIEW_BOX_PADDING,
|
|
spacing: PREVIEW_BOX_SPACING });
|
|
|
|
let textDetailsWidth = this._availableWidth - PREVIEW_BOX_PADDING * 2;
|
|
|
|
this._ensurePreviewIconCreated();
|
|
|
|
if (this._previewIcon != null) {
|
|
this._preview.append(this._previewIcon, Big.BoxPackFlags.EXPAND);
|
|
textDetailsWidth = this._availableWidth - this._previewIcon.width - PREVIEW_BOX_PADDING * 2 - PREVIEW_BOX_SPACING;
|
|
}
|
|
|
|
// Inner box with name and description
|
|
let textDetails = new Big.Box({ orientation: Big.BoxOrientation.VERTICAL,
|
|
spacing: PREVIEW_BOX_SPACING });
|
|
let detailsName = new Clutter.Text({ color: ITEM_DISPLAY_NAME_COLOR,
|
|
font_name: "Sans bold 14px",
|
|
text: this._name.text});
|
|
|
|
textDetails.width = Math.max(PREVIEW_DETAILS_MIN_WIDTH, textDetailsWidth, detailsName.width);
|
|
|
|
textDetails.append(detailsName, Big.BoxPackFlags.NONE);
|
|
|
|
let detailsDescription = new Clutter.Text({ color: ITEM_DISPLAY_NAME_COLOR,
|
|
font_name: "Sans 14px",
|
|
line_wrap: true,
|
|
text: this._description.text });
|
|
textDetails.append(detailsDescription, Big.BoxPackFlags.NONE);
|
|
|
|
this._preview.append(textDetails, Big.BoxPackFlags.EXPAND);
|
|
|
|
// Add the preview to global stage to allow for top-level layering
|
|
let global = Shell.Global.get();
|
|
global.stage.add_actor(this._preview);
|
|
this._preview.hide();
|
|
},
|
|
|
|
// Performs actions on mouse enter event for the item. Currently, shows the preview for the item.
|
|
_onEnter: function(actor, event) {
|
|
this._havePointer = true;
|
|
let tooltipTimeout = Gtk.Settings.get_default().gtk_tooltip_timeout;
|
|
this._previewEventSourceId = Mainloop.timeout_add(tooltipTimeout,
|
|
Lang.bind(this,
|
|
function() {
|
|
if (this._havePointer) {
|
|
this.showPreview();
|
|
}
|
|
this._previewEventSourceId = null;
|
|
return false;
|
|
}));
|
|
},
|
|
|
|
// Performs actions on mouse leave event for the item. Currently, hides the preview for the item.
|
|
_onLeave: function(actor, event) {
|
|
this._havePointer = false;
|
|
this.hidePreview();
|
|
},
|
|
|
|
// Hides the preview once the item starts being dragged.
|
|
_onDragBegin : function (draggable, time) {
|
|
// For some reason, we are not getting leave-event signal when we are dragging an item,
|
|
// so the preview box stays behind if we didn't have the call here. It makes sense to hide
|
|
// the preview as soon as the item starts being dragged anyway.
|
|
this._havePointer = false;
|
|
this.hidePreview();
|
|
}
|
|
};
|
|
|
|
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
|
|
* height - height available for the display
|
|
*/
|
|
function GenericDisplay(width, height, numberOfColumns, columnGap) {
|
|
this._init(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;
|
|
|
|
this._numberOfColumns = numberOfColumns;
|
|
this._columnGap = columnGap;
|
|
if (this._columnGap == null)
|
|
this._columnGap = DEFAULT_COLUMN_GAP;
|
|
|
|
this._maxItemsPerPage = null;
|
|
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<itemId, Object> where Object represents the item info
|
|
this._allItems = {};
|
|
// an array of itemIds of items that match the current request
|
|
// in the order in which the items should be displayed
|
|
this._matchedItems = [];
|
|
// map<itemId, GenericDisplayItem>
|
|
this._displayedItems = {};
|
|
this._displayedItemsCount = 0;
|
|
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,
|
|
height: 24,
|
|
spacing: 12,
|
|
orientation: Big.BoxOrientation.HORIZONTAL});
|
|
|
|
this._availableWidthForItemDetails = this._columnWidth;
|
|
this.selectedItemDetails = new Big.Box({});
|
|
},
|
|
|
|
//// Public methods ////
|
|
|
|
setAvailableWidthForItemDetails: function(availableWidth) {
|
|
this._availableWidthForItemDetails = availableWidth;
|
|
},
|
|
|
|
getAvailableWidthForItemDetails: function() {
|
|
return this._availableWidthForItemDetails;
|
|
},
|
|
|
|
// Sets the search string and displays the matching items.
|
|
setSearch: function(text) {
|
|
this._search = text.toLowerCase();
|
|
this._redisplay(true);
|
|
},
|
|
|
|
// Launches the item that is currently selected and emits 'activated' signal.
|
|
activateSelected: function() {
|
|
if (this._selectedIndex != -1) {
|
|
let selected = this._findDisplayedByIndex(this._selectedIndex);
|
|
selected.launch()
|
|
this.emit('activated');
|
|
}
|
|
},
|
|
|
|
// Moves the selection one up. If the selection was already on the top item, it's moved
|
|
// to the bottom one. Returns true if the selection actually moved up, false if it wrapped
|
|
// around to the bottom.
|
|
selectUp: function() {
|
|
let selectedUp = true;
|
|
let prev = this._selectedIndex - 1;
|
|
if (this._selectedIndex <= 0) {
|
|
prev = this._displayedItemsCount - 1;
|
|
selectedUp = false;
|
|
}
|
|
this._selectIndex(prev);
|
|
return selectedUp;
|
|
},
|
|
|
|
// Moves the selection one down. If the selection was already on the bottom item, it's moved
|
|
// to the top one. Returns true if the selection actually moved down, false if it wrapped
|
|
// around to the top.
|
|
selectDown: function() {
|
|
let selectedDown = true;
|
|
let next = this._selectedIndex + 1;
|
|
if (this._selectedIndex == this._displayedItemsCount - 1) {
|
|
next = 0;
|
|
selectedDown = false;
|
|
}
|
|
this._selectIndex(next);
|
|
return selectedDown;
|
|
},
|
|
|
|
// Selects the first item among the displayed items.
|
|
selectFirstItem: function() {
|
|
if (this.hasItems())
|
|
this._selectIndex(0);
|
|
},
|
|
|
|
// Selects the last item among the displayed items.
|
|
selectLastItem: function() {
|
|
if (this.hasItems())
|
|
this._selectIndex(this._displayedItemsCount - 1);
|
|
},
|
|
|
|
// Returns true if the display has some item selected.
|
|
hasSelected: function() {
|
|
return this._selectedIndex != -1;
|
|
},
|
|
|
|
// Removes selection if some display item is selected.
|
|
unsetSelected: function() {
|
|
this._selectIndex(-1);
|
|
},
|
|
|
|
// Hides the preview if any item has one being displayed.
|
|
hidePreview: function() {
|
|
for (itemId in this._displayedItems) {
|
|
let item = this._displayedItems[itemId];
|
|
item.hidePreview();
|
|
}
|
|
},
|
|
|
|
// Returns true if the display has any displayed items.
|
|
hasItems: function() {
|
|
return this._displayedItemsCount > 0;
|
|
},
|
|
|
|
// Readjusts display layout and the items displayed based on the new dimensions.
|
|
setExpanded: function(expanded, baseWidth, expandWidth, height, numberOfColumns) {
|
|
this._expanded = expanded;
|
|
this._numberOfColumns = numberOfColumns;
|
|
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.
|
|
show: function() {
|
|
this._keepDisplayCurrent = true;
|
|
this._redisplay(true);
|
|
this._grid.show();
|
|
},
|
|
|
|
// 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 ////
|
|
|
|
/*
|
|
* Displays items that match the current request and should show up on the current page.
|
|
* Updates the display control to reflect the matched items set and the page selected.
|
|
*
|
|
* resetDisplayControl - indicates if the display control should be re-created because
|
|
* the results or the space allocated for them changed. If it's false,
|
|
* the existing display control is used and only the page links are
|
|
* updated to reflect the current page selection.
|
|
*/
|
|
_displayMatchedItems: function(resetDisplayControl) {
|
|
// When generating a new list to display, we first remove all the old
|
|
// displayed items which will unset the selection. So we need
|
|
// to keep a flag which indicates if this display had the selection.
|
|
let hadSelected = this.hasSelected();
|
|
|
|
this._removeAllDisplayItems();
|
|
|
|
for (let i = this._maxItemsPerPage * this._pageDisplayed; i < this._matchedItems.length && i < this._maxItemsPerPage * (this._pageDisplayed + 1); i++) {
|
|
|
|
this._addDisplayItem(this._matchedItems[i]);
|
|
}
|
|
|
|
if (hadSelected) {
|
|
this._selectedIndex = -1;
|
|
this.selectFirstItem();
|
|
}
|
|
|
|
this._updateDisplayControl(resetDisplayControl);
|
|
|
|
// We currently redisplay matching items and raise the sideshow as part of two different callbacks.
|
|
// Checking what is under the pointer after a timeout allows us to not merge these callbacks into one, at least for now.
|
|
Mainloop.timeout_add(5,
|
|
Lang.bind(this,
|
|
function() {
|
|
// Check if the pointer is over one of the items and display the preview pop-up if it is.
|
|
let [child, x, y, mask] = Gdk.Screen.get_default().get_root_window().get_pointer();
|
|
let global = Shell.Global.get();
|
|
let actor = global.stage.get_actor_at_pos(x, y);
|
|
if (actor != null) {
|
|
let item = this._findDisplayedByActor(actor.get_parent());
|
|
if (item != null) {
|
|
item.onDrawnUnderPointer();
|
|
}
|
|
}
|
|
return false;
|
|
}));
|
|
},
|
|
|
|
// Creates a display item based on the information associated with itemId
|
|
// and adds it to the displayed items.
|
|
_addDisplayItem : function(itemId) {
|
|
if (this._displayedItems.hasOwnProperty(itemId)) {
|
|
log("Tried adding a display item for " + itemId + ", but an item with this item id is already among displayed items.");
|
|
return;
|
|
}
|
|
|
|
let itemInfo = this._allItems[itemId];
|
|
let displayItem = this._createDisplayItem(itemInfo);
|
|
displayItem.setShowPreview(true);
|
|
|
|
displayItem.connect('activate',
|
|
Lang.bind(this,
|
|
function() {
|
|
// update the selection
|
|
this._selectIndex(this._getIndexOfDisplayedActor(displayItem.actor));
|
|
this.activateSelected();
|
|
}));
|
|
|
|
displayItem.connect('select',
|
|
Lang.bind(this,
|
|
function() {
|
|
// update the selection
|
|
this._selectIndex(this._getIndexOfDisplayedActor(displayItem.actor));
|
|
}));
|
|
this._grid.add_actor(displayItem.actor);
|
|
this._displayedItems[itemId] = displayItem;
|
|
this._displayedItemsCount++;
|
|
},
|
|
|
|
// Removes an item identifed by the itemId from the displayed items.
|
|
_removeDisplayItem: function(itemId) {
|
|
let displayItem = this._displayedItems[itemId];
|
|
let displayItemIndex = this._getIndexOfDisplayedActor(displayItem.actor);
|
|
|
|
if (this.hasSelected() && this._displayedItemsCount == 1) {
|
|
this.unsetSelected();
|
|
} else if (this.hasSelected() && displayItemIndex < this._selectedIndex) {
|
|
this.selectUp();
|
|
}
|
|
|
|
if (displayItem.dragActor) {
|
|
// The user might be handling a dragActor when the list of items
|
|
// changes (for example, if the dragging caused us to transition
|
|
// from an expanded overlay view to the regular view). So we need
|
|
// to keep the item around so that the drag and drop action initiated
|
|
// by the user can be completed. However, we remove the item from the list.
|
|
//
|
|
// For some reason, just removing the displayItem.actor
|
|
// is not enough to get displayItem._icon.visible
|
|
// to return false, so we hide the display item and
|
|
// all its children first. (We check displayItem._icon.visible
|
|
// when deciding if a dragActor has a place to snap back to
|
|
// in case the drop was not accepted by any actor.)
|
|
displayItem.actor.hide_all();
|
|
this._grid.remove_actor(displayItem.actor);
|
|
// We should not destroy the item up-front, because that would also
|
|
// destroy the icon that was used to clone the image for the drag actor.
|
|
// We destroy it once the dragActor is destroyed instead.
|
|
displayItem.dragActor.connect('destroy',
|
|
function(item) {
|
|
displayItem.destroy();
|
|
});
|
|
|
|
} else {
|
|
displayItem.destroy();
|
|
}
|
|
delete this._displayedItems[itemId];
|
|
this._displayedItemsCount--;
|
|
},
|
|
|
|
// Removes all displayed items.
|
|
_removeAllDisplayItems: function() {
|
|
for (itemId in this._displayedItems)
|
|
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
|
|
* their own while the user was browsing through the result pages.
|
|
*/
|
|
_redisplay: function(resetPage) {
|
|
if (!this._keepDisplayCurrent)
|
|
return;
|
|
|
|
this._refreshCache();
|
|
if (!this._filterActive())
|
|
this._setDefaultList();
|
|
else
|
|
this._doSearchFilter();
|
|
|
|
if (resetPage)
|
|
this._pageDisplayed = 0;
|
|
|
|
this._displayMatchedItems(true);
|
|
|
|
this.emit('redisplayed');
|
|
},
|
|
|
|
//// Pure virtual protected methods ////
|
|
|
|
// Performs the steps needed to have the latest information about the items.
|
|
_refreshCache: function() {
|
|
throw new Error("Not implemented");
|
|
},
|
|
|
|
// Sets the list of the displayed items based on the default sorting order.
|
|
// The default sorting order is specific to each implementing class.
|
|
_setDefaultList: function() {
|
|
throw new Error("Not implemented");
|
|
},
|
|
|
|
// Compares items associated with the item ids based on the order in which the
|
|
// items should be displayed.
|
|
// Intended to be used as a compareFunction for array.sort().
|
|
// Returns an integer value indicating the result of the comparison.
|
|
_compareItems: function(itemIdA, itemIdB) {
|
|
throw new Error("Not implemented");
|
|
},
|
|
|
|
// Checks if the item info can be a match for the search string.
|
|
// Returns a boolean flag indicating if that's the case.
|
|
_isInfoMatching: function(itemInfo, search) {
|
|
throw new Error("Not implemented");
|
|
},
|
|
|
|
// Creates a display item based on itemInfo.
|
|
_createDisplayItem: function(itemInfo) {
|
|
throw new Error("Not implemented");
|
|
},
|
|
|
|
//// Private methods ////
|
|
|
|
// 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(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;
|
|
},
|
|
|
|
_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.
|
|
let terms = this._search.split(/\s+/);
|
|
for (let i = 0; i < terms.length; i++) {
|
|
let term = terms[i];
|
|
for (itemId in this._allItems) {
|
|
let item = this._allItems[itemId];
|
|
if (this._isInfoMatching(item, term)) {
|
|
let count = matchedItemsForSearch[itemId];
|
|
if (!count)
|
|
count = 0;
|
|
count += 1;
|
|
matchedItemsForSearch[itemId] = count;
|
|
}
|
|
}
|
|
}
|
|
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) {
|
|
this._matchedItems.push(itemId);
|
|
}
|
|
this._matchedItems.sort(Lang.bind(this, function (a, b) {
|
|
let countA = matchedItemsForSearch[a];
|
|
let countB = matchedItemsForSearch[b];
|
|
if (countA > countB)
|
|
return -1;
|
|
else if (countA < countB)
|
|
return 1;
|
|
else
|
|
return this._compareItems(a, b);
|
|
}));
|
|
},
|
|
|
|
// Displays the page specified by the pageNumber argument. The pageNumber is 0-based.
|
|
_displayPage: function(pageNumber) {
|
|
this._pageDisplayed = pageNumber;
|
|
this._displayMatchedItems(false);
|
|
},
|
|
|
|
/*
|
|
* Updates the display control to reflect the matched items set and the page selected.
|
|
*
|
|
* resetDisplayControl - indicates if the display control should be re-created because
|
|
* the results or the space allocated for them changed. If it's false,
|
|
* the existing display control is used and only the page links are
|
|
* updated to reflect the current page selection.
|
|
*/
|
|
_updateDisplayControl: function(resetDisplayControl) {
|
|
if (resetDisplayControl) {
|
|
this.displayControl.remove_all();
|
|
let pageNumber = 0;
|
|
for (let i = 0; i < this._matchedItems.length; i = i + this._maxItemsPerPage) {
|
|
let pageControl = new Link.Link({ color: (pageNumber == this._pageDisplayed) ? DISPLAY_CONTROL_SELECTED_COLOR : ITEM_DISPLAY_DESCRIPTION_COLOR,
|
|
font_name: "Sans Bold 16px",
|
|
text: (pageNumber + 1) + "",
|
|
height: LABEL_HEIGHT,
|
|
reactive: (pageNumber == this._pageDisplayed) ? false : true});
|
|
this.displayControl.append(pageControl.actor, Big.BoxPackFlags.NONE);
|
|
|
|
// we use pageNumberLocalScope to get the page number right in the callback function
|
|
let pageNumberLocalScope = pageNumber;
|
|
pageControl.connect('clicked',
|
|
Lang.bind(this,
|
|
function(o, event) {
|
|
this._displayPage(pageNumberLocalScope);
|
|
}));
|
|
pageNumber ++;
|
|
}
|
|
} else {
|
|
let pageControlActors = this.displayControl.get_children();
|
|
for (let i = 0; i < pageControlActors.length; i++) {
|
|
let pageControlActor = pageControlActors[i];
|
|
if (i == this._pageDisplayed) {
|
|
pageControlActor.color = DISPLAY_CONTROL_SELECTED_COLOR;
|
|
pageControlActor.reactive = false;
|
|
} else {
|
|
pageControlActor.color = ITEM_DISPLAY_DESCRIPTION_COLOR;
|
|
pageControlActor.reactive = true;
|
|
}
|
|
}
|
|
}
|
|
},
|
|
|
|
// Returns a display item based on its index in the ordering of the
|
|
// display children.
|
|
_findDisplayedByIndex: function(index) {
|
|
let displayedActors = this._grid.get_children();
|
|
let actor = displayedActors[index];
|
|
return this._findDisplayedByActor(actor);
|
|
},
|
|
|
|
// Returns a display item based on the actor that represents it in
|
|
// the display.
|
|
_findDisplayedByActor: function(actor) {
|
|
for (itemId in this._displayedItems) {
|
|
let item = this._displayedItems[itemId];
|
|
if (item.actor == actor) {
|
|
return item;
|
|
}
|
|
}
|
|
return null;
|
|
},
|
|
|
|
// Returns and index that the actor has in the ordering of the display's
|
|
// children.
|
|
_getIndexOfDisplayedActor: function(actor) {
|
|
let children = this._grid.get_children();
|
|
for (let i = 0; i < children.length; i++) {
|
|
if (children[i] == actor)
|
|
return i;
|
|
}
|
|
return -1;
|
|
},
|
|
|
|
// Selects (e.g. highlights) a display item at the provided index,
|
|
// updates this.selectedItemDetails actor, and emits 'selected' signal.
|
|
_selectIndex: function(index) {
|
|
if (this._selectedIndex != -1) {
|
|
let prev = this._findDisplayedByIndex(this._selectedIndex);
|
|
prev.markSelected(false);
|
|
this.selectedItemDetails.remove_all();
|
|
}
|
|
this._selectedIndex = index;
|
|
if (index != -1 && index < this._displayedItemsCount) {
|
|
let item = this._findDisplayedByIndex(index);
|
|
item.markSelected(true);
|
|
this.selectedItemDetails.append(item.createDetailsActor(this._availableWidthForItemDetails), Big.BoxPackFlags.NONE);
|
|
this.emit('selected');
|
|
}
|
|
}
|
|
};
|
|
|
|
Signals.addSignalMethods(GenericDisplay.prototype);
|