// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- const Lang = imports.lang; const Clutter = imports.gi.Clutter; const St = imports.gi.St; const Meta = imports.gi.Meta; const Pango = imports.gi.Pango; const Cinnamon = imports.gi.Cinnamon; const Signals = imports.signals; const Mainloop = imports.mainloop; const AppSwitcher = imports.ui.appSwitcher.appSwitcher; const Main = imports.ui.main; const Tweener = imports.ui.tweener; const WindowUtils = imports.misc.windowUtils; const POPUP_SCROLL_TIME = 0.10; // seconds const POPUP_DELAY_TIMEOUT = 150; // milliseconds const POPUP_FADE_OUT_TIME = 0.1; // seconds const APP_ICON_HOVER_TIMEOUT = 200; // milliseconds const THUMBNAIL_DEFAULT_SIZE = 256; const THUMBNAIL_POPUP_TIME = 0; // milliseconds const THUMBNAIL_FADE_TIME = 0.1; // seconds const PREVIEW_DELAY_TIMEOUT = 0; // milliseconds var PREVIEW_SWITCHER_FADEOUT_TIME = 0.2; // seconds const iconSizes = [96, 64, 48]; function mod(a, b) { return (a + b) % b; } function ClassicSwitcher() { this._init.apply(this, arguments); } ClassicSwitcher.prototype = { __proto__: AppSwitcher.AppSwitcher.prototype, _init: function() { AppSwitcher.AppSwitcher.prototype._init.apply(this, arguments); this.actor = new Cinnamon.GenericContainer({ name: 'altTabPopup', reactive: true, visible: false }); this._thumbnailTimeoutId = 0; this.thumbnailsVisible = false; this._displayPreviewTimeoutId = 0; Main.uiGroup.add_actor(this.actor); if (!this._setupModal()) return; let styleSettings = global.settings.get_string("alttab-switcher-style"); let features = styleSettings.split('+'); this._iconsEnabled = features.indexOf('icons') !== -1; this._previewEnabled = features.indexOf('preview') !== -1; this._thumbnailsEnabled = features.indexOf('thumbnails') !== -1; if (!this._iconsEnabled && !this._previewEnabled && !this._thumbnailsEnabled) this._iconsEnabled = true; this._showThumbnails = this._thumbnailsEnabled && !this._iconsEnabled; this._showArrows = this._thumbnailsEnabled && this._iconsEnabled; this._updateList(0); this.actor.connect('get-preferred-width', Lang.bind(this, this._getPreferredWidth)); this.actor.connect('get-preferred-height', Lang.bind(this, this._getPreferredHeight)); this.actor.connect('allocate', Lang.bind(this, this._allocate)); this._applist_act_id = 0; this._applist_enter_id = 0; // Need to force an allocation so we can figure out whether we // need to scroll when selecting this.actor.opacity = 0; this.actor.show(); this.actor.get_allocation_box(); }, _getPreferredWidth: function (actor, forHeight, alloc) { alloc.min_size = global.screen_width; alloc.natural_size = global.screen_width; }, _getPreferredHeight: function (actor, forWidth, alloc) { alloc.min_size = global.screen_height; alloc.natural_size = global.screen_height; }, _allocate: function (actor, box, flags) { let childBox = new Clutter.ActorBox(); let monitor = this._activeMonitor; let leftPadding = this.actor.get_theme_node().get_padding(St.Side.LEFT); let rightPadding = this.actor.get_theme_node().get_padding(St.Side.RIGHT); let bottomPadding = this.actor.get_theme_node().get_padding(St.Side.BOTTOM); let vPadding = this.actor.get_theme_node().get_vertical_padding(); let hPadding = leftPadding + rightPadding; // Allocate the appSwitcher // We select a size based on an icon size that does not overflow the screen let [childMinHeight, childNaturalHeight] = this._appList.actor.get_preferred_height(monitor.width - hPadding); let [childMinWidth, childNaturalWidth] = this._appList.actor.get_preferred_width(childNaturalHeight); childBox.x1 = Math.max(monitor.x + leftPadding, monitor.x + Math.floor((monitor.width - childNaturalWidth) / 2)); childBox.x2 = Math.min(monitor.x + monitor.width - rightPadding, childBox.x1 + childNaturalWidth); childBox.y1 = monitor.y + Math.floor((monitor.height - childNaturalHeight) / 2); childBox.y2 = childBox.y1 + childNaturalHeight; this._appList.actor.allocate(childBox, flags); // Allocate the thumbnails // We try to avoid overflowing the screen so we base the resulting size on // those calculations if (this._thumbnails && this._appIcons.length > 0) { let icon = this._appIcons[this._currentIndex].actor; let [posX, posY] = icon.get_transformed_position(); let thumbnailCenter = posX + icon.width / 2; let [childMinWidth, childNaturalWidth] = this._thumbnails.actor.get_preferred_width(-1); childBox.x1 = Math.max(monitor.x + leftPadding, Math.floor(thumbnailCenter - childNaturalWidth / 2)); if (childBox.x1 + childNaturalWidth > monitor.x + monitor.width - rightPadding) { let offset = (childBox.x1 + childNaturalWidth) - (monitor.x + monitor.width - rightPadding); childBox.x1 -= offset; } let spacing = this.actor.get_theme_node().get_length('spacing'); childBox.x2 = childBox.x1 + childNaturalWidth; childBox.y1 = this._appList.actor.allocation.y2 + spacing; this._thumbnails.addClones(monitor.y + monitor.height - bottomPadding - childBox.y1); let [childMinHeight, childNaturalHeight] = this._thumbnails.actor.get_preferred_height(-1); childBox.y2 = childBox.y1 + childNaturalHeight; this._thumbnails.actor.allocate(childBox, flags); } }, _show: function() { Main.panelManager.panels.forEach(function(panel) { panel.actor.set_reactive(false); }); this.actor.opacity = 255; this._initialDelayTimeoutId = 0; this._next(); }, _hide: function() { // window title and icon if(this._windowTitle) { this._windowTitle.hide(); this._applicationIconBox.hide(); } // panels Main.panelManager.panels.forEach(function(panel) { panel.actor.set_reactive(true); }); Tweener.addTween(this.actor, { opacity: 0, time: POPUP_FADE_OUT_TIME, transition: 'easeOutQuad', onComplete: Lang.bind(this, this._destroyActors) }); }, _destroyActors: function() { Main.uiGroup.remove_actor(this.actor); this.actor.destroy(); }, _updateList: function(direction) { if(direction !== 0) return; if (this._appList) { if (this._applist_act_id !== 0) { this._appList.disconnect(this._applist_act_id); this._applist_act_id = 0; } if (this._applist_enter_id !== 0) { this._appList.disconnect(this._applist_enter_id); this._applist_enter_id = 0; } this._clearPreview(); this._destroyThumbnails(); this.actor.remove_actor(this._appList.actor); this._appList.actor.destroy(); } this._appList = new AppList(this._windows, this._showThumbnails, this._showArrows, this._activeMonitor); this.actor.add_actor(this._appList.actor); if (!this._iconsEnabled && !this._thumbnailsEnabled) { this._appList.actor.hide(); } this._applist_act_id = this._appList.connect('item-activated', Lang.bind(this, this._appActivated)); this._applist_enter_id = this._appList.connect('item-entered', Lang.bind(this, this._appEntered)); this._appIcons = this._appList.icons; this.actor.get_allocation_box(); }, _selectNext: function() { if (this._currentIndex == this._windows.length - 1) { this._currentIndex = 0; } else { this._currentIndex = this._currentIndex + 1; } }, _selectPrevious: function() { if (this._currentIndex == 0) { this._currentIndex = this._windows.length-1; } else { this._currentIndex = this._currentIndex - 1; } }, _onWorkspaceSelected: function() { this._windows = AppSwitcher.getWindowsForBinding(this._binding); this._currentIndex = 0; this._updateList(0); this._select(0); }, _setCurrentWindow: function(window) { this._appList.highlight(this._currentIndex, false); this._doWindowPreview(); this._destroyThumbnails(); if (this._thumbnailTimeoutId != 0) { Mainloop.source_remove(this._thumbnailTimeoutId); this._thumbnailTimeoutId = 0; } if (this._showArrows) { this._thumbnailTimeoutId = Mainloop.timeout_add( THUMBNAIL_POPUP_TIME, Lang.bind(this, function() { if (!this._thumbnails) this._createThumbnails(); this._thumbnails.highlight(0, false); this._thumbnailTimeoutId = 0; })); } }, _onDestroy: function() { if (this._appList !== null) { if (this._applist_act_id > 0) { this._appList.disconnect(this._applist_act_id); this._applist_act_id = 0; } if (this._applist_enter_id > 0) { this._appList.disconnect(this._applist_enter_id); this._applist_enter_id = 0; } } if (this._thumbnailTimeoutId != 0) { Mainloop.source_remove(this._thumbnailTimeoutId); this._thumbnailTimeoutId = 0; } if (this._displayPreviewTimeoutId != 0) { Mainloop.source_remove(this._displayPreviewTimeoutId); this._displayPreviewTimeoutId = 0; } }, _appActivated : function(appSwitcher, n) { this._activateSelected(); }, _appEntered : function(appSwitcher, n) { if (!this._mouseActive) return; this._select(n); }, _windowActivated : function(thumbnailList, n) { this._activateSelected(); }, _clearPreview: function() { if (this._previewClones) { for (let i = 0; i < this._previewClones.length; ++i) { let clone = this._previewClones[i]; Tweener.addTween(clone, { opacity: 0, time: PREVIEW_SWITCHER_FADEOUT_TIME / 4, transition: 'linear', onCompleteScope: this, onComplete: function() { this.actor.remove_actor(clone); clone.destroy(); } }); } this._previewClones = null; } }, _doWindowPreview: function() { if (!this._previewEnabled || this._windows.length < 1) { return; } // Use a cancellable timeout to avoid flickering effect when tabbing rapidly through the set. if (this._displayPreviewTimeoutId) { Mainloop.source_remove(this._displayPreviewTimeoutId); this._displayPreviewTimeoutId = 0; } let delay = PREVIEW_DELAY_TIMEOUT; this._displayPreviewTimeoutId = Mainloop.timeout_add(delay, Lang.bind(this, this._showWindowPreview)); }, _showWindowPreview: function() { this._displayPreviewTimeoutId = 0; let childBox = new Clutter.ActorBox(); let lastClone = null; let previewClones = []; let window = this._windows[this._currentIndex]; let clones = WindowUtils.createWindowClone(window, 0, 0, true, false); for (let i = 0; i < clones.length; i++) { let clone = clones[i]; previewClones.push(clone.actor); this.actor.add_actor(clone.actor); let [width, height] = clone.actor.get_size(); childBox.x1 = clone.x; childBox.x2 = clone.x + width; childBox.y1 = clone.y; childBox.y2 = clone.y + height; clone.actor.allocate(childBox, 0); clone.actor.lower(this._appList.actor); if (lastClone) { lastClone.lower(clone.actor); } lastClone = clone.actor; } this._clearPreview(); this._previewClones = previewClones; if (!this._previewBackdrop) { let backdrop = this._previewBackdrop = new St.Bin({style_class: 'switcher-preview-backdrop'}); this.actor.add_actor(backdrop); // Make sure that the backdrop does not overlap the switcher. backdrop.lower(this._appList.actor); backdrop.lower(lastClone); childBox.x1 = this.actor.x; childBox.x2 = this.actor.x + this.actor.width; childBox.y1 = this.actor.y; childBox.y2 = this.actor.y + this.actor.height; backdrop.allocate(childBox, 0); backdrop.opacity = 0; Tweener.addTween(backdrop, { opacity: 255, time: PREVIEW_SWITCHER_FADEOUT_TIME / 4, transition: 'linear' }); } }, _destroyThumbnails : function() { if (!this._thumbnails) { return; } let thumbnailsActor = this._thumbnails.actor; this._thumbnails = null; this.actor.remove_actor(thumbnailsActor); thumbnailsActor.destroy(); this.thumbnailsVisible = false; }, _createThumbnails : function() { this._thumbnails = new ThumbnailList ([this._windows[this._currentIndex]], this._activeMonitor); this._thumbnails.connect('item-activated', Lang.bind(this, this._windowActivated)); this.actor.add_actor(this._thumbnails.actor); // Need to force an allocation so we can figure out whether we // need to scroll when selecting this._thumbnails.actor.get_allocation_box(); this._thumbnails.actor.opacity = 0; Tweener.addTween(this._thumbnails.actor, { opacity: 255, time: THUMBNAIL_FADE_TIME, transition: 'easeOutQuad', onComplete: Lang.bind(this, function () { this.thumbnailsVisible = true; }) }); } }; function AppIcon(window, showThumbnail) { this._init(window, showThumbnail); } AppIcon.prototype = { _init: function(window, showThumbnail) { this.window = window; this.showThumbnail = showThumbnail; let tracker = Cinnamon.WindowTracker.get_default(); this.app = tracker.get_window_app(window); this.actor = new St.BoxLayout({ style_class: 'alt-tab-app', vertical: true }); this.icon = null; this._iconBin = new St.Bin(); this.actor.add(this._iconBin, { x_fill: false, y_fill: false } ); let title = window.get_title(); if (title) { if (window.minimized) { this.label = new St.Label({ text: "[" + title + "]"}); let contrast_effect = new Clutter.BrightnessContrastEffect(); contrast_effect.set_brightness_full(-0.5, -0.5, -0.5); this._iconBin.add_effect(contrast_effect); } else { this.label = new St.Label({ text: title }); } let bin = new St.Bin({ x_align: St.Align.MIDDLE }); bin.add_actor(this.label); this.actor.add(bin); } else { this.label = new St.Label({ text: this.app ? this.app.get_name() : window.title }); this.actor.add(this.label, { x_fill: false }); } }, set_size: function(size) { if (this.showThumbnail){ this.icon = new St.Widget(); let clones = WindowUtils.createWindowClone(this.window, size * global.ui_scale, size * global.ui_scale, true, true); for (let i in clones) { let clone = clones[i]; this.icon.add_actor(clone.actor); // the following 2 lines are used when cloning without positions (param #4 = false) //let [width, height] = clone.actor.get_size(); //clone.actor.set_position(Math.round((size - width) / 2), Math.round((size - height) / 2)); clone.actor.set_position(clone.x, clone.y); } } else { this.icon = this.app ? this.app.create_icon_texture_for_window(size, this.window) : new St.Icon({ icon_name: 'application-default-icon', icon_type: St.IconType.FULLCOLOR, icon_size: size }); } size *= global.ui_scale; this._iconBin.set_size(size, size); this._iconBin.child = this.icon; } }; function SwitcherList(squareItems, activeMonitor) { this._init(squareItems, activeMonitor); } SwitcherList.prototype = { _init : function(squareItems, activeMonitor) { this.actor = new Cinnamon.GenericContainer({ style_class: 'switcher-list' }); this.actor.connect('get-preferred-width', Lang.bind(this, this._getPreferredWidth)); this.actor.connect('get-preferred-height', Lang.bind(this, this._getPreferredHeight)); this.actor.connect('allocate', Lang.bind(this, this._allocateTop)); // Here we use a GenericContainer so that we can force all the // children except the separator to have the same width. // TODO: Separator is gone, we could use an St.ScrollView now. this._list = new Cinnamon.GenericContainer({ style_class: 'switcher-list-item-container' }); this._list.spacing = -1; this._list.connect('style-changed', Lang.bind(this, function() { this._list.spacing = this._list.get_theme_node().get_length('spacing'); })); this._list.connect('get-preferred-width', Lang.bind(this, this._getPreferredWidth)); this._list.connect('get-preferred-height', Lang.bind(this, this._getPreferredHeight)); this._list.connect('allocate', Lang.bind(this, this._allocate)); this._clipBin = new St.Bin({style_class: 'cbin'}); this._clipBin.child = this._list; this.actor.add_actor(this._clipBin); this._leftGradient = new St.BoxLayout({style_class: 'thumbnail-scroll-gradient-left', vertical: true}); this._rightGradient = new St.BoxLayout({style_class: 'thumbnail-scroll-gradient-right', vertical: true}); this.actor.add_actor(this._leftGradient); this.actor.add_actor(this._rightGradient); // Those arrows indicate whether scrolling in one direction is possible this._leftArrow = new St.DrawingArea({ style_class: 'switcher-arrow', pseudo_class: 'highlighted' }); this._leftArrow.connect('repaint', Lang.bind(this, function() { _drawArrow(this._leftArrow, St.Side.LEFT); })); this._rightArrow = new St.DrawingArea({ style_class: 'switcher-arrow', pseudo_class: 'highlighted' }); this._rightArrow.connect('repaint', Lang.bind(this, function() { _drawArrow(this._rightArrow, St.Side.RIGHT); })); this.actor.add_actor(this._leftArrow); this.actor.add_actor(this._rightArrow); this._items = []; this._highlighted = -1; this._squareItems = squareItems; this._minSize = 0; this._scrollableRight = true; this._scrollableLeft = false; this._activeMonitor = activeMonitor; }, _allocateTop: function(actor, box, flags) { if (this._list.spacing === -1) { this._list.spacing = this._list.get_theme_node().get_length('spacing'); } let leftPadding = this.actor.get_theme_node().get_padding(St.Side.LEFT); let rightPadding = this.actor.get_theme_node().get_padding(St.Side.RIGHT); let childBox = new Clutter.ActorBox(); let scrollable = this._minSize > box.x2 - box.x1; this._clipBin.allocate(box, flags); childBox.x1 = 0; childBox.y1 = 0; childBox.x2 = this._leftGradient.width; childBox.y2 = this.actor.height; this._leftGradient.allocate(childBox, flags); this._leftGradient.opacity = (this._scrollableLeft && scrollable) ? 255 : 0; childBox.x1 = (this.actor.allocation.x2 - this.actor.allocation.x1) - this._rightGradient.width; childBox.y1 = 0; childBox.x2 = childBox.x1 + this._rightGradient.width; childBox.y2 = this.actor.height; this._rightGradient.allocate(childBox, flags); this._rightGradient.opacity = (this._scrollableRight && scrollable) ? 255 : 0; let arrowWidth = Math.floor(leftPadding / 3); let arrowHeight = arrowWidth * 2; childBox.x1 = leftPadding / 2; childBox.y1 = this.actor.height / 2 - arrowWidth; childBox.x2 = childBox.x1 + arrowWidth; childBox.y2 = childBox.y1 + arrowHeight; this._leftArrow.allocate(childBox, flags); this._leftArrow.opacity = this._leftGradient.opacity; arrowWidth = Math.floor(rightPadding / 3); arrowHeight = arrowWidth * 2; childBox.x1 = this.actor.width - arrowWidth - rightPadding / 2; childBox.y1 = this.actor.height / 2 - arrowWidth; childBox.x2 = childBox.x1 + arrowWidth; childBox.y2 = childBox.y1 + arrowHeight; this._rightArrow.allocate(childBox, flags); this._rightArrow.opacity = this._rightGradient.opacity; }, addItem : function(item, label) { let bbox = new St.Button({ style_class: 'item-box', reactive: true }); bbox.set_child(item); this._list.add_actor(bbox); let n = this._items.length; bbox.connect('clicked', Lang.bind(this, function() { this._onItemClicked(n); })); bbox.connect('enter-event', Lang.bind(this, function() { this._onItemEnter(n); })); bbox.label_actor = label; this._items.push(bbox); }, _onItemClicked: function (index) { this._itemActivated(index); }, _onItemEnter: function (index) { this._itemEntered(index); }, highlight: function(index, justOutline) { if (this._highlighted != -1) { this._items[this._highlighted].remove_style_pseudo_class('outlined'); this._items[this._highlighted].remove_style_pseudo_class('selected'); } this._highlighted = index; if (this._highlighted != -1) { if (justOutline) this._items[this._highlighted].add_style_pseudo_class('outlined'); else this._items[this._highlighted].add_style_pseudo_class('selected'); } let [absItemX, absItemY] = this._items[index].get_transformed_position(); let [result, posX, posY] = this.actor.transform_stage_point(absItemX, 0); let [containerWidth, containerHeight] = this.actor.get_transformed_size(); if (posX + this._items[index].get_width() > containerWidth) this._scrollToRight(); else if (posX < 0) this._scrollToLeft(); }, _scrollToLeft : function() { let x = this._items[this._highlighted].allocation.x1; this._scrollableRight = true; Tweener.addTween(this._list, { anchor_x: x, time: POPUP_SCROLL_TIME, transition: 'easeOutQuad', onComplete: Lang.bind(this, function () { if (this._highlighted == 0) { this._scrollableLeft = false; this.actor.queue_relayout(); } }) }); }, _scrollToRight : function() { this._scrollableLeft = true; let monitor = this._activeMonitor; let padding = this.actor.get_theme_node().get_horizontal_padding(); let parentPadding = this.actor.get_parent().get_theme_node().get_horizontal_padding(); let x = this._items[this._highlighted].allocation.x2 - monitor.width + padding + parentPadding; Tweener.addTween(this._list, { anchor_x: x, time: POPUP_SCROLL_TIME, transition: 'easeOutQuad', onComplete: Lang.bind(this, function () { if (this._highlighted == this._items.length - 1) { this._scrollableRight = false; this.actor.queue_relayout(); } }) }); }, _itemActivated: function(n) { this.emit('item-activated', n); }, _itemEntered: function(n) { this.emit('item-entered', n); }, _maxChildWidth: function () { let maxChildMin = 0; let maxChildNat = 0; if (this._items.length > 0) { return this._items[0].get_preferred_width(-1); } return [0, 0] }, _getPreferredWidth: function (actor, forHeight, alloc) { let [maxChildMin, maxChildNat] = this._maxChildWidth(); let totalSpacing = this._list.spacing * Math.max(1, (this._items.length - 1)); alloc.min_size = this._items.length * maxChildMin + totalSpacing; alloc.natural_size = alloc.min_size; this._minSize = alloc.min_size; }, _getPreferredHeight: function (actor, forWidth, alloc) { let maxChildMin = 0; let maxChildNat = 0; for (let i = 0; i < this._items.length; i++) { let [childMin, childNat] = this._items[i].get_preferred_height(-1); maxChildMin = Math.max(childMin, maxChildMin); maxChildNat = Math.max(childNat, maxChildNat); } if (this._squareItems) { let [childMin, childNat] = this._maxChildWidth(); maxChildMin = Math.max(childMin, maxChildMin); maxChildNat = maxChildMin; } alloc.min_size = maxChildMin; alloc.natural_size = maxChildNat; }, _allocate: function (actor, box, flags) { let childHeight = box.y2 - box.y1; let [maxChildMin, maxChildNat] = this._maxChildWidth(); let totalSpacing = this._list.spacing * (this._items.length - 1); let childWidth = Math.floor(Math.max(0, box.x2 - box.x1 - totalSpacing) / this._items.length); let x = 0; let children = this._list.get_children(); let childBox = new Clutter.ActorBox(); let monitor = this._activeMonitor; let parentRightPadding = this.actor.get_parent().get_theme_node().get_padding(St.Side.RIGHT); if (this.actor.allocation.x2 == monitor.x + monitor.width - parentRightPadding) { if (this._squareItems) childWidth = childHeight; else { let [childMin, childNat] = children[0].get_preferred_width(childHeight); childWidth = childMin; } } for (let i = 0; i < children.length; i++) { if (this._items.indexOf(children[i]) != -1) { let [childMin, childNat] = children[i].get_preferred_height(childWidth); let vSpacing = (childHeight - childNat) / 2; childBox.x1 = x; childBox.y1 = vSpacing; childBox.x2 = x + childWidth; childBox.y2 = childBox.y1 + childNat; children[i].allocate(childBox, flags); x += this._list.spacing + childWidth; } else { // Something else, eg, AppList's arrows; // we don't allocate it. } } let leftPadding = this.actor.get_theme_node().get_padding(St.Side.LEFT); let rightPadding = this.actor.get_theme_node().get_padding(St.Side.RIGHT); let topPadding = this.actor.get_theme_node().get_padding(St.Side.TOP); let bottomPadding = this.actor.get_theme_node().get_padding(St.Side.BOTTOM); // Clip the area for scrolling this._clipBin.set_clip(0, -topPadding, (this.actor.allocation.x2 - this.actor.allocation.x1) - leftPadding - rightPadding, this.actor.height + bottomPadding); } }; Signals.addSignalMethods(SwitcherList.prototype); function AppList() { this._init.apply(this, arguments); } AppList.prototype = { __proto__ : SwitcherList.prototype, _init : function(windows, showThumbnails, showArrows, activeMonitor) { SwitcherList.prototype._init.call(this, true, activeMonitor); // Construct the AppIcons, add to the popup let activeWorkspace = global.workspace_manager.get_active_workspace(); let workspaceIcons = []; let otherIcons = []; for (let i = 0; i < windows.length; i++) { workspaceIcons.push(new AppIcon(windows[i], showThumbnails)); } this.icons = []; this._arrows = []; for (let i = 0; i < workspaceIcons.length; i++) this._addIcon(workspaceIcons[i]); if (workspaceIcons.length > 0 && otherIcons.length > 0) this.addSeparator(); for (let i = 0; i < otherIcons.length; i++) this._addIcon(otherIcons[i]); this._curApp = -1; this._iconSize = 0; this._showArrows = showArrows; this._mouseTimeOutId = 0; this._activeMonitor = activeMonitor; }, _getPreferredHeight: function (actor, forWidth, alloc) { if (this._items.length < 1) { alloc.min_size = alloc.natural_size = 32; return; } let j = 0; while(this._items.length > 1 && this._items[j].style_class != 'item-box') { j++; } let themeNode = this._items[j].get_theme_node(); let iconPadding = themeNode.get_horizontal_padding(); let iconBorder = themeNode.get_border_width(St.Side.LEFT) + themeNode.get_border_width(St.Side.RIGHT); let [iconMinHeight, iconNaturalHeight] = this.icons[j].label.get_preferred_height(-1); let iconSpacing = iconNaturalHeight + iconPadding + iconBorder; let totalSpacing = this._list.spacing * (this._items.length - 1); // We just assume the whole screen here due to weirdness happing with the passed width let parentPadding = this.actor.get_parent().get_theme_node().get_horizontal_padding(); let availWidth = this._activeMonitor.width - parentPadding - this.actor.get_theme_node().get_horizontal_padding(); let height = 0; for(let i = 0; i < iconSizes.length; i++) { this._iconSize = iconSizes[i]; height = (iconSizes[i] * global.ui_scale) + iconSpacing; let w = height * this._items.length + totalSpacing; if (w <= availWidth) break; } if (this._items.length == 1) { this._iconSize = iconSizes[0]; height = (iconSizes[0] * global.ui_scale) + iconSpacing; } for(let i = 0; i < this.icons.length; i++) { if (this.icons[i].icon != null) break; this.icons[i].set_size(this._iconSize); } alloc.min_size = height; alloc.natural_size = height; }, _allocate: function (actor, box, flags) { // Allocate the main list items SwitcherList.prototype._allocate.call(this, actor, box, flags); if (this._showArrows) { let arrowHeight = Math.floor(this.actor.get_theme_node().get_padding(St.Side.BOTTOM) / 3); let arrowWidth = arrowHeight * 2; // Now allocate each arrow underneath its item let childBox = new Clutter.ActorBox(); for (let i = 0; i < this._items.length; i++) { let itemBox = this._items[i].allocation; childBox.x1 = Math.floor(itemBox.x1 + (itemBox.x2 - itemBox.x1 - arrowWidth) / 2); childBox.x2 = childBox.x1 + arrowWidth; childBox.y1 = itemBox.y2 + arrowHeight; childBox.y2 = childBox.y1 + arrowHeight; this._arrows[i].allocate(childBox, flags); } } }, // We override SwitcherList's _onItemEnter method to delay // activation when the thumbnail list is open _onItemEnter: function (index) { if (this._mouseTimeOutId != 0) Mainloop.source_remove(this._mouseTimeOutId); this._itemEntered(index); }, _enterItem: function(index) { let [x, y, mask] = global.get_pointer(); let pickedActor = global.stage.get_actor_at_pos(Clutter.PickMode.ALL, x, y); if (this._items[index].contains(pickedActor)) this._itemEntered(index); }, // We override SwitcherList's highlight() method to also deal with // the AppList->ThumbnailList arrows. highlight : function(n, justOutline) { if (this._curApp != -1) { this._arrows[this._curApp].hide(); } SwitcherList.prototype.highlight.call(this, n, justOutline); this._curApp = n; if (n != -1 && this._showArrows) { this._arrows[n].show(); } }, _addIcon : function(appIcon) { this.icons.push(appIcon); this.addItem(appIcon.actor, appIcon.label); let n = this._arrows.length; let arrow = new St.DrawingArea({ style_class: 'switcher-arrow' }); arrow.connect('repaint', function() { _drawArrow(arrow, St.Side.BOTTOM); }); this._list.add_actor(arrow); this._arrows.push(arrow); arrow.hide(); } }; function ThumbnailList(windows, activeMonitor) { this._init(windows, activeMonitor); } ThumbnailList.prototype = { __proto__ : SwitcherList.prototype, _init : function(windows, activeMonitor) { SwitcherList.prototype._init.call(this, false, activeMonitor); let activeWorkspace = global.workspace_manager.get_active_workspace(); this._labels = new Array(); this._thumbnailBins = new Array(); this._clones = new Array(); this._windows = windows; for (let i = 0; i < windows.length; i++) { let box = new St.BoxLayout({ style_class: 'thumbnail-box', vertical: true }); let bin = new St.Bin({ style_class: 'thumbnail' }); box.add_actor(bin); this._thumbnailBins.push(bin); let title = windows[i].get_title(); if (title) { let name = new St.Label({ text: title }); // St.Label doesn't support text-align so use a Bin let bin = new St.Bin({ x_align: St.Align.MIDDLE }); this._labels.push(bin); bin.add_actor(name); box.add_actor(bin); this.addItem(box, name); } else { this.addItem(box, null); } } }, addClones : function (availHeight) { if (!this._thumbnailBins.length) return; let totalPadding = this._items[0].get_theme_node().get_horizontal_padding() + this._items[0].get_theme_node().get_vertical_padding(); totalPadding += this.actor.get_theme_node().get_horizontal_padding() + this.actor.get_theme_node().get_vertical_padding(); let [labelMinHeight, labelNaturalHeight] = this._labels.length > 0 ? this._labels[0].get_preferred_height(-1) : [0, 0]; let spacing = this._items[0].child.get_theme_node().get_length('spacing'); availHeight = Math.min(availHeight - labelNaturalHeight - totalPadding - spacing, THUMBNAIL_DEFAULT_SIZE * global.ui_scale); let binHeight = availHeight + this._items[0].get_theme_node().get_vertical_padding() + this.actor.get_theme_node().get_vertical_padding() - spacing; binHeight = Math.min(THUMBNAIL_DEFAULT_SIZE * global.ui_scale, binHeight); for (let i = 0; i < this._thumbnailBins.length; i++) { let metaWindow = this._windows[i]; let container = new St.Widget(); let clones = WindowUtils.createWindowClone(metaWindow, availHeight, availHeight, true, true); for (let j = 0; j < clones.length; j++) { let clone = clones[j]; container.add_actor(clone.actor); clone.actor.set_position(clone.x, clone.y); } this._thumbnailBins[i].set_height(binHeight); this._thumbnailBins[i].add_actor(container); this._clones.push(container); } // Make sure we only do this once this._thumbnailBins = new Array(); } }; function _drawArrow(area, side) { let themeNode = area.get_theme_node(); let borderColor = themeNode.get_border_color(side); let bodyColor = themeNode.get_foreground_color(); let [width, height] = area.get_surface_size (); let cr = area.get_context(); cr.setLineWidth(1.0); Clutter.cairo_set_source_color(cr, borderColor); switch (side) { case St.Side.TOP: cr.moveTo(0, height); cr.lineTo(Math.floor(width * 0.5), 0); cr.lineTo(width, height); break; case St.Side.BOTTOM: cr.moveTo(width, 0); cr.lineTo(Math.floor(width * 0.5), height); cr.lineTo(0, 0); break; case St.Side.LEFT: cr.moveTo(width, height); cr.lineTo(0, Math.floor(height * 0.5)); cr.lineTo(width, 0); break; case St.Side.RIGHT: cr.moveTo(0, 0); cr.lineTo(width, Math.floor(height * 0.5)); cr.lineTo(0, height); break; } cr.strokePreserve(); Clutter.cairo_set_source_color(cr, bodyColor); cr.fill(); cr.$dispose(); }