classicSwitcher.js 38 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010
  1. // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
  2. const Lang = imports.lang;
  3. const Clutter = imports.gi.Clutter;
  4. const St = imports.gi.St;
  5. const Meta = imports.gi.Meta;
  6. const Pango = imports.gi.Pango;
  7. const Cinnamon = imports.gi.Cinnamon;
  8. const Signals = imports.signals;
  9. const Mainloop = imports.mainloop;
  10. const AppSwitcher = imports.ui.appSwitcher.appSwitcher;
  11. const Main = imports.ui.main;
  12. const Tweener = imports.ui.tweener;
  13. const WindowUtils = imports.misc.windowUtils;
  14. const POPUP_SCROLL_TIME = 0.10; // seconds
  15. const POPUP_DELAY_TIMEOUT = 150; // milliseconds
  16. const POPUP_FADE_OUT_TIME = 0.1; // seconds
  17. const APP_ICON_HOVER_TIMEOUT = 200; // milliseconds
  18. const THUMBNAIL_DEFAULT_SIZE = 256;
  19. const THUMBNAIL_POPUP_TIME = 0; // milliseconds
  20. const THUMBNAIL_FADE_TIME = 0.1; // seconds
  21. const PREVIEW_DELAY_TIMEOUT = 0; // milliseconds
  22. var PREVIEW_SWITCHER_FADEOUT_TIME = 0.2; // seconds
  23. const iconSizes = [96, 64, 48];
  24. function mod(a, b) {
  25. return (a + b) % b;
  26. }
  27. function ClassicSwitcher() {
  28. this._init.apply(this, arguments);
  29. }
  30. ClassicSwitcher.prototype = {
  31. __proto__: AppSwitcher.AppSwitcher.prototype,
  32. _init: function() {
  33. AppSwitcher.AppSwitcher.prototype._init.apply(this, arguments);
  34. this.actor = new Cinnamon.GenericContainer({ name: 'altTabPopup',
  35. reactive: true,
  36. visible: false });
  37. this._thumbnailTimeoutId = 0;
  38. this.thumbnailsVisible = false;
  39. this._displayPreviewTimeoutId = 0;
  40. Main.uiGroup.add_actor(this.actor);
  41. if (!this._setupModal())
  42. return;
  43. let styleSettings = global.settings.get_string("alttab-switcher-style");
  44. let features = styleSettings.split('+');
  45. this._iconsEnabled = features.indexOf('icons') !== -1;
  46. this._previewEnabled = features.indexOf('preview') !== -1;
  47. this._thumbnailsEnabled = features.indexOf('thumbnails') !== -1;
  48. if (!this._iconsEnabled && !this._previewEnabled && !this._thumbnailsEnabled)
  49. this._iconsEnabled = true;
  50. this._showThumbnails = this._thumbnailsEnabled && !this._iconsEnabled;
  51. this._showArrows = this._thumbnailsEnabled && this._iconsEnabled;
  52. this._updateList(0);
  53. this.actor.connect('get-preferred-width', Lang.bind(this, this._getPreferredWidth));
  54. this.actor.connect('get-preferred-height', Lang.bind(this, this._getPreferredHeight));
  55. this.actor.connect('allocate', Lang.bind(this, this._allocate));
  56. this._applist_act_id = 0;
  57. this._applist_enter_id = 0;
  58. // Need to force an allocation so we can figure out whether we
  59. // need to scroll when selecting
  60. this.actor.opacity = 0;
  61. this.actor.show();
  62. this.actor.get_allocation_box();
  63. },
  64. _getPreferredWidth: function (actor, forHeight, alloc) {
  65. alloc.min_size = global.screen_width;
  66. alloc.natural_size = global.screen_width;
  67. },
  68. _getPreferredHeight: function (actor, forWidth, alloc) {
  69. alloc.min_size = global.screen_height;
  70. alloc.natural_size = global.screen_height;
  71. },
  72. _allocate: function (actor, box, flags) {
  73. let childBox = new Clutter.ActorBox();
  74. let monitor = this._activeMonitor;
  75. let leftPadding = this.actor.get_theme_node().get_padding(St.Side.LEFT);
  76. let rightPadding = this.actor.get_theme_node().get_padding(St.Side.RIGHT);
  77. let bottomPadding = this.actor.get_theme_node().get_padding(St.Side.BOTTOM);
  78. let vPadding = this.actor.get_theme_node().get_vertical_padding();
  79. let hPadding = leftPadding + rightPadding;
  80. // Allocate the appSwitcher
  81. // We select a size based on an icon size that does not overflow the screen
  82. let [childMinHeight, childNaturalHeight] = this._appList.actor.get_preferred_height(monitor.width - hPadding);
  83. let [childMinWidth, childNaturalWidth] = this._appList.actor.get_preferred_width(childNaturalHeight);
  84. childBox.x1 = Math.max(monitor.x + leftPadding, monitor.x + Math.floor((monitor.width - childNaturalWidth) / 2));
  85. childBox.x2 = Math.min(monitor.x + monitor.width - rightPadding, childBox.x1 + childNaturalWidth);
  86. childBox.y1 = monitor.y + Math.floor((monitor.height - childNaturalHeight) / 2);
  87. childBox.y2 = childBox.y1 + childNaturalHeight;
  88. this._appList.actor.allocate(childBox, flags);
  89. // Allocate the thumbnails
  90. // We try to avoid overflowing the screen so we base the resulting size on
  91. // those calculations
  92. if (this._thumbnails && this._appIcons.length > 0) {
  93. let icon = this._appIcons[this._currentIndex].actor;
  94. let [posX, posY] = icon.get_transformed_position();
  95. let thumbnailCenter = posX + icon.width / 2;
  96. let [childMinWidth, childNaturalWidth] = this._thumbnails.actor.get_preferred_width(-1);
  97. childBox.x1 = Math.max(monitor.x + leftPadding, Math.floor(thumbnailCenter - childNaturalWidth / 2));
  98. if (childBox.x1 + childNaturalWidth > monitor.x + monitor.width - rightPadding) {
  99. let offset = (childBox.x1 + childNaturalWidth) - (monitor.x + monitor.width - rightPadding);
  100. childBox.x1 -= offset;
  101. }
  102. let spacing = this.actor.get_theme_node().get_length('spacing');
  103. childBox.x2 = childBox.x1 + childNaturalWidth;
  104. childBox.y1 = this._appList.actor.allocation.y2 + spacing;
  105. this._thumbnails.addClones(monitor.y + monitor.height - bottomPadding - childBox.y1);
  106. let [childMinHeight, childNaturalHeight] = this._thumbnails.actor.get_preferred_height(-1);
  107. childBox.y2 = childBox.y1 + childNaturalHeight;
  108. this._thumbnails.actor.allocate(childBox, flags);
  109. }
  110. },
  111. _show: function() {
  112. Main.panelManager.panels.forEach(function(panel) { panel.actor.set_reactive(false); });
  113. this.actor.opacity = 255;
  114. this._initialDelayTimeoutId = 0;
  115. this._next();
  116. },
  117. _hide: function() {
  118. // window title and icon
  119. if(this._windowTitle) {
  120. this._windowTitle.hide();
  121. this._applicationIconBox.hide();
  122. }
  123. // panels
  124. Main.panelManager.panels.forEach(function(panel) { panel.actor.set_reactive(true); });
  125. Tweener.addTween(this.actor, { opacity: 0,
  126. time: POPUP_FADE_OUT_TIME,
  127. transition: 'easeOutQuad',
  128. onComplete: Lang.bind(this, this._destroyActors)
  129. });
  130. },
  131. _destroyActors: function() {
  132. Main.uiGroup.remove_actor(this.actor);
  133. this.actor.destroy();
  134. },
  135. _updateList: function(direction) {
  136. if(direction !== 0)
  137. return;
  138. if (this._appList) {
  139. if (this._applist_act_id !== 0) {
  140. this._appList.disconnect(this._applist_act_id);
  141. this._applist_act_id = 0;
  142. }
  143. if (this._applist_enter_id !== 0) {
  144. this._appList.disconnect(this._applist_enter_id);
  145. this._applist_enter_id = 0;
  146. }
  147. this._clearPreview();
  148. this._destroyThumbnails();
  149. this.actor.remove_actor(this._appList.actor);
  150. this._appList.actor.destroy();
  151. }
  152. this._appList = new AppList(this._windows, this._showThumbnails, this._showArrows, this._activeMonitor);
  153. this.actor.add_actor(this._appList.actor);
  154. if (!this._iconsEnabled && !this._thumbnailsEnabled) {
  155. this._appList.actor.hide();
  156. }
  157. this._applist_act_id = this._appList.connect('item-activated', Lang.bind(this, this._appActivated));
  158. this._applist_enter_id = this._appList.connect('item-entered', Lang.bind(this, this._appEntered));
  159. this._appIcons = this._appList.icons;
  160. this.actor.get_allocation_box();
  161. },
  162. _selectNext: function() {
  163. if (this._currentIndex == this._windows.length - 1) {
  164. this._currentIndex = 0;
  165. } else {
  166. this._currentIndex = this._currentIndex + 1;
  167. }
  168. },
  169. _selectPrevious: function() {
  170. if (this._currentIndex == 0) {
  171. this._currentIndex = this._windows.length-1;
  172. } else {
  173. this._currentIndex = this._currentIndex - 1;
  174. }
  175. },
  176. _onWorkspaceSelected: function() {
  177. this._windows = AppSwitcher.getWindowsForBinding(this._binding);
  178. this._currentIndex = 0;
  179. this._updateList(0);
  180. this._select(0);
  181. },
  182. _setCurrentWindow: function(window) {
  183. this._appList.highlight(this._currentIndex, false);
  184. this._doWindowPreview();
  185. this._destroyThumbnails();
  186. if (this._thumbnailTimeoutId != 0) {
  187. Mainloop.source_remove(this._thumbnailTimeoutId);
  188. this._thumbnailTimeoutId = 0;
  189. }
  190. if (this._showArrows) {
  191. this._thumbnailTimeoutId = Mainloop.timeout_add(
  192. THUMBNAIL_POPUP_TIME, Lang.bind(this, function() {
  193. if (!this._thumbnails)
  194. this._createThumbnails();
  195. this._thumbnails.highlight(0, false);
  196. this._thumbnailTimeoutId = 0;
  197. }));
  198. }
  199. },
  200. _onDestroy: function() {
  201. if (this._appList !== null) {
  202. if (this._applist_act_id > 0) {
  203. this._appList.disconnect(this._applist_act_id);
  204. this._applist_act_id = 0;
  205. }
  206. if (this._applist_enter_id > 0) {
  207. this._appList.disconnect(this._applist_enter_id);
  208. this._applist_enter_id = 0;
  209. }
  210. }
  211. if (this._thumbnailTimeoutId != 0) {
  212. Mainloop.source_remove(this._thumbnailTimeoutId);
  213. this._thumbnailTimeoutId = 0;
  214. }
  215. if (this._displayPreviewTimeoutId != 0) {
  216. Mainloop.source_remove(this._displayPreviewTimeoutId);
  217. this._displayPreviewTimeoutId = 0;
  218. }
  219. },
  220. _appActivated : function(appSwitcher, n) {
  221. this._activateSelected();
  222. },
  223. _appEntered : function(appSwitcher, n) {
  224. if (!this._mouseActive)
  225. return;
  226. this._select(n);
  227. },
  228. _windowActivated : function(thumbnailList, n) {
  229. this._activateSelected();
  230. },
  231. _clearPreview: function() {
  232. if (this._previewClones) {
  233. for (let i = 0; i < this._previewClones.length; ++i) {
  234. let clone = this._previewClones[i];
  235. Tweener.addTween(clone, {
  236. opacity: 0,
  237. time: PREVIEW_SWITCHER_FADEOUT_TIME / 4,
  238. transition: 'linear',
  239. onCompleteScope: this,
  240. onComplete: function() {
  241. this.actor.remove_actor(clone);
  242. clone.destroy();
  243. }
  244. });
  245. }
  246. this._previewClones = null;
  247. }
  248. },
  249. _doWindowPreview: function() {
  250. if (!this._previewEnabled || this._windows.length < 1)
  251. {
  252. return;
  253. }
  254. // Use a cancellable timeout to avoid flickering effect when tabbing rapidly through the set.
  255. if (this._displayPreviewTimeoutId) {
  256. Mainloop.source_remove(this._displayPreviewTimeoutId);
  257. this._displayPreviewTimeoutId = 0;
  258. }
  259. let delay = PREVIEW_DELAY_TIMEOUT;
  260. this._displayPreviewTimeoutId = Mainloop.timeout_add(delay, Lang.bind(this, this._showWindowPreview));
  261. },
  262. _showWindowPreview: function() {
  263. this._displayPreviewTimeoutId = 0;
  264. let childBox = new Clutter.ActorBox();
  265. let lastClone = null;
  266. let previewClones = [];
  267. let window = this._windows[this._currentIndex];
  268. let clones = WindowUtils.createWindowClone(window, 0, 0, true, false);
  269. for (let i = 0; i < clones.length; i++) {
  270. let clone = clones[i];
  271. previewClones.push(clone.actor);
  272. this.actor.add_actor(clone.actor);
  273. let [width, height] = clone.actor.get_size();
  274. childBox.x1 = clone.x;
  275. childBox.x2 = clone.x + width;
  276. childBox.y1 = clone.y;
  277. childBox.y2 = clone.y + height;
  278. clone.actor.allocate(childBox, 0);
  279. clone.actor.lower(this._appList.actor);
  280. if (lastClone) {
  281. lastClone.lower(clone.actor);
  282. }
  283. lastClone = clone.actor;
  284. }
  285. this._clearPreview();
  286. this._previewClones = previewClones;
  287. if (!this._previewBackdrop) {
  288. let backdrop = this._previewBackdrop = new St.Bin({style_class: 'switcher-preview-backdrop'});
  289. this.actor.add_actor(backdrop);
  290. // Make sure that the backdrop does not overlap the switcher.
  291. backdrop.lower(this._appList.actor);
  292. backdrop.lower(lastClone);
  293. childBox.x1 = this.actor.x;
  294. childBox.x2 = this.actor.x + this.actor.width;
  295. childBox.y1 = this.actor.y;
  296. childBox.y2 = this.actor.y + this.actor.height;
  297. backdrop.allocate(childBox, 0);
  298. backdrop.opacity = 0;
  299. Tweener.addTween(backdrop,
  300. { opacity: 255,
  301. time: PREVIEW_SWITCHER_FADEOUT_TIME / 4,
  302. transition: 'linear'
  303. });
  304. }
  305. },
  306. _destroyThumbnails : function() {
  307. if (!this._thumbnails) {
  308. return;
  309. }
  310. let thumbnailsActor = this._thumbnails.actor;
  311. this._thumbnails = null;
  312. this.actor.remove_actor(thumbnailsActor);
  313. thumbnailsActor.destroy();
  314. this.thumbnailsVisible = false;
  315. },
  316. _createThumbnails : function() {
  317. this._thumbnails = new ThumbnailList ([this._windows[this._currentIndex]], this._activeMonitor);
  318. this._thumbnails.connect('item-activated', Lang.bind(this, this._windowActivated));
  319. this.actor.add_actor(this._thumbnails.actor);
  320. // Need to force an allocation so we can figure out whether we
  321. // need to scroll when selecting
  322. this._thumbnails.actor.get_allocation_box();
  323. this._thumbnails.actor.opacity = 0;
  324. Tweener.addTween(this._thumbnails.actor,
  325. { opacity: 255,
  326. time: THUMBNAIL_FADE_TIME,
  327. transition: 'easeOutQuad',
  328. onComplete: Lang.bind(this, function () { this.thumbnailsVisible = true; })
  329. });
  330. }
  331. };
  332. function AppIcon(window, showThumbnail) {
  333. this._init(window, showThumbnail);
  334. }
  335. AppIcon.prototype = {
  336. _init: function(window, showThumbnail) {
  337. this.window = window;
  338. this.showThumbnail = showThumbnail;
  339. let tracker = Cinnamon.WindowTracker.get_default();
  340. this.app = tracker.get_window_app(window);
  341. this.actor = new St.BoxLayout({ style_class: 'alt-tab-app',
  342. vertical: true });
  343. this.icon = null;
  344. this._iconBin = new St.Bin();
  345. this.actor.add(this._iconBin, { x_fill: false, y_fill: false } );
  346. let title = window.get_title();
  347. if (title) {
  348. if (window.minimized) {
  349. this.label = new St.Label({ text: "[" + title + "]"});
  350. let contrast_effect = new Clutter.BrightnessContrastEffect();
  351. contrast_effect.set_brightness_full(-0.5, -0.5, -0.5);
  352. this._iconBin.add_effect(contrast_effect);
  353. }
  354. else {
  355. this.label = new St.Label({ text: title });
  356. }
  357. let bin = new St.Bin({ x_align: St.Align.MIDDLE });
  358. bin.add_actor(this.label);
  359. this.actor.add(bin);
  360. }
  361. else {
  362. this.label = new St.Label({ text: this.app ? this.app.get_name() : window.title });
  363. this.actor.add(this.label, { x_fill: false });
  364. }
  365. },
  366. set_size: function(size) {
  367. if (this.showThumbnail){
  368. this.icon = new St.Widget();
  369. let clones = WindowUtils.createWindowClone(this.window, size * global.ui_scale, size * global.ui_scale, true, true);
  370. for (let i in clones) {
  371. let clone = clones[i];
  372. this.icon.add_actor(clone.actor);
  373. // the following 2 lines are used when cloning without positions (param #4 = false)
  374. //let [width, height] = clone.actor.get_size();
  375. //clone.actor.set_position(Math.round((size - width) / 2), Math.round((size - height) / 2));
  376. clone.actor.set_position(clone.x, clone.y);
  377. }
  378. } else {
  379. this.icon = this.app ?
  380. this.app.create_icon_texture_for_window(size, this.window) :
  381. new St.Icon({ icon_name: 'application-default-icon',
  382. icon_type: St.IconType.FULLCOLOR,
  383. icon_size: size });
  384. }
  385. size *= global.ui_scale;
  386. this._iconBin.set_size(size, size);
  387. this._iconBin.child = this.icon;
  388. }
  389. };
  390. function SwitcherList(squareItems, activeMonitor) {
  391. this._init(squareItems, activeMonitor);
  392. }
  393. SwitcherList.prototype = {
  394. _init : function(squareItems, activeMonitor) {
  395. this.actor = new Cinnamon.GenericContainer({ style_class: 'switcher-list' });
  396. this.actor.connect('get-preferred-width', Lang.bind(this, this._getPreferredWidth));
  397. this.actor.connect('get-preferred-height', Lang.bind(this, this._getPreferredHeight));
  398. this.actor.connect('allocate', Lang.bind(this, this._allocateTop));
  399. // Here we use a GenericContainer so that we can force all the
  400. // children except the separator to have the same width.
  401. // TODO: Separator is gone, we could use an St.ScrollView now.
  402. this._list = new Cinnamon.GenericContainer({ style_class: 'switcher-list-item-container' });
  403. this._list.spacing = -1;
  404. this._list.connect('style-changed', Lang.bind(this, function() {
  405. this._list.spacing = this._list.get_theme_node().get_length('spacing');
  406. }));
  407. this._list.connect('get-preferred-width', Lang.bind(this, this._getPreferredWidth));
  408. this._list.connect('get-preferred-height', Lang.bind(this, this._getPreferredHeight));
  409. this._list.connect('allocate', Lang.bind(this, this._allocate));
  410. this._clipBin = new St.Bin({style_class: 'cbin'});
  411. this._clipBin.child = this._list;
  412. this.actor.add_actor(this._clipBin);
  413. this._leftGradient = new St.BoxLayout({style_class: 'thumbnail-scroll-gradient-left', vertical: true});
  414. this._rightGradient = new St.BoxLayout({style_class: 'thumbnail-scroll-gradient-right', vertical: true});
  415. this.actor.add_actor(this._leftGradient);
  416. this.actor.add_actor(this._rightGradient);
  417. // Those arrows indicate whether scrolling in one direction is possible
  418. this._leftArrow = new St.DrawingArea({ style_class: 'switcher-arrow',
  419. pseudo_class: 'highlighted' });
  420. this._leftArrow.connect('repaint', Lang.bind(this,
  421. function() { _drawArrow(this._leftArrow, St.Side.LEFT); }));
  422. this._rightArrow = new St.DrawingArea({ style_class: 'switcher-arrow',
  423. pseudo_class: 'highlighted' });
  424. this._rightArrow.connect('repaint', Lang.bind(this,
  425. function() { _drawArrow(this._rightArrow, St.Side.RIGHT); }));
  426. this.actor.add_actor(this._leftArrow);
  427. this.actor.add_actor(this._rightArrow);
  428. this._items = [];
  429. this._highlighted = -1;
  430. this._squareItems = squareItems;
  431. this._minSize = 0;
  432. this._scrollableRight = true;
  433. this._scrollableLeft = false;
  434. this._activeMonitor = activeMonitor;
  435. },
  436. _allocateTop: function(actor, box, flags) {
  437. if (this._list.spacing === -1) {
  438. this._list.spacing = this._list.get_theme_node().get_length('spacing');
  439. }
  440. let leftPadding = this.actor.get_theme_node().get_padding(St.Side.LEFT);
  441. let rightPadding = this.actor.get_theme_node().get_padding(St.Side.RIGHT);
  442. let childBox = new Clutter.ActorBox();
  443. let scrollable = this._minSize > box.x2 - box.x1;
  444. this._clipBin.allocate(box, flags);
  445. childBox.x1 = 0;
  446. childBox.y1 = 0;
  447. childBox.x2 = this._leftGradient.width;
  448. childBox.y2 = this.actor.height;
  449. this._leftGradient.allocate(childBox, flags);
  450. this._leftGradient.opacity = (this._scrollableLeft && scrollable) ? 255 : 0;
  451. childBox.x1 = (this.actor.allocation.x2 - this.actor.allocation.x1) - this._rightGradient.width;
  452. childBox.y1 = 0;
  453. childBox.x2 = childBox.x1 + this._rightGradient.width;
  454. childBox.y2 = this.actor.height;
  455. this._rightGradient.allocate(childBox, flags);
  456. this._rightGradient.opacity = (this._scrollableRight && scrollable) ? 255 : 0;
  457. let arrowWidth = Math.floor(leftPadding / 3);
  458. let arrowHeight = arrowWidth * 2;
  459. childBox.x1 = leftPadding / 2;
  460. childBox.y1 = this.actor.height / 2 - arrowWidth;
  461. childBox.x2 = childBox.x1 + arrowWidth;
  462. childBox.y2 = childBox.y1 + arrowHeight;
  463. this._leftArrow.allocate(childBox, flags);
  464. this._leftArrow.opacity = this._leftGradient.opacity;
  465. arrowWidth = Math.floor(rightPadding / 3);
  466. arrowHeight = arrowWidth * 2;
  467. childBox.x1 = this.actor.width - arrowWidth - rightPadding / 2;
  468. childBox.y1 = this.actor.height / 2 - arrowWidth;
  469. childBox.x2 = childBox.x1 + arrowWidth;
  470. childBox.y2 = childBox.y1 + arrowHeight;
  471. this._rightArrow.allocate(childBox, flags);
  472. this._rightArrow.opacity = this._rightGradient.opacity;
  473. },
  474. addItem : function(item, label) {
  475. let bbox = new St.Button({ style_class: 'item-box',
  476. reactive: true });
  477. bbox.set_child(item);
  478. this._list.add_actor(bbox);
  479. let n = this._items.length;
  480. bbox.connect('clicked', Lang.bind(this, function() { this._onItemClicked(n); }));
  481. bbox.connect('enter-event', Lang.bind(this, function() { this._onItemEnter(n); }));
  482. bbox.label_actor = label;
  483. this._items.push(bbox);
  484. },
  485. _onItemClicked: function (index) {
  486. this._itemActivated(index);
  487. },
  488. _onItemEnter: function (index) {
  489. this._itemEntered(index);
  490. },
  491. highlight: function(index, justOutline) {
  492. if (this._highlighted != -1) {
  493. this._items[this._highlighted].remove_style_pseudo_class('outlined');
  494. this._items[this._highlighted].remove_style_pseudo_class('selected');
  495. }
  496. this._highlighted = index;
  497. if (this._highlighted != -1) {
  498. if (justOutline)
  499. this._items[this._highlighted].add_style_pseudo_class('outlined');
  500. else
  501. this._items[this._highlighted].add_style_pseudo_class('selected');
  502. }
  503. let [absItemX, absItemY] = this._items[index].get_transformed_position();
  504. let [result, posX, posY] = this.actor.transform_stage_point(absItemX, 0);
  505. let [containerWidth, containerHeight] = this.actor.get_transformed_size();
  506. if (posX + this._items[index].get_width() > containerWidth)
  507. this._scrollToRight();
  508. else if (posX < 0)
  509. this._scrollToLeft();
  510. },
  511. _scrollToLeft : function() {
  512. let x = this._items[this._highlighted].allocation.x1;
  513. this._scrollableRight = true;
  514. Tweener.addTween(this._list, { anchor_x: x,
  515. time: POPUP_SCROLL_TIME,
  516. transition: 'easeOutQuad',
  517. onComplete: Lang.bind(this, function () {
  518. if (this._highlighted == 0) {
  519. this._scrollableLeft = false;
  520. this.actor.queue_relayout();
  521. }
  522. })
  523. });
  524. },
  525. _scrollToRight : function() {
  526. this._scrollableLeft = true;
  527. let monitor = this._activeMonitor;
  528. let padding = this.actor.get_theme_node().get_horizontal_padding();
  529. let parentPadding = this.actor.get_parent().get_theme_node().get_horizontal_padding();
  530. let x = this._items[this._highlighted].allocation.x2 - monitor.width + padding + parentPadding;
  531. Tweener.addTween(this._list, { anchor_x: x,
  532. time: POPUP_SCROLL_TIME,
  533. transition: 'easeOutQuad',
  534. onComplete: Lang.bind(this, function () {
  535. if (this._highlighted == this._items.length - 1) {
  536. this._scrollableRight = false;
  537. this.actor.queue_relayout();
  538. }
  539. })
  540. });
  541. },
  542. _itemActivated: function(n) {
  543. this.emit('item-activated', n);
  544. },
  545. _itemEntered: function(n) {
  546. this.emit('item-entered', n);
  547. },
  548. _maxChildWidth: function () {
  549. let maxChildMin = 0;
  550. let maxChildNat = 0;
  551. if (this._items.length > 0) {
  552. return this._items[0].get_preferred_width(-1);
  553. }
  554. return [0, 0]
  555. },
  556. _getPreferredWidth: function (actor, forHeight, alloc) {
  557. let [maxChildMin, maxChildNat] = this._maxChildWidth();
  558. let totalSpacing = this._list.spacing * Math.max(1, (this._items.length - 1));
  559. alloc.min_size = this._items.length * maxChildMin + totalSpacing;
  560. alloc.natural_size = alloc.min_size;
  561. this._minSize = alloc.min_size;
  562. },
  563. _getPreferredHeight: function (actor, forWidth, alloc) {
  564. let maxChildMin = 0;
  565. let maxChildNat = 0;
  566. for (let i = 0; i < this._items.length; i++) {
  567. let [childMin, childNat] = this._items[i].get_preferred_height(-1);
  568. maxChildMin = Math.max(childMin, maxChildMin);
  569. maxChildNat = Math.max(childNat, maxChildNat);
  570. }
  571. if (this._squareItems) {
  572. let [childMin, childNat] = this._maxChildWidth();
  573. maxChildMin = Math.max(childMin, maxChildMin);
  574. maxChildNat = maxChildMin;
  575. }
  576. alloc.min_size = maxChildMin;
  577. alloc.natural_size = maxChildNat;
  578. },
  579. _allocate: function (actor, box, flags) {
  580. let childHeight = box.y2 - box.y1;
  581. let [maxChildMin, maxChildNat] = this._maxChildWidth();
  582. let totalSpacing = this._list.spacing * (this._items.length - 1);
  583. let childWidth = Math.floor(Math.max(0, box.x2 - box.x1 - totalSpacing) / this._items.length);
  584. let x = 0;
  585. let children = this._list.get_children();
  586. let childBox = new Clutter.ActorBox();
  587. let monitor = this._activeMonitor;
  588. let parentRightPadding = this.actor.get_parent().get_theme_node().get_padding(St.Side.RIGHT);
  589. if (this.actor.allocation.x2 == monitor.x + monitor.width - parentRightPadding) {
  590. if (this._squareItems)
  591. childWidth = childHeight;
  592. else {
  593. let [childMin, childNat] = children[0].get_preferred_width(childHeight);
  594. childWidth = childMin;
  595. }
  596. }
  597. for (let i = 0; i < children.length; i++) {
  598. if (this._items.indexOf(children[i]) != -1) {
  599. let [childMin, childNat] = children[i].get_preferred_height(childWidth);
  600. let vSpacing = (childHeight - childNat) / 2;
  601. childBox.x1 = x;
  602. childBox.y1 = vSpacing;
  603. childBox.x2 = x + childWidth;
  604. childBox.y2 = childBox.y1 + childNat;
  605. children[i].allocate(childBox, flags);
  606. x += this._list.spacing + childWidth;
  607. } else {
  608. // Something else, eg, AppList's arrows;
  609. // we don't allocate it.
  610. }
  611. }
  612. let leftPadding = this.actor.get_theme_node().get_padding(St.Side.LEFT);
  613. let rightPadding = this.actor.get_theme_node().get_padding(St.Side.RIGHT);
  614. let topPadding = this.actor.get_theme_node().get_padding(St.Side.TOP);
  615. let bottomPadding = this.actor.get_theme_node().get_padding(St.Side.BOTTOM);
  616. // Clip the area for scrolling
  617. this._clipBin.set_clip(0, -topPadding, (this.actor.allocation.x2 - this.actor.allocation.x1) - leftPadding - rightPadding, this.actor.height + bottomPadding);
  618. }
  619. };
  620. Signals.addSignalMethods(SwitcherList.prototype);
  621. function AppList() {
  622. this._init.apply(this, arguments);
  623. }
  624. AppList.prototype = {
  625. __proto__ : SwitcherList.prototype,
  626. _init : function(windows, showThumbnails, showArrows, activeMonitor) {
  627. SwitcherList.prototype._init.call(this, true, activeMonitor);
  628. // Construct the AppIcons, add to the popup
  629. let activeWorkspace = global.workspace_manager.get_active_workspace();
  630. let workspaceIcons = [];
  631. let otherIcons = [];
  632. for (let i = 0; i < windows.length; i++) {
  633. workspaceIcons.push(new AppIcon(windows[i], showThumbnails));
  634. }
  635. this.icons = [];
  636. this._arrows = [];
  637. for (let i = 0; i < workspaceIcons.length; i++)
  638. this._addIcon(workspaceIcons[i]);
  639. if (workspaceIcons.length > 0 && otherIcons.length > 0)
  640. this.addSeparator();
  641. for (let i = 0; i < otherIcons.length; i++)
  642. this._addIcon(otherIcons[i]);
  643. this._curApp = -1;
  644. this._iconSize = 0;
  645. this._showArrows = showArrows;
  646. this._mouseTimeOutId = 0;
  647. this._activeMonitor = activeMonitor;
  648. },
  649. _getPreferredHeight: function (actor, forWidth, alloc) {
  650. if (this._items.length < 1) {
  651. alloc.min_size = alloc.natural_size = 32;
  652. return;
  653. }
  654. let j = 0;
  655. while(this._items.length > 1 && this._items[j].style_class != 'item-box') {
  656. j++;
  657. }
  658. let themeNode = this._items[j].get_theme_node();
  659. let iconPadding = themeNode.get_horizontal_padding();
  660. let iconBorder = themeNode.get_border_width(St.Side.LEFT) + themeNode.get_border_width(St.Side.RIGHT);
  661. let [iconMinHeight, iconNaturalHeight] = this.icons[j].label.get_preferred_height(-1);
  662. let iconSpacing = iconNaturalHeight + iconPadding + iconBorder;
  663. let totalSpacing = this._list.spacing * (this._items.length - 1);
  664. // We just assume the whole screen here due to weirdness happing with the passed width
  665. let parentPadding = this.actor.get_parent().get_theme_node().get_horizontal_padding();
  666. let availWidth = this._activeMonitor.width - parentPadding - this.actor.get_theme_node().get_horizontal_padding();
  667. let height = 0;
  668. for(let i = 0; i < iconSizes.length; i++) {
  669. this._iconSize = iconSizes[i];
  670. height = (iconSizes[i] * global.ui_scale) + iconSpacing;
  671. let w = height * this._items.length + totalSpacing;
  672. if (w <= availWidth)
  673. break;
  674. }
  675. if (this._items.length == 1) {
  676. this._iconSize = iconSizes[0];
  677. height = (iconSizes[0] * global.ui_scale) + iconSpacing;
  678. }
  679. for(let i = 0; i < this.icons.length; i++) {
  680. if (this.icons[i].icon != null)
  681. break;
  682. this.icons[i].set_size(this._iconSize);
  683. }
  684. alloc.min_size = height;
  685. alloc.natural_size = height;
  686. },
  687. _allocate: function (actor, box, flags) {
  688. // Allocate the main list items
  689. SwitcherList.prototype._allocate.call(this, actor, box, flags);
  690. if (this._showArrows) {
  691. let arrowHeight = Math.floor(this.actor.get_theme_node().get_padding(St.Side.BOTTOM) / 3);
  692. let arrowWidth = arrowHeight * 2;
  693. // Now allocate each arrow underneath its item
  694. let childBox = new Clutter.ActorBox();
  695. for (let i = 0; i < this._items.length; i++) {
  696. let itemBox = this._items[i].allocation;
  697. childBox.x1 = Math.floor(itemBox.x1 + (itemBox.x2 - itemBox.x1 - arrowWidth) / 2);
  698. childBox.x2 = childBox.x1 + arrowWidth;
  699. childBox.y1 = itemBox.y2 + arrowHeight;
  700. childBox.y2 = childBox.y1 + arrowHeight;
  701. this._arrows[i].allocate(childBox, flags);
  702. }
  703. }
  704. },
  705. // We override SwitcherList's _onItemEnter method to delay
  706. // activation when the thumbnail list is open
  707. _onItemEnter: function (index) {
  708. if (this._mouseTimeOutId != 0)
  709. Mainloop.source_remove(this._mouseTimeOutId);
  710. this._itemEntered(index);
  711. },
  712. _enterItem: function(index) {
  713. let [x, y, mask] = global.get_pointer();
  714. let pickedActor = global.stage.get_actor_at_pos(Clutter.PickMode.ALL, x, y);
  715. if (this._items[index].contains(pickedActor))
  716. this._itemEntered(index);
  717. },
  718. // We override SwitcherList's highlight() method to also deal with
  719. // the AppList->ThumbnailList arrows.
  720. highlight : function(n, justOutline) {
  721. if (this._curApp != -1) {
  722. this._arrows[this._curApp].hide();
  723. }
  724. SwitcherList.prototype.highlight.call(this, n, justOutline);
  725. this._curApp = n;
  726. if (n != -1 && this._showArrows) {
  727. this._arrows[n].show();
  728. }
  729. },
  730. _addIcon : function(appIcon) {
  731. this.icons.push(appIcon);
  732. this.addItem(appIcon.actor, appIcon.label);
  733. let n = this._arrows.length;
  734. let arrow = new St.DrawingArea({ style_class: 'switcher-arrow' });
  735. arrow.connect('repaint', function() { _drawArrow(arrow, St.Side.BOTTOM); });
  736. this._list.add_actor(arrow);
  737. this._arrows.push(arrow);
  738. arrow.hide();
  739. }
  740. };
  741. function ThumbnailList(windows, activeMonitor) {
  742. this._init(windows, activeMonitor);
  743. }
  744. ThumbnailList.prototype = {
  745. __proto__ : SwitcherList.prototype,
  746. _init : function(windows, activeMonitor) {
  747. SwitcherList.prototype._init.call(this, false, activeMonitor);
  748. let activeWorkspace = global.workspace_manager.get_active_workspace();
  749. this._labels = new Array();
  750. this._thumbnailBins = new Array();
  751. this._clones = new Array();
  752. this._windows = windows;
  753. for (let i = 0; i < windows.length; i++) {
  754. let box = new St.BoxLayout({ style_class: 'thumbnail-box',
  755. vertical: true });
  756. let bin = new St.Bin({ style_class: 'thumbnail' });
  757. box.add_actor(bin);
  758. this._thumbnailBins.push(bin);
  759. let title = windows[i].get_title();
  760. if (title) {
  761. let name = new St.Label({ text: title });
  762. // St.Label doesn't support text-align so use a Bin
  763. let bin = new St.Bin({ x_align: St.Align.MIDDLE });
  764. this._labels.push(bin);
  765. bin.add_actor(name);
  766. box.add_actor(bin);
  767. this.addItem(box, name);
  768. } else {
  769. this.addItem(box, null);
  770. }
  771. }
  772. },
  773. addClones : function (availHeight) {
  774. if (!this._thumbnailBins.length)
  775. return;
  776. let totalPadding = this._items[0].get_theme_node().get_horizontal_padding() + this._items[0].get_theme_node().get_vertical_padding();
  777. totalPadding += this.actor.get_theme_node().get_horizontal_padding() + this.actor.get_theme_node().get_vertical_padding();
  778. let [labelMinHeight, labelNaturalHeight] = this._labels.length > 0 ?
  779. this._labels[0].get_preferred_height(-1) : [0, 0];
  780. let spacing = this._items[0].child.get_theme_node().get_length('spacing');
  781. availHeight = Math.min(availHeight - labelNaturalHeight - totalPadding - spacing, THUMBNAIL_DEFAULT_SIZE * global.ui_scale);
  782. let binHeight = availHeight + this._items[0].get_theme_node().get_vertical_padding() + this.actor.get_theme_node().get_vertical_padding() - spacing;
  783. binHeight = Math.min(THUMBNAIL_DEFAULT_SIZE * global.ui_scale, binHeight);
  784. for (let i = 0; i < this._thumbnailBins.length; i++) {
  785. let metaWindow = this._windows[i];
  786. let container = new St.Widget();
  787. let clones = WindowUtils.createWindowClone(metaWindow, availHeight, availHeight, true, true);
  788. for (let j = 0; j < clones.length; j++) {
  789. let clone = clones[j];
  790. container.add_actor(clone.actor);
  791. clone.actor.set_position(clone.x, clone.y);
  792. }
  793. this._thumbnailBins[i].set_height(binHeight);
  794. this._thumbnailBins[i].add_actor(container);
  795. this._clones.push(container);
  796. }
  797. // Make sure we only do this once
  798. this._thumbnailBins = new Array();
  799. }
  800. };
  801. function _drawArrow(area, side) {
  802. let themeNode = area.get_theme_node();
  803. let borderColor = themeNode.get_border_color(side);
  804. let bodyColor = themeNode.get_foreground_color();
  805. let [width, height] = area.get_surface_size ();
  806. let cr = area.get_context();
  807. cr.setLineWidth(1.0);
  808. Clutter.cairo_set_source_color(cr, borderColor);
  809. switch (side) {
  810. case St.Side.TOP:
  811. cr.moveTo(0, height);
  812. cr.lineTo(Math.floor(width * 0.5), 0);
  813. cr.lineTo(width, height);
  814. break;
  815. case St.Side.BOTTOM:
  816. cr.moveTo(width, 0);
  817. cr.lineTo(Math.floor(width * 0.5), height);
  818. cr.lineTo(0, 0);
  819. break;
  820. case St.Side.LEFT:
  821. cr.moveTo(width, height);
  822. cr.lineTo(0, Math.floor(height * 0.5));
  823. cr.lineTo(width, 0);
  824. break;
  825. case St.Side.RIGHT:
  826. cr.moveTo(0, 0);
  827. cr.lineTo(width, Math.floor(height * 0.5));
  828. cr.lineTo(0, height);
  829. break;
  830. }
  831. cr.strokePreserve();
  832. Clutter.cairo_set_source_color(cr, bodyColor);
  833. cr.fill();
  834. cr.$dispose();
  835. }