commit 093338beac08979c2799ed4dd2c4b43d46ac3ae1 Author: Artyom Zorin Date: Wed Sep 21 00:18:00 2016 +0000 Imported from previous codebase diff --git a/COPYING b/COPYING new file mode 100644 index 0000000..8d61b15 --- /dev/null +++ b/COPYING @@ -0,0 +1,341 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc. + 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Library General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Library General +Public License instead of this License. + diff --git a/convenience.js b/convenience.js new file mode 100644 index 0000000..0918d5c --- /dev/null +++ b/convenience.js @@ -0,0 +1,103 @@ +/* + * Taskbar: A taskbar extension for the Gnome panel. + * Copyright (C) 2016 Zorin OS + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * + * Credits: + * This file is based on code from the Dash to Dock extension by micheleg. + * Some code was also adapted from the upstream Gnome Shell source code. + */ + + +const Lang = imports.lang; + + +// simplify global signals and function injections handling +// abstract class +const BasicHandler = new Lang.Class({ + Name: 'Taskbar.BasicHandler', + + _init: function(){ + this._storage = new Object(); + }, + + add: function(/*unlimited 3-long array arguments*/){ + + // convert arguments object to array, concatenate with generic + let args = Array.concat('generic', Array.slice(arguments)); + // call addWithLabel with ags as if they were passed arguments + this.addWithLabel.apply(this, args); + }, + + destroy: function() { + for( let label in this._storage ) + this.removeWithLabel(label); + }, + + addWithLabel: function( label /* plus unlimited 3-long array arguments*/) { + + if(this._storage[label] == undefined) + this._storage[label] = new Array(); + + // skip first element of the arguments + for( let i = 1; i < arguments.length; i++ ) { + this._storage[label].push( this._create(arguments[i]) ); + } + + }, + + removeWithLabel: function(label){ + + if(this._storage[label]) { + for( let i = 0; i < this._storage[label].length; i++ ) { + this._remove(this._storage[label][i]); + } + + delete this._storage[label]; + } + }, + + /* Virtual methods to be implemented by subclass */ + // create single element to be stored in the storage structure + _create: function(item){ + throw new Error('no implementation of _create in ' + this); + }, + + // correctly delete single element + _remove: function(item){ + throw new Error('no implementation of _remove in ' + this); + } +}); + +// Manage global signals +const GlobalSignalsHandler = new Lang.Class({ + Name: 'Taskbar.GlobalSignalsHandler', + Extends: BasicHandler, + + _create: function(item) { + + let object = item[0]; + let event = item[1]; + let callback = item[2] + let id = object.connect(event, callback); + + return [object, id]; + }, + + _remove: function(item){ + item[0].disconnect(item[1]); + } +}); diff --git a/extension.js b/extension.js new file mode 100644 index 0000000..80ea801 --- /dev/null +++ b/extension.js @@ -0,0 +1,165 @@ +/* + * Taskbar: A taskbar extension for the Gnome panel. + * Copyright (C) 2016 Zorin OS + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * + * Credits: + * This file is based on code from the Dash to Dock extension by micheleg. + * Some code was also adapted from the upstream Gnome Shell source code. + */ + + +const Me = imports.misc.extensionUtils.getCurrentExtension(); +const Clutter = imports.gi.Clutter; +const Convenience = Me.imports.convenience; +const Taskbar = Me.imports.taskbar; +const Lang = imports.lang; +const Main = imports.ui.main; + +let appMenu; +let container; +let panel; +let panelConnectId; +let signalsHandler; +let taskbar; + +function init() { +} + +function enable() { + panel = Main.panel; + container = panel._leftBox; + appMenu = panel.statusArea['appMenu']; + + panelConnectId = panel.actor.connect('allocate', allocate); + container.remove_child(appMenu.container); + taskbar = new Taskbar.taskbar(); + Main.overview.dashIconSize = taskbar.iconSize; + + container.insert_child_at_index( taskbar.actor, 2 ); + + // Since Gnome 3.8 dragging an app without having opened the overview before cause the attemp to + //animate a null target since some variables are not initialized when the viewSelector is created + if(Main.overview.viewSelector._activePage == null) + Main.overview.viewSelector._activePage = Main.overview.viewSelector._workspacesPage; + + // sync hover after a popupmenu is closed + taskbar.connect('menu-closed', Lang.bind(this, function(){container.sync_hover();})); + + signalsHandler = new Convenience.GlobalSignalsHandler(); + signalsHandler.add( + // Keep dragged icon consistent in size with this dash + [ + taskbar, + 'icon-size-changed', + Lang.bind(this, function() { + Main.overview.dashIconSize = taskbar.iconSize; + }) + ], + // This duplicate the similar signal which is in owerview.js. + // Being connected and thus executed later this effectively + // overwrite any attempt to use the size of the default dash + // which given the customization is usually much smaller. + // I can't easily disconnect the original signal + [ + Main.overview._controls.dash, + 'icon-size-changed', + Lang.bind(this, function() { + Main.overview.dashIconSize = taskbar.iconSize; + }) + ] + ); + } + +function disable() { + signalsHandler.destroy(); + container.remove_child(taskbar.actor); + container.add_child(appMenu.container); + taskbar.destroy(); + panel.actor.disconnect(panelConnectId); + + // reset stored icon size to the default dash + Main.overview.dashIconSize = Main.overview._controls.dash.iconSize; + + appMenu = null; + container = null; + panel = null; + panelConnectId = null; + signalsHandler = null; + taskbar = null; +} + +function allocate(actor, box, flags) { + let allocWidth = box.x2 - box.x1; + let allocHeight = box.y2 - box.y1; + + let [leftMinWidth, leftNaturalWidth] = panel._leftBox.get_preferred_width(-1); + let [centerMinWidth, centerNaturalWidth] = panel._centerBox.get_preferred_width(-1); + let [rightMinWidth, rightNaturalWidth] = panel._rightBox.get_preferred_width(-1); + + let sideWidth = allocWidth - rightNaturalWidth - centerNaturalWidth; + + let childBox = new Clutter.ActorBox(); + + childBox.y1 = 0; + childBox.y2 = allocHeight; + if (panel.actor.get_text_direction() == Clutter.TextDirection.RTL) { + childBox.x1 = allocWidth - Math.min(Math.floor(sideWidth), leftNaturalWidth); + childBox.x2 = allocWidth; + } else { + childBox.x1 = 0; + childBox.x2 = sideWidth; + } + panel._leftBox.allocate(childBox, flags); + + childBox.y1 = 0; + childBox.y2 = allocHeight; + if (panel.actor.get_text_direction() == Clutter.TextDirection.RTL) { + childBox.x1 = rightNaturalWidth; + childBox.x2 = childBox.x1 + centerNaturalWidth; + } else { + childBox.x1 = allocWidth - centerNaturalWidth - rightNaturalWidth; + childBox.x2 = childBox.x1 + centerNaturalWidth; + } + panel._centerBox.allocate(childBox, flags); + + childBox.y1 = 0; + childBox.y2 = allocHeight; + if (panel.actor.get_text_direction() == Clutter.TextDirection.RTL) { + childBox.x1 = 0; + childBox.x2 = rightNaturalWidth; + } else { + childBox.x1 = allocWidth - rightNaturalWidth; + childBox.x2 = allocWidth; + } + panel._rightBox.allocate(childBox, flags); + + let [cornerMinWidth, cornerWidth] = panel._leftCorner.actor.get_preferred_width(-1); + let [cornerMinHeight, cornerHeight] = panel._leftCorner.actor.get_preferred_width(-1); + childBox.x1 = 0; + childBox.x2 = cornerWidth; + childBox.y1 = allocHeight; + childBox.y2 = allocHeight + cornerHeight; + panel._leftCorner.actor.allocate(childBox, flags); + + let [cornerMinWidth, cornerWidth] = panel._rightCorner.actor.get_preferred_width(-1); + let [cornerMinHeight, cornerHeight] = panel._rightCorner.actor.get_preferred_width(-1); + childBox.x1 = allocWidth - cornerWidth; + childBox.x2 = allocWidth; + childBox.y1 = allocHeight; + childBox.y2 = allocHeight + cornerHeight; + panel._rightCorner.actor.allocate(childBox, flags); +} diff --git a/metadata.json b/metadata.json new file mode 100755 index 0000000..ecbb294 --- /dev/null +++ b/metadata.json @@ -0,0 +1,8 @@ +{ +"extension-id": "zorin-taskbar", +"uuid": "zorin-taskbar@zorinos.com", +"name": "Zorin Taskbar", +"description": "The official taskbar for Zorin OS.", +"shell-version": [ "3.18" ], +"url": "https://github.com/ZorinOS/zorin-taskbar" +} diff --git a/secondaryMenu.js b/secondaryMenu.js new file mode 100644 index 0000000..afd4dbc --- /dev/null +++ b/secondaryMenu.js @@ -0,0 +1,87 @@ +/* + * Taskbar: A taskbar extension for the Gnome panel. + * Copyright (C) 2016 Zorin OS + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * + * Credits: + * This file is based on code from the Dash to Dock extension by micheleg. + * Some code was also adapted from the upstream Gnome Shell source code. + */ + + +const AppDisplay = imports.ui.appDisplay; +const Lang = imports.lang; + +const Me = imports.misc.extensionUtils.getCurrentExtension(); +const Taskbar = Me.imports.taskbar; + +/** + * Extend AppIconMenu + * + * - set popup arrow side based on taskbar orientation + * - Add close windows option based on quitfromdash extension + * (https://github.com/deuill/shell-extension-quitfromdash) + */ + +const taskbarSecondaryMenu = new Lang.Class({ + Name: 'taskbarSecondaryMenu', + Extends: AppDisplay.AppIconMenu, + + _init: function(source) { + + let side = Taskbar.getPosition(); + + // Damm it, there has to be a proper way of doing this... + // As I can't call the parent parent constructor (?) passing the side + // parameter, I overwite what I need later + this.parent(source); + + // Change the initialized side where required. + this._arrowSide = side; + this._boxPointer._arrowSide = side; + this._boxPointer._userArrowSide = side; + }, + + // helper function for the quit windows abilities + _closeWindowInstance: function(metaWindow) { + metaWindow.delete(global.get_current_time()); + }, + + _redisplay: function() { + this.parent(); + + // quit menu + let app = this._source.app; + let count = Taskbar.getAppInterestingWindows(app).length; + if ( count > 0) { + this._appendSeparator(); + let quitFromTaskbarMenuText = ""; + if (count == 1) + quitFromTaskbarMenuText = _("Quit"); + else + quitFromTaskbarMenuText = _("Quit") + ' ' + count + ' ' + _("Windows"); + + this._quitfromTaskbarMenuItem = this._appendMenuItem(quitFromTaskbarMenuText); + this._quitfromTaskbarMenuItem.connect('activate', Lang.bind(this, function() { + let app = this._source.app; + let windows = app.get_windows(); + for (let i = 0; i < windows.length; i++) { + this._closeWindowInstance(windows[i]) + } + })); + } + } +}); diff --git a/stylesheet.css b/stylesheet.css new file mode 100644 index 0000000..f6cbcb9 --- /dev/null +++ b/stylesheet.css @@ -0,0 +1,61 @@ +/* + * Taskbar: A taskbar extension for the Gnome panel. + * Copyright (C) 2016 Zorin OS + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#taskbarScrollview .dash-item-container > StWidget { + padding: 0; + margin: 0 8px; +} + +#taskbarScrollview .app-well-app .overview-icon { + border-radius: 0; + padding: 8px; + border: none; +} + +#taskbarScrollview .app-well-app-running-dot { + margin-bottom: 0; +} + +#thumbnailPreviewScrollview, +#taskbarScrollview { + -st-hfade-offset: 48px; +} + +.taskbarSecondaryMenu { + max-width: 400px; +} + +#thumbnailPreviewList { + spacing: 0em; + padding: 0 1em; +} + +#thumbnailPreviewList .popup-menu-item { + padding: 1em; + border-radius: 5px; + spacing: 0; +} + +#thumbnailPreviewList .window-box { + padding: 0; + spacing: 0; +} + +#thumbnailPreviewList .preview-window-title { + padding-top: 1em; +} diff --git a/taskbar.js b/taskbar.js new file mode 100644 index 0000000..6b285a6 --- /dev/null +++ b/taskbar.js @@ -0,0 +1,1374 @@ +/* + * Taskbar: A taskbar extension for the Gnome panel. + * Copyright (C) 2016 Zorin OS + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * + * Credits: + * This file is based on code from the Dash to Dock extension by micheleg. + * Some code was also adapted from the upstream Gnome Shell source code. + */ + + +const Clutter = imports.gi.Clutter; +const Gio = imports.gi.Gio; +const GLib = imports.gi.GLib; +const Gtk = imports.gi.Gtk; +const Signals = imports.signals; +const Lang = imports.lang; +const Meta = imports.gi.Meta; +const Shell = imports.gi.Shell; +const St = imports.gi.St; +const Mainloop = imports.mainloop; + +const AppDisplay = imports.ui.appDisplay; +const AppFavorites = imports.ui.appFavorites; +const Dash = imports.ui.dash; +const DND = imports.ui.dnd; +const IconGrid = imports.ui.iconGrid; +const Main = imports.ui.main; +const PopupMenu = imports.ui.popupMenu; +const Tweener = imports.ui.tweener; +const Util = imports.misc.util; +const Workspace = imports.ui.workspace; + +const Me = imports.misc.extensionUtils.getCurrentExtension(); +const Convenience = Me.imports.convenience; +const SecondaryMenu = Me.imports.secondaryMenu; +const WindowPreview = Me.imports.windowPreview; + +let DASH_ANIMATION_TIME = Dash.DASH_ANIMATION_TIME; +let DASH_ITEM_LABEL_SHOW_TIME = Dash.DASH_ITEM_LABEL_SHOW_TIME; +let DASH_ITEM_LABEL_HIDE_TIME = Dash.DASH_ITEM_LABEL_HIDE_TIME; +let DASH_ITEM_HOVER_TIMEOUT = Dash.DASH_ITEM_HOVER_TIMEOUT; +let LABEL_GAP = 5; +let RUNNING_INDICATOR_SIZE = 3; +let HFADE_WIDTH = 48; + +function getPosition() { + return Main.layoutManager.panelBox.anchor_y == 0 ? St.Side.TOP : St.Side.BOTTOM; +} + +/** + * Extend DashItemContainer + * + * - set label position based on taskbar orientation + * + * I can't subclass the original object because of this: https://bugzilla.gnome.org/show_bug.cgi?id=688973. + * thus use this ugly pattern. + */ + +// define first this function to use it in extendDashItemContainer +function ItemShowLabel() { + if (!this._labelText) + return; + + this.label.set_text(this._labelText); + this.label.opacity = 0; + this.label.show(); + + let [stageX, stageY] = this.get_transformed_position(); + let node = this.label.get_theme_node(); + + let itemWidth = this.allocation.x2 - this.allocation.x1; + let itemHeight = this.allocation.y2 - this.allocation.y1; + + let labelWidth = this.label.get_width(); + let labelHeight = this.label.get_height(); + + let x, y, xOffset, yOffset; + + let position = getPosition(); + let labelOffset = node.get_length('-x-offset'); + + switch(position) { + case St.Side.TOP: + y = stageY + labelOffset + itemHeight; + xOffset = Math.floor((itemWidth - labelWidth) / 2); + x = stageX + xOffset; + break; + case St.Side.BOTTOM: + yOffset = labelOffset; + y = stageY - labelHeight - yOffset; + xOffset = Math.floor((itemWidth - labelWidth) / 2); + x = stageX + xOffset; + break; + } + + // keep the label inside the screen border + // Only needed for the x coordinate. + + // Leave a few pixel gap + let gap = LABEL_GAP; + let monitor = Main.layoutManager.findMonitorForActor(this); + if ( x - monitor.x < gap) + x += monitor.x - x + labelOffset; + else if ( x + labelWidth > monitor.x + monitor.width - gap) + x -= x + labelWidth -( monitor.x + monitor.width) + gap; + + this.label.set_position(x, y); + Tweener.addTween(this.label, + { opacity: 255, + time: DASH_ITEM_LABEL_SHOW_TIME, + transition: 'easeOutQuad', + }); +}; + +function extendDashItemContainer(dashItemContainer) { + dashItemContainer.showLabel = ItemShowLabel; +}; + +/* This class is a fork of the upstream DashActor class (ui.dash.js) + * + * Summary of changes: + * - modified chldBox calculations for when 'show-apps-at-top' option is checked + * - handle horizontal dash + */ + +const taskbarActor = new Lang.Class({ + Name: 'taskbarActor', + + _init: function() { + this._rtl = Clutter.get_default_text_direction() == Clutter.TextDirection.RTL; + + this._position = getPosition(); + + let layout = new Clutter.BoxLayout({ orientation: Clutter.Orientation.HORIZONTAL }); + + this.actor = new Shell.GenericContainer({ name: 'taskbar', + layout_manager: layout, + clip_to_allocation: true }); + 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.actor._delegate = this; + + }, + + _allocate: function(actor, box, flags) { + let contentBox = box; + + let [appIcons] = actor.get_children(); + + let childBox = new Clutter.ActorBox(); + childBox.x1 = contentBox.x1; + childBox.y1 = contentBox.y1; + childBox.x2 = contentBox.x2; + childBox.y2 = contentBox.y2; + appIcons.allocate(childBox, flags); + }, + + _getPreferredWidth: function(actor, forHeight, alloc) { + // We want to request the natural height of all our children + // as our natural height, so we chain up to StWidget (which + // then calls BoxLayout) + let [, natWidth] = this.actor.layout_manager.get_preferred_width(this.actor, forHeight); + alloc.min_size = 0; + alloc.natural_size = natWidth + HFADE_WIDTH; + }, + + _getPreferredHeight: function(actor, forWidth, alloc) { + // We want to request the natural height of all our children + // as our natural height, so we chain up to StWidget (which + // then calls BoxLayout) + let [, natHeight] = this.actor.layout_manager.get_preferred_height(this.actor, forWidth); + alloc.min_size = 0; + alloc.natural_size = natHeight; + } +}); + +/* This class is a fork of the upstream dash class (ui.dash.js) + * + * Summary of changes: + * - disconnect global signals adding a destroy method; + * - play animations even when not in overview mode + * - set a maximum icon size + * - show running and/or favorite applications + * - emit a custom signal when an app icon is added + * - Add scrollview + * Ensure actor is visible on keyfocus inside the scrollview + * - add 128px icon size, might be useful for hidpi display + * - Sync minimization application target position. + */ + +const baseIconSizes = [ 16, 22, 24, 32, 48, 64, 96, 128 ]; + +const taskbar = new Lang.Class({ + Name: 'taskbar.taskbar', + + _init : function() { + this._maxHeight = -1; + this.iconSize = 32; + this._availableIconSizes = baseIconSizes; + this._shownInitially = false; + + this._position = getPosition(); + this._signalsHandler = new Convenience.GlobalSignalsHandler(); + + this._dragPlaceholder = null; + this._dragPlaceholderPos = -1; + this._animatingPlaceholdersCount = 0; + this._showLabelTimeoutId = 0; + this._resetHoverTimeoutId = 0; + this._ensureAppIconVisibilityTimeoutId = 0; + this._labelShowing = false; + + this._containerObject = new taskbarActor(); + this._container = this._containerObject.actor; + this._scrollView = new St.ScrollView({ name: 'taskbarScrollview', + hscrollbar_policy: Gtk.PolicyType.NEVER, + vscrollbar_policy: Gtk.PolicyType.NEVER, + enable_mouse_scrolling: true }); + + this._scrollView.connect('scroll-event', Lang.bind(this, this._onScrollEvent )); + + this._box = new St.BoxLayout({ vertical: false, + clip_to_allocation: false, + x_align: Clutter.ActorAlign.START, + y_align: Clutter.ActorAlign.START }); + this._box._delegate = this; + this._container.add_actor(this._scrollView); + this._scrollView.add_actor(this._box); + + let rtl = Clutter.get_default_text_direction() == Clutter.TextDirection.RTL; + this.actor = new St.Bin({ child: this._container, + y_align: St.Align.START, x_align:rtl?St.Align.END:St.Align.START + }); + + Main.panel.actor.connect('notify::height', Lang.bind(this, + function() { + this._queueRedisplay(); + })); + + this.actor.connect('notify::width', Lang.bind(this, + function() { + if (this._maxHeight != this.actor.width) + this._queueRedisplay(); + this._maxHeight = this.actor.width; + })); + + // Update minimization animation target position on allocation of the + // container and on scrollview change. + this._box.connect('notify::allocation', Lang.bind(this, this._updateAppIconsGeometry)); + let scrollViewAdjustment = this._scrollView.hscroll.adjustment; + scrollViewAdjustment.connect('notify::value', Lang.bind(this, this._updateAppIconsGeometry)); + + this._workId = Main.initializeDeferredWork(this._box, Lang.bind(this, this._redisplay)); + + this._settings = new Gio.Settings({ schema_id: 'org.gnome.shell' }); + + this._appSystem = Shell.AppSystem.get_default(); + + this._signalsHandler.add( + [ + this._appSystem, + 'installed-changed', + Lang.bind(this, function() { + AppFavorites.getAppFavorites().reload(); + this._queueRedisplay(); + }) + ], + [ + AppFavorites.getAppFavorites(), + 'changed', + Lang.bind(this, this._queueRedisplay) + ], + [ + this._appSystem, + 'app-state-changed', + Lang.bind(this, this._queueRedisplay) + ], + [ + Main.overview, + 'item-drag-begin', + Lang.bind(this, this._onDragBegin) + ], + [ + Main.overview, + 'item-drag-end', + Lang.bind(this, this._onDragEnd) + ], + [ + Main.overview, + 'item-drag-cancelled', + Lang.bind(this, this._onDragCancelled) + ] + ); + + }, + + destroy: function() { + this._signalsHandler.destroy(); + }, + + _onScrollEvent: function(actor, event) { + + // Event coordinates are relative to the stage but can be transformed + // as the actor will only receive events within his bounds. + let stage_x, stage_y, ok, event_x, event_y, actor_w, actor_h; + [stage_x, stage_y] = event.get_coords(); + [ok, event_x, event_y] = actor.transform_stage_point(stage_x, stage_y); + [actor_w, actor_h] = actor.get_size(); + + // If the scroll event is within a 1px margin from + // the relevant edge of the actor, let the event propagate. + if ((this._position == St.Side.TOP && event_y <= 1) || + (this._position == St.Side.BOTTOM && event_y >= actor_h - 2)) + return Clutter.EVENT_PROPAGATE; + + // reset timeout to avid conflicts with the mousehover event + if (this._ensureAppIconVisibilityTimeoutId>0) { + Mainloop.source_remove(this._ensureAppIconVisibilityTimeoutId); + this._ensureAppIconVisibilityTimeoutId = 0; + } + + // Skip to avoid double events mouse + if (event.is_pointer_emulated()) + return Clutter.EVENT_STOP; + + let adjustment, delta; + + adjustment = this._scrollView.get_hscroll_bar().get_adjustment(); + + let increment = adjustment.step_increment; + + switch ( event.get_scroll_direction() ) { + case Clutter.ScrollDirection.UP: + delta = -increment; + break; + case Clutter.ScrollDirection.DOWN: + delta = +increment; + break; + case Clutter.ScrollDirection.SMOOTH: + let [dx, dy] = event.get_scroll_delta(); + delta = dy*increment; + delta += dx*increment; + break; + + } + + adjustment.set_value(adjustment.get_value() + delta); + + return Clutter.EVENT_STOP; + + }, + + _onDragBegin: function() { + this._dragCancelled = false; + this._dragMonitor = { + dragMotion: Lang.bind(this, this._onDragMotion) + }; + DND.addDragMonitor(this._dragMonitor); + + if (this._box.get_n_children() == 0) { + this._emptyDropTarget = new Dash.EmptyDropTargetItem(); + this._box.insert_child_at_index(this._emptyDropTarget, 0); + this._emptyDropTarget.show(true); + } + }, + + _onDragCancelled: function() { + this._dragCancelled = true; + this._endDrag(); + }, + + _onDragEnd: function() { + if (this._dragCancelled) + return; + + this._endDrag(); + }, + + _endDrag: function() { + this._clearDragPlaceholder(); + this._clearEmptyDropTarget(); + DND.removeDragMonitor(this._dragMonitor); + }, + + _onDragMotion: function(dragEvent) { + let app = Dash.getAppFromSource(dragEvent.source); + if (app == null) + return DND.DragMotionResult.CONTINUE; + + if (!this._box.contains(dragEvent.targetActor)) + this._clearDragPlaceholder(); + + return DND.DragMotionResult.CONTINUE; + }, + + _appIdListToHash: function(apps) { + let ids = {}; + for (let i = 0; i < apps.length; i++) + ids[apps[i].get_id()] = apps[i]; + return ids; + }, + + _queueRedisplay: function () { + Main.queueDeferredWork(this._workId); + }, + + _hookUpLabel: function(item, appIcon) { + item.child.connect('notify::hover', Lang.bind(this, function() { + this._syncLabel(item, appIcon); + })); + + if (appIcon) { + appIcon.connect('sync-tooltip', Lang.bind(this, function() { + this._syncLabel(item, appIcon); + })); + } + }, + + _createAppItem: function(app) { + let appIcon = new taskbarAppIcon(app, + { setSizeManually: true, + showLabel: false }); + if (appIcon._draggable) { + appIcon._draggable.connect('drag-begin', + Lang.bind(this, function() { + appIcon.actor.opacity = 50; + })); + appIcon._draggable.connect('drag-end', + Lang.bind(this, function() { + appIcon.actor.opacity = 255; + })); + } + + appIcon.connect('menu-state-changed', + Lang.bind(this, function(appIcon, opened) { + this._itemMenuStateChanged(item, opened); + })); + + let item = new Dash.DashItemContainer(); + + extendDashItemContainer(item); + item.setChild(appIcon.actor); + + appIcon.actor.connect('notify::hover', Lang.bind(this, function() { + if (appIcon.actor.hover){ + this._ensureAppIconVisibilityTimeoutId = Mainloop.timeout_add(100, Lang.bind(this, function(){ + ensureActorVisibleInScrollView(this._scrollView, appIcon.actor); + this._ensureAppIconVisibilityTimeoutId = 0; + return GLib.SOURCE_REMOVE; + })); + } else { + if (this._ensureAppIconVisibilityTimeoutId>0) { + Mainloop.source_remove(this._ensureAppIconVisibilityTimeoutId); + this._ensureAppIconVisibilityTimeoutId = 0; + } + } + })); + + appIcon.actor.connect('clicked', + Lang.bind(this, function(actor) { + ensureActorVisibleInScrollView(this._scrollView, actor); + })); + + appIcon.actor.connect('key-focus-in', Lang.bind(this, function(actor) { + let [x_shift, y_shift] = ensureActorVisibleInScrollView(this._scrollView, actor); + + // This signal is triggered also by mouse click. The popup menu is opened at the original + // coordinates. Thus correct for the shift which is going to be applied to the scrollview. + if (appIcon._menu) { + appIcon._menu._boxPointer.xOffset = -x_shift; + appIcon._menu._boxPointer.yOffset = -y_shift; + } + })); + + // Override default AppIcon label_actor, now the + // accessible_name is set at DashItemContainer.setLabelText + appIcon.actor.label_actor = null; + item.setLabelText(app.get_name()); + + appIcon.icon.setIconSize(this.iconSize); + this._hookUpLabel(item, appIcon); + + return item; + }, + + // Return an array with the "proper" appIcons currently in the taskbar + _getAppIcons: function() { + // Only consider children which are "proper" + // icons (i.e. ignoring drag placeholders) and which are not + // animating out (which means they will be destroyed at the end of + // the animation) + let iconChildren = this._box.get_children().filter(function(actor) { + return actor.child && + actor.child._delegate && + actor.child._delegate.icon && + !actor.animatingOut; + }); + + let appIcons = iconChildren.map(function(actor){ + return actor.child._delegate; + }); + + return appIcons; + }, + + _updateAppIconsGeometry: function() { + let appIcons = this._getAppIcons(); + appIcons.forEach(function(icon){ + icon.updateIconGeometry(); + }); + }, + + _itemMenuStateChanged: function(item, opened) { + // When the menu closes, it calls sync_hover, which means + // that the notify::hover handler does everything we need to. + if (opened) { + if (this._showLabelTimeoutId > 0) { + Mainloop.source_remove(this._showLabelTimeoutId); + this._showLabelTimeoutId = 0; + } + + item.hideLabel(); + } else { + // I want to listen from outside when a menu is closed. I used to + // add a custom signal to the appIcon, since gnome 3.8 the signal + // calling this callback was added upstream. + this.emit('menu-closed'); + } + }, + + _syncLabel: function (item, appIcon) { + let shouldShow = appIcon ? appIcon.shouldShowTooltip() : item.child.get_hover(); + + if (shouldShow) { + if (this._showLabelTimeoutId == 0) { + let timeout = this._labelShowing ? 0 : DASH_ITEM_HOVER_TIMEOUT; + this._showLabelTimeoutId = Mainloop.timeout_add(timeout, + Lang.bind(this, function() { + this._labelShowing = true; + item.showLabel(); + this._showLabelTimeoutId = 0; + return GLib.SOURCE_REMOVE; + })); + GLib.Source.set_name_by_id(this._showLabelTimeoutId, '[gnome-shell] item.showLabel'); + if (this._resetHoverTimeoutId > 0) { + Mainloop.source_remove(this._resetHoverTimeoutId); + this._resetHoverTimeoutId = 0; + } + } + } else { + if (this._showLabelTimeoutId > 0) + Mainloop.source_remove(this._showLabelTimeoutId); + this._showLabelTimeoutId = 0; + item.hideLabel(); + if (this._labelShowing) { + this._resetHoverTimeoutId = Mainloop.timeout_add(DASH_ITEM_HOVER_TIMEOUT, + Lang.bind(this, function() { + this._labelShowing = false; + this._resetHoverTimeoutId = 0; + return GLib.SOURCE_REMOVE; + })); + GLib.Source.set_name_by_id(this._resetHoverTimeoutId, '[gnome-shell] this._labelShowing'); + } + } + }, + + _adjustIconSize: function() { + // For the icon size, we only consider children which are "proper" + // icons (i.e. ignoring drag placeholders) and which are not + // animating out (which means they will be destroyed at the end of + // the animation) + let iconChildren = this._box.get_children().filter(function(actor) { + return actor.child && + actor.child._delegate && + actor.child._delegate.icon && + !actor.animatingOut; + }); + + if (this._maxHeight == -1) + return; + + let scaleFactor = St.ThemeContext.get_for_stage(global.stage).scale_factor; + let iconSizes = this._availableIconSizes.map(function(s) { + return s * scaleFactor; + }); + + // Getting the panel height and making sure that the icon padding is at + // least the size of the app running indicator on both the top and bottom. + let availSize = Main.panel.actor.get_height() - (RUNNING_INDICATOR_SIZE * 2); + + let newIconSize = this._availableIconSizes[0]; + for (let i = 0; i < iconSizes.length ; i++) { + if (iconSizes[i] < availSize) { + newIconSize = this._availableIconSizes[i]; + } + } + + if (newIconSize == this.iconSize) + return; + + let oldIconSize = this.iconSize; + this.iconSize = newIconSize; + this.emit('icon-size-changed'); + + let scale = oldIconSize / newIconSize; + for (let i = 0; i < iconChildren.length; i++) { + let icon = iconChildren[i].child._delegate.icon; + + // Set the new size immediately, to keep the icons' sizes + // in sync with this.iconSize + icon.setIconSize(this.iconSize); + + // Don't animate the icon size change when the overview + // is transitioning, or when initially filling + // the taskbar + if (Main.overview.animationInProgress || + !this._shownInitially) + continue; + + let [targetWidth, targetHeight] = icon.icon.get_size(); + + // Scale the icon's texture to the previous size and + // tween to the new size + icon.icon.set_size(icon.icon.width * scale, + icon.icon.height * scale); + + Tweener.addTween(icon.icon, + { width: targetWidth, + height: targetHeight, + time: DASH_ANIMATION_TIME, + transition: 'easeOutQuad', + }); + } + }, + + sortAppsCompareFunction: function(appA, appB) { + let windowA = getAppInterestingWindows(appA)[0]; + let windowB = getAppInterestingWindows(appB)[0]; + return windowA.get_stable_sequence() > windowB.get_stable_sequence(); + }, + + _redisplay: function () { + let favorites = AppFavorites.getAppFavorites().getFavoriteMap(); + + let running = this._appSystem.get_running().sort(this.sortAppsCompareFunction); + + let children = this._box.get_children().filter(function(actor) { + return actor.child && + actor.child._delegate && + actor.child._delegate.app; + }); + // Apps currently in the taskbar + let oldApps = children.map(function(actor) { + return actor.child._delegate.app; + }); + // Apps supposed to be in the taskbar + let newApps = []; + + // Adding favorites + for (let id in favorites) + newApps.push(favorites[id]); + + // Adding running apps + for (let i = 0; i < running.length; i++) { + let app = running[i]; + if (app.get_id() in favorites) + continue; + newApps.push(app); + } + + // Figure out the actual changes to the list of items; we iterate + // over both the list of items currently in the taskbar and the list + // of items expected there, and collect additions and removals. + // Moves are both an addition and a removal, where the order of + // the operations depends on whether we encounter the position + // where the item has been added first or the one from where it + // was removed. + // There is an assumption that only one item is moved at a given + // time; when moving several items at once, everything will still + // end up at the right position, but there might be additional + // additions/removals (e.g. it might remove all the launchers + // and add them back in the new order even if a smaller set of + // additions and removals is possible). + // If above assumptions turns out to be a problem, we might need + // to use a more sophisticated algorithm, e.g. Longest Common + // Subsequence as used by diff. + let addedItems = []; + let removedActors = []; + + let newIndex = 0; + let oldIndex = 0; + while (newIndex < newApps.length || oldIndex < oldApps.length) { + // No change at oldIndex/newIndex + if (oldApps[oldIndex] == newApps[newIndex]) { + oldIndex++; + newIndex++; + continue; + } + + // App removed at oldIndex + if (oldApps[oldIndex] && + newApps.indexOf(oldApps[oldIndex]) == -1) { + removedActors.push(children[oldIndex]); + oldIndex++; + continue; + } + + // App added at newIndex + if (newApps[newIndex] && + oldApps.indexOf(newApps[newIndex]) == -1) { + addedItems.push({ app: newApps[newIndex], + item: this._createAppItem(newApps[newIndex]), + pos: newIndex }); + newIndex++; + continue; + } + + // App moved + let insertHere = newApps[newIndex + 1] && + newApps[newIndex + 1] == oldApps[oldIndex]; + let alreadyRemoved = removedActors.reduce(function(result, actor) { + let removedApp = actor.child._delegate.app; + return result || removedApp == newApps[newIndex]; + }, false); + + if (insertHere || alreadyRemoved) { + let newItem = this._createAppItem(newApps[newIndex]); + addedItems.push({ app: newApps[newIndex], + item: newItem, + pos: newIndex + removedActors.length }); + newIndex++; + } else { + removedActors.push(children[oldIndex]); + oldIndex++; + } + } + + for (let i = 0; i < addedItems.length; i++) + this._box.insert_child_at_index(addedItems[i].item, + addedItems[i].pos); + + for (let i = 0; i < removedActors.length; i++) { + let item = removedActors[i]; + item.animateOutAndDestroy(); + } + + this._adjustIconSize(); + + for (let i = 0; i < addedItems.length; i++){ + // Emit a custom signal notifying that a new item has been added + this.emit('item-added', addedItems[i]); + } + + // Skip animations on first run when adding the initial set + // of items, to avoid all items zooming in at once + + let animate = this._shownInitially; + + if (!this._shownInitially) + this._shownInitially = true; + + for (let i = 0; i < addedItems.length; i++) { + addedItems[i].item.show(animate); + } + + // Workaround for https://bugzilla.gnome.org/show_bug.cgi?id=692744 + // Without it, StBoxLayout may use a stale size cache + this._box.queue_relayout(); + + // This is required for icon reordering when the scrollview is used. + this._updateAppIconsGeometry(); + }, + + // Reset the displayed apps icon to mantain the correct order + resetAppIcons : function() { + + let children = this._box.get_children().filter(function(actor) { + return actor.child && + actor.child._delegate && + actor.child._delegate.icon; + }); + for (let i = 0; i < children.length; i++) { + let item = children[i]; + item.destroy(); + } + + // to avoid ugly animations, just suppress them like when taskbar is first loaded. + this._shownInitially = false; + this._redisplay(); + + }, + + _clearDragPlaceholder: function() { + if (this._dragPlaceholder) { + this._animatingPlaceholdersCount++; + this._dragPlaceholder.animateOutAndDestroy(); + this._dragPlaceholder.connect('destroy', + Lang.bind(this, function() { + this._animatingPlaceholdersCount--; + })); + this._dragPlaceholder = null; + } + this._dragPlaceholderPos = -1; + }, + + _clearEmptyDropTarget: function() { + if (this._emptyDropTarget) { + this._emptyDropTarget.animateOutAndDestroy(); + this._emptyDropTarget = null; + } + }, + + handleDragOver : function(source, actor, x, y, time) { + let app = Dash.getAppFromSource(source); + + // Don't allow favoriting of transient apps + if (app == null || app.is_window_backed()) + return DND.DragMotionResult.NO_DROP; + + if (!this._settings.is_writable('favorite-apps')) + return DND.DragMotionResult.NO_DROP; + + let favorites = AppFavorites.getAppFavorites().getFavorites(); + let numFavorites = favorites.length; + + let favPos = favorites.indexOf(app); + + let children = this._box.get_children(); + let numChildren = children.length; + let boxHeight = 0; + for (let i = 0; i < numChildren; i++) { + boxHeight += children[i].width; + } + + // Keep the placeholder out of the index calculation; assuming that + // the remove target has the same size as "normal" items, we don't + // need to do the same adjustment there. + if (this._dragPlaceholder) { + boxHeight -= this._dragPlaceholder.width; + numChildren--; + } + + let pos; + if (!this._emptyDropTarget){ + pos = Math.floor(x * numChildren / boxHeight); + if (pos > numChildren) + pos = numChildren; + } else + pos = 0; // always insert at the top when taskbar is empty + + /* Take into account childredn position in rtl*/ + if (Clutter.get_default_text_direction() == Clutter.TextDirection.RTL) + pos = numChildren - pos; + + if (pos != this._dragPlaceholderPos && pos <= numFavorites && this._animatingPlaceholdersCount == 0) { + this._dragPlaceholderPos = pos; + + // Don't allow positioning before or after self + if (favPos != -1 && (pos == favPos || pos == favPos + 1)) { + this._clearDragPlaceholder(); + return DND.DragMotionResult.CONTINUE; + } + + // If the placeholder already exists, we just move + // it, but if we are adding it, expand its size in + // an animation + let fadeIn; + if (this._dragPlaceholder) { + this._dragPlaceholder.destroy(); + fadeIn = false; + } else { + fadeIn = true; + } + + this._dragPlaceholder = new Dash.DragPlaceholderItem(); + this._dragPlaceholder.child.set_width(this.iconSize); + this._dragPlaceholder.child.set_height(this.iconSize); + this._box.insert_child_at_index(this._dragPlaceholder, + this._dragPlaceholderPos); + this._dragPlaceholder.show(fadeIn); + // Ensure the next and previous icon are visible when moving the placeholder + // (I assume there's room for both of them) + if (this._dragPlaceholderPos > 1) + ensureActorVisibleInScrollView(this._scrollView, this._box.get_children()[this._dragPlaceholderPos-1]); + if (this._dragPlaceholderPos < this._box.get_children().length-1) + ensureActorVisibleInScrollView(this._scrollView, this._box.get_children()[this._dragPlaceholderPos+1]); + } + + // Remove the drag placeholder if we are not in the + // "favorites zone" + if (pos > numFavorites) + this._clearDragPlaceholder(); + + if (!this._dragPlaceholder) + return DND.DragMotionResult.NO_DROP; + + let srcIsFavorite = (favPos != -1); + + if (srcIsFavorite) + return DND.DragMotionResult.MOVE_DROP; + + return DND.DragMotionResult.COPY_DROP; + }, + + // Draggable target interface + acceptDrop : function(source, actor, x, y, time) { + + let app = Dash.getAppFromSource(source); + + // Don't allow favoriting of transient apps + if (app == null || app.is_window_backed()) { + return false; + } + + if (!this._settings.is_writable('favorite-apps')) + return false; + + let id = app.get_id(); + + let favorites = AppFavorites.getAppFavorites().getFavoriteMap(); + + let srcIsFavorite = (id in favorites); + + let favPos = 0; + let children = this._box.get_children(); + for (let i = 0; i < this._dragPlaceholderPos; i++) { + if (this._dragPlaceholder && + children[i] == this._dragPlaceholder) + continue; + + let childId = children[i].child._delegate.app.get_id(); + if (childId == id) + continue; + if (childId in favorites) + favPos++; + } + + // No drag placeholder means we don't wan't to favorite the app + // and we are dragging it to its original position + if (!this._dragPlaceholder) + return true; + + Meta.later_add(Meta.LaterType.BEFORE_REDRAW, Lang.bind(this, + function () { + let appFavorites = AppFavorites.getAppFavorites(); + if (srcIsFavorite) + appFavorites.moveFavoriteToPos(id, favPos); + else + appFavorites.addFavoriteAtPos(id, favPos); + return false; + })); + + return true; + } + +}); + +Signals.addSignalMethods(taskbar.prototype); + + +/** + * Extend AppIcon + * + * - Apply a css class based on the number of windows of each application (#N); + * - Draw a dot for each window of the application based on the default "dot" style which is hidden (#N); + * a class of the form "running#N" is applied to the AppWellIcon actor. + * like the original .running one. + * - add a .focused style to the focused app + * - Customize click actions. + * - Update minimization animation target + * + */ + +let tracker = Shell.WindowTracker.get_default(); + +const taskbarAppIcon = new Lang.Class({ + Name: 'taskbar.AppIcon', + Extends: AppDisplay.AppIcon, + + _init: function(app, iconParams, onActivateOverride) { + + this._nWindows = 0; + + this.parent(app, iconParams, onActivateOverride); + + this._dot.set_width(0); + this._focused = tracker.focus_app == this.app; + + // Monitor windows-changes instead of app state. + // Keep using the same Id and function callback (that is extended) + if(this._stateChangedId > 0) { + this.app.disconnect(this._stateChangedId); + this._stateChangedId = 0; + } + + this._stateChangedId = this.app.connect('windows-changed', + Lang.bind(this, this.onWindowsChanged)); + this._focuseAppChangeId = tracker.connect('notify::focus-app', + Lang.bind(this, this._onFocusAppChanged)); + + this._dots = null; + + this._showDots(); + + // Creating a new menu manager for window previews as adding it to the + // using the secondary menu's menu manager (which uses the "ignoreRelease" + // function) caused the extension to crash. + this._menuManagerWindowPreview = new PopupMenu.PopupMenuManager(this); + + this._windowPreview = new WindowPreview.thumbnailPreviewMenu(this); + this._windowPreview.connect('open-state-changed', Lang.bind(this, function (menu, isPoppedUp) { + if (!isPoppedUp) + this._onMenuPoppedDown(); + })); + this._menuManagerWindowPreview.addMenu(this._windowPreview); + }, + + shouldShowTooltip: function() { + let windows = getAppInterestingWindows(this.app); + if (windows.length > 0) { + return false; + } else { + return this.actor.hover && (!this._menu || !this._menu.isOpen) && (!this._windowPreview || !this._windowPreview.isOpen); + } + }, + + _onDestroy: function() { + this.parent(); + + // Disconect global signals + // stateChangedId is already handled by parent) + if(this._focusAppId>0) + tracker.disconnect(this._focusAppId); + }, + + onWindowsChanged: function() { + this._updateRunningStyle(); + this.updateIconGeometry(); + }, + + // Update taraget for minimization animation + updateIconGeometry: function() { + + // If (for unknown reason) the actor is not on the stage the reported size + // and position are random values, which might exceeds the integer range + // resulting in an error when assigned to the a rect. This is a more like + // a workaround to prevent flooding the system with errors. + if (this.actor.get_stage() == null) + return + + let rect = new Meta.Rectangle(); + + [rect.x, rect.y] = this.actor.get_transformed_position(); + [rect.width, rect.height] = this.actor.get_transformed_size(); + + let windows = this.app.get_windows(); + windows.forEach(function(w) { + w.set_icon_geometry(rect); + }); + + }, + + _showDots: function() { + // Just update style if dots already exist + if (this._dots) { + this._updateCounterClass(); + return; + } + + this._dots = new St.DrawingArea({x_expand: true, y_expand: true}); + this._dots.connect('repaint', Lang.bind(this, + function() { + this._drawCircles(this._dots); + this._onFocusAppChanged(); + })); + this._iconContainer.add_child(this._dots); + this._updateCounterClass(); + + }, + + _updateRunningStyle: function() { + this._updateCounterClass(); + }, + + popupMenu: function() { + this._removeMenuTimeout(); + this.actor.fake_release(); + this._draggable.fakeRelease(); + + if (!this._menu) { + this._menu = new SecondaryMenu.taskbarSecondaryMenu(this); + this._menu.connect('activate-window', Lang.bind(this, function (menu, window) { + this.activateWindow(window); + })); + this._menu.connect('open-state-changed', Lang.bind(this, function (menu, isPoppedUp) { + if (!isPoppedUp) + this._onMenuPoppedDown(); + })); + let id = Main.overview.connect('hiding', Lang.bind(this, function () { this._menu.close(); })); + this._menu.actor.connect('destroy', function() { + Main.overview.disconnect(id); + }); + + this._menuManager.addMenu(this._menu); + } + + this.emit('menu-state-changed', true); + + this._windowPreview.shouldOpen = false; + this._windowPreview.close(); + + this.actor.set_hover(true); + this._menu.actor.add_style_class_name('taskbarSecondaryMenu'); + this._menu.popup(); + this._menuManager.ignoreRelease(); + this.emit('sync-tooltip'); + + return false; + }, + + _onFocusAppChanged: function() { + if(tracker.focus_app == this.app) { + this._dot.opacity = 255; + Tweener.addTween(this._dot, + { width: this._iconContainer.get_width(), + height: RUNNING_INDICATOR_SIZE, + time: DASH_ANIMATION_TIME, + transition: 'easeInOutCubic', + }); + Tweener.addTween(this._dots, + { opacity: 0, + time: DASH_ANIMATION_TIME, + transition: 'easeInOutCubic', + }); + } else { + this._dot.opacity = 255; + Tweener.addTween(this._dot, + { width: 0, + height: RUNNING_INDICATOR_SIZE, + time: DASH_ANIMATION_TIME, + transition: 'easeInOutCubic', + }); + Tweener.addTween(this._dots, + { opacity: 255, + time: DASH_ANIMATION_TIME, + transition: 'easeInOutCubic', + }); + } + }, + + activate: function(button) { + this._windowPreview.shouldOpen = false; + this._windowPreview.requestCloseMenu(); + + let event = Clutter.get_current_event(); + let modifiers = event ? event.get_state() : 0; + let openNewWindow = modifiers & Clutter.ModifierType.CONTROL_MASK && + this.app.state == Shell.AppState.RUNNING || + button && button == 2; + let focusedApp = tracker.focus_app; + + if (this.app.state == Shell.AppState.STOPPED || openNewWindow) + this.animateLaunch(); + + if (button && button == 1 && this.app.state == Shell.AppState.RUNNING) { + + if (modifiers & Clutter.ModifierType.CONTROL_MASK){ + // Keep default behaviour: launch new window + // By calling the parent method I make it compatible + // with other extensions tweaking ctrl + click + this.animateLaunch(); + this.app.open_new_window(-1); + return; + + } else if (this.app == focusedApp && !Main.overview._shown){ + minimizeWindow(this.app, true); + } else { + // If click minimizes all, then one expects all windows to be reshown + activateAllWindows(this.app); + } + } else { + // Default behaviour + if (openNewWindow) + this.app.open_new_window(-1); + else + this.app.activate(); + } + + Main.overview.hide(); + }, + + _updateCounterClass: function() { + + let maxN = 4; + this._nWindows = Math.min(getAppInterestingWindows(this.app).length, maxN); + + for (let i = 1; i <= maxN; i++){ + let className = 'running'+i; + if(i != this._nWindows) + this.actor.remove_style_class_name(className); + else + this.actor.add_style_class_name(className); + } + + if (this._dots) + this._dots.queue_repaint(); + }, + + _drawCircles: function(area) { + // Re-use the style - background color, and border width and color - + // of the default dot + let themeNode = this._dot.get_theme_node(); + let bodyColor = themeNode.get_background_color(); + + let [width, height] = area.get_surface_size(); + let cr = area.get_context(); + + // Draw the required numbers of dots + let radius = RUNNING_INDICATOR_SIZE/2; + let padding = 0; // distance from the margin + let spacing = width/22; // separation between the dots + let n = this._nWindows; + + Clutter.cairo_set_source_color(cr, bodyColor); + + cr.translate((width - (2*n)*radius - (n-1)*spacing)/2, height- padding- 2*radius); + for (let i = 0; i < n; i++) { + cr.newSubPath(); + cr.arc((2*i+1)*radius + i*spacing, radius, radius, 0, 2*Math.PI); + } + + cr.fill(); + cr.$dispose(); + } + +}); + +function minimizeWindow(app, param){ + // Param true make all app windows minimize + let windows = getAppInterestingWindows(app); + let current_workspace = global.screen.get_active_workspace(); + for (let i = 0; i < windows.length; i++) { + let w = windows[i]; + if (w.get_workspace() == current_workspace && w.showing_on_its_workspace()){ + w.minimize(); + // Just minimize one window. By specification it should be the + // focused window on the current workspace. + if(!param) + break; + } + } +} + +/* + * By default only non minimized windows are activated. + * This activates all windows in the current workspace. + */ +function activateAllWindows(app){ + + // First activate first window so workspace is switched if needed, + // then activate all other app windows in the current workspace. + let windows = getAppInterestingWindows(app); + let w = windows[0]; + Main.activateWindow(w); + let activeWorkspace = global.screen.get_active_workspace_index(); + + if (windows.length <= 0) + return; + + let activatedWindows = 0; + + for (let i = windows.length - 1; i >= 0; i--){ + if (windows[i].get_workspace().index() == activeWorkspace){ + Main.activateWindow(windows[i]); + activatedWindows++; + } + } +} + +function getAppInterestingWindows(app) { + // Filter out unnecessary windows, for instance + // nautilus desktop window. + let windows = app.get_windows().filter(function(w) { + return !w.skip_taskbar; + }); + + return windows; +} + + +/* + * This is a copy of the same function in utils.js, but also adjust horizontal scrolling + * and perform few further cheks on the current value to avoid changing the values when + * it would be clamp to the current one in any case. + * Return the amount of shift applied +*/ +function ensureActorVisibleInScrollView(scrollView, actor) { + + let adjust_v = true; + let adjust_h = true; + + let vadjustment = scrollView.vscroll.adjustment; + let hadjustment = scrollView.hscroll.adjustment; + let [vvalue, vlower, vupper, vstepIncrement, vpageIncrement, vpageSize] = vadjustment.get_values(); + let [hvalue, hlower, hupper, hstepIncrement, hpageIncrement, hpageSize] = hadjustment.get_values(); + + let [hvalue0, vvalue0] = [hvalue, vvalue]; + + let voffset = 0; + let hoffset = 0; + let fade = scrollView.get_effect("fade"); + if (fade){ + voffset = fade.vfade_offset; + hoffset = fade.hfade_offset; + } + + let box = actor.get_allocation_box(); + let y1 = box.y1, y2 = box.y2, x1 = box.x1, x2 = box.x2; + + let parent = actor.get_parent(); + while (parent != scrollView) { + if (!parent) + throw new Error("actor not in scroll view"); + + let box = parent.get_allocation_box(); + y1 += box.y1; + y2 += box.y1; + x1 += box.x1; + x2 += box.x1; + parent = parent.get_parent(); + } + + if (y1 < vvalue + voffset) + vvalue = Math.max(0, y1 - voffset); + else if (vvalue < vupper - vpageSize && y2 > vvalue + vpageSize - voffset) + vvalue = Math.min(vupper -vpageSize, y2 + voffset - vpageSize); + + if (x1 < hvalue + hoffset) + hvalue = Math.max(0, x1 - hoffset); + else if (hvalue < hupper - hpageSize && x2 > hvalue + hpageSize - hoffset) + hvalue = Math.min(hupper - hpageSize, x2 + hoffset - hpageSize); + + if (vvalue !== vvalue0) { + Tweener.addTween(vadjustment, + { value: vvalue, + time: Util.SCROLL_TIME, + transition: 'easeOutQuad' }); + } + + if (hvalue !== hvalue0) { + Tweener.addTween(hadjustment, + { value: hvalue, + time: Util.SCROLL_TIME, + transition: 'easeOutQuad' }); + } + + return [hvalue- hvalue0, vvalue - vvalue0]; +} diff --git a/windowPreview.js b/windowPreview.js new file mode 100644 index 0000000..fd18092 --- /dev/null +++ b/windowPreview.js @@ -0,0 +1,640 @@ +/* + * Taskbar: A taskbar extension for the Gnome panel. + * Copyright (C) 2016 Zorin OS + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * + * Credits: + * This file is based on code from the Dash to Dock extension by micheleg. + * Some code was also adapted from the upstream Gnome Shell source code. + */ + + +const Clutter = imports.gi.Clutter; +const GLib = imports.gi.GLib; +const Gtk = imports.gi.Gtk; +const Lang = imports.lang; +const Main = imports.ui.main; +const Mainloop = imports.mainloop; +const PopupMenu = imports.ui.popupMenu; +const Signals = imports.signals; +const St = imports.gi.St; +const Tweener = imports.ui.tweener; +const Workspace = imports.ui.workspace; + +const Me = imports.misc.extensionUtils.getCurrentExtension(); +const Taskbar = Me.imports.taskbar; + +let THUMBNAIL_WIDTH = 350; +let THUMBNAIL_HEIGHT = 200; + +const thumbnailPreviewMenu = new Lang.Class({ + Name: 'thumbnailPreviewMenu', + Extends: PopupMenu.PopupMenu, + + _init: function(source) { + + let side = Taskbar.getPosition(); + + this.parent(source.actor, 0.5, side); + + // We want to keep the item hovered while the menu is up + this.blockSourceEvents = false; + + this._source = source; + this._app = this._source.app; + this.shouldOpen = true; + this.shouldClose = false; + + this.actor.add_style_class_name('app-well-menu'); + this.actor.set_style("max-width: " + (Main.layoutManager.primaryMonitor.width - 22) + "px;"); + this.actor.hide(); + + // Chain our visibility and lifecycle to that of the source + source.actor.connect('notify::mapped', Lang.bind(this, function () { + if (!source.actor.mapped) + this.close(); + })); + source.actor.connect('destroy', Lang.bind(this, this.destroy)); + + Main.uiGroup.add_actor(this.actor); + + source.actor.connect('enter-event', Lang.bind(this, this._onEnter)); + source.actor.connect('leave-event', Lang.bind(this, this._onLeave)); + + this.actor.connect('enter-event', Lang.bind(this, this._onMenuEnter)); + this.actor.connect('leave-event', Lang.bind(this, this._onMenuLeave)); + + // Change the initialized side where required. + this._arrowSide = side; + this._boxPointer._arrowSide = side; + this._boxPointer._userArrowSide = side; + + this._previewBox = new thumbnailPreviewList(this._app, THUMBNAIL_HEIGHT); + this.addMenuItem(this._previewBox); + }, + + requestCloseMenu: function() { + // The "~0" argument makes the animation display. + this.close(~0); + }, + + _redisplay: function() { + this._previewBox._shownInitially = false; + this._previewBox._redisplay(); + }, + + popup: function() { + let windows = Taskbar.getAppInterestingWindows(this._app); + if (windows.length > 0) { + this._redisplay(); + this.open(); + this._source.emit('sync-tooltip'); + } + }, + + _onMenuEnter: function () { + this.shouldOpen = true; + this.shouldClose = false; + + Mainloop.timeout_add(Taskbar.DASH_ITEM_HOVER_TIMEOUT, Lang.bind(this, this.hoverOpen)); + }, + + _onMenuLeave: function () { + this.shouldOpen = false; + this.shouldClose = true; + Mainloop.timeout_add(Taskbar.DASH_ITEM_HOVER_TIMEOUT, Lang.bind(this, this.hoverClose)); + }, + + _onEnter: function () { + this.shouldOpen = true; + this.shouldClose = false; + + Mainloop.timeout_add(Taskbar.DASH_ITEM_HOVER_TIMEOUT, Lang.bind(this, this.hoverOpen)); + }, + + _onLeave: function () { + this.shouldClose = true; + this.shouldOpen = false; + + Mainloop.timeout_add(0, Lang.bind(this, this.hoverClose)); + }, + + hoverOpen: function () { + if (this.shouldOpen && !this.isOpen) { + this.popup(); + } + }, + + hoverClose: function () { + if (this.shouldClose) { + this.close(~0); + } + }, + + close: function(animate) { + if (this.isOpen) + this.emit('open-state-changed', false); + if (this._activeMenuItem) + this._activeMenuItem.setActive(false); + + if (this._boxPointer.actor.visible) { + this._boxPointer.hide(animate, Lang.bind(this, function() { + this.emit('menu-closed'); + })); + } + + this.isOpen = false; + } + +}); + +const thumbnailPreview = new Lang.Class({ + Name: 'thumbnailPreview', + Extends: PopupMenu.PopupBaseMenuItem, + + _init: function(window) { + this.window = window; + + this.parent({reactive: true}); + this._workId = Main.initializeDeferredWork(this.actor, Lang.bind(this, this._onResize)); + this._closeButtonId = Main.initializeDeferredWork(this.actor, Lang.bind(this, this._repositionCloseButton)); + this.scale = 0; + + this.preview = this.getThumbnail(); + + this.actor.remove_child(this._ornamentLabel); + this.actor._delegate = this; + + this.animatingOut = false; + + this._windowBox = new St.BoxLayout({ style_class: 'window-box', + x_expand: true, + vertical: true }); + + this._previewBin = new St.Bin(); + this._previewBin.set_size(THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT); + + this._closeButton = new St.Button({ style_class: 'window-close', + accessible_name: "Close window" }); + this._closeButton.opacity = 0; + this._closeButton.connect('clicked', Lang.bind(this, this._closeWindow)); + + this.overlayGroup = new Clutter.Actor({layout_manager: new Clutter.BinLayout()}); + this.overlayGroup.add_actor(this._previewBin); + this.overlayGroup.add_actor(this._closeButton); + + this._title = new St.Label({ text: window.title }); + this._titleBin = new St.Bin({ child: this._title, + x_align: St.Align.MIDDLE, + width: THUMBNAIL_WIDTH + }); + this._titleBin.add_style_class_name("preview-window-title"); + + this.window.connect('notify::title', Lang.bind(this, function() { + this._title.set_text(this.window.title); + })); + + this._windowBin = new St.Bin({ child: this.overlayGroup, + x_align: St.Align.MIDDLE, + width: THUMBNAIL_WIDTH, + height: THUMBNAIL_HEIGHT + }); + + this._windowBox.add_child(this._windowBin); + + if (this.preview) + this._previewBin.set_child(this.preview); + this._windowBox.add_child(this._titleBin); + this.actor.add_child(this._windowBox); + this._queueRepositionCloseButton(); + + this.actor.connect('enter-event', + Lang.bind(this, this._onEnter)); + this.actor.connect('leave-event', + Lang.bind(this, this._onLeave)); + this.actor.connect('key-focus-in', + Lang.bind(this, this._onEnter)); + this.actor.connect('key-focus-out', + Lang.bind(this, this._onLeave)); + }, + + _onEnter: function() { + this._showCloseButton(); + return Clutter.EVENT_PROPAGATE; + }, + + _onLeave: function() { + if (!this._previewBin.has_pointer && + !this._closeButton.has_pointer) + this._hideCloseButton(); + + return Clutter.EVENT_PROPAGATE; + }, + + _idleToggleCloseButton: function() { + this._idleToggleCloseId = 0; + + if (!this._previewBin.has_pointer && + !this._closeButton.has_pointer) + this._hideCloseButton(); + + return GLib.SOURCE_REMOVE; + }, + + _showCloseButton: function() { + if (this._windowCanClose()) { + this._closeButton.show(); + Tweener.addTween(this._closeButton, + { opacity: 255, + time: Workspace.CLOSE_BUTTON_FADE_TIME, + transition: 'easeOutQuad' }); + } + }, + + _windowCanClose: function() { + return this.window.can_close() && + !this._hasAttachedDialogs(); + }, + + _hasAttachedDialogs: function() { + // count trasient windows + let n = 0; + this.window.foreach_transient(function() {n++;}); + return n > 0; + }, + + _hideCloseButton: function() { + Tweener.addTween(this._closeButton, + { opacity: 0, + time: Workspace.CLOSE_BUTTON_FADE_TIME, + transition: 'easeInQuad' }); + }, + + getThumbnail: function() { + let thumbnail = null; + let mutterWindow = this.window.get_compositor_private(); + if (mutterWindow) { + let windowTexture = mutterWindow.get_texture(); + let [width, height] = windowTexture.get_size(); + this.scale = Math.min(1.0, THUMBNAIL_WIDTH / width, THUMBNAIL_HEIGHT / height); + thumbnail = new Clutter.Clone ({ source: windowTexture, + reactive: true, + width: width * this.scale, + height: height * this.scale }); + this._resizeId = mutterWindow.meta_window.connect('size-changed', + Lang.bind(this, this._queueResize)); + this._destroyId = mutterWindow.connect('destroy', Lang.bind(this, function() { + thumbnail.destroy(); + this._destroyId = 0; + this.animateOutAndDestroy(); + })); + } + + return thumbnail; + }, + + _queueResize: function () { + Main.queueDeferredWork(this._workId); + }, + + _onResize: function() { + let [width, height] = this.preview.get_source().get_size(); + this.scale = Math.min(1.0, THUMBNAIL_WIDTH / width, THUMBNAIL_HEIGHT / height); + this.preview.set_size(width * this.scale, height * this.scale); + + this._queueRepositionCloseButton(); + }, + + _queueRepositionCloseButton: function () { + Main.queueDeferredWork(this._closeButtonId); + }, + + _repositionCloseButton: function() { + let rect = this.window.get_compositor_private().meta_window.get_frame_rect(); + let cloneWidth = Math.floor(rect.width) * this.scale; + let cloneHeight = Math.floor(rect.height) * this.scale; + + let cloneX = (THUMBNAIL_WIDTH - cloneWidth) / 2 ; + let cloneY = (THUMBNAIL_HEIGHT - cloneHeight) / 2; + + let buttonX; + if (Clutter.get_default_text_direction() == Clutter.TextDirection.RTL) { + buttonX = cloneX - (this._closeButton.width / 2); + buttonX = Math.max(buttonX, 0); + } else { + buttonX = cloneX + (cloneWidth - (this._closeButton.width / 2)); + buttonX = Math.min(buttonX, THUMBNAIL_WIDTH - this._closeButton.width); + } + + let buttonY = cloneY - (this._closeButton.height / 2); + buttonY = Math.max(buttonY, 0); + + this._closeButton.set_position(Math.floor(buttonX), Math.floor(buttonY)); + }, + + _closeWindow: function() { + this.window.delete(global.get_current_time()); + }, + + show: function(animate) { + let fullWidth = this.actor.get_width(); + + this.actor.opacity = 0; + this.actor.set_width(0); + + let time = animate ? Taskbar.DASH_ANIMATION_TIME : 0; + Tweener.addTween(this.actor, + { opacity: 255, + width: fullWidth, + time: time, + transition: 'easeInOutQuad' + }); + }, + + animateOutAndDestroy: function() { + this.animatingOut = true; + this._hideCloseButton(); + Tweener.addTween(this.actor, + { width: 0, + opacity: 0, + time: Taskbar.DASH_ANIMATION_TIME, + transition: 'easeOutQuad', + onComplete: Lang.bind(this, function() { + this.destroy(); + }) + }); + }, + + activate: function() { + Main.activateWindow(this.window); + this._getTopMenu().close(~0); + } +}); + +const thumbnailPreviewList = new Lang.Class({ + Name: 'thumbnailPreviewList', + Extends: PopupMenu.PopupMenuSection, + + _init: function(app) { + this.parent(); + + this._ensurePreviewVisibilityTimeoutId = 0; + + this.actor = new St.ScrollView({ name: 'thumbnailPreviewScrollview', + hscrollbar_policy: Gtk.PolicyType.NEVER, + vscrollbar_policy: Gtk.PolicyType.NEVER, + enable_mouse_scrolling: true }); + + this.actor.connect('scroll-event', Lang.bind(this, this._onScrollEvent )); + + this.box.set_vertical(false); + this.box.set_name("thumbnailPreviewList"); + this.actor.add_actor(this.box); + this.actor._delegate = this; + + this._shownInitially = false; + + this.app = app; + + this._redisplayId = Main.initializeDeferredWork(this.actor, Lang.bind(this, this._redisplay)); + this._scrollbarId = Main.initializeDeferredWork(this.actor, Lang.bind(this, this._showHideScrollbar)); + + if (this._stateChangedId > 0) { + this.app.disconnect(this._stateChangedId); + this._stateChangedId = 0; + } + + this._stateChangedId = this.app.connect('windows-changed', + Lang.bind(this, + this._queueRedisplay)); + }, + + _needsScrollbar: function() { + let topMenu = this._getTopMenu(); + let [topMinWidth, topNaturalWidth] = topMenu.actor.get_preferred_width(-1); + let topThemeNode = topMenu.actor.get_theme_node(); + + let topMaxWidth = topThemeNode.get_max_width(); + return topMaxWidth >= 0 && topNaturalWidth >= topMaxWidth; + }, + + _showHideScrollbar: function() { + let needsScrollbar = this._needsScrollbar(); + + // St.ScrollView always requests space vertically for a possible horizontal + // scrollbar if in AUTOMATIC mode. This looks bad when we *don't* need it, + // so turn off the scrollbar when that's true. Dynamic changes in whether + // we need it aren't handled properly. + + this.actor.hscrollbar_policy = + needsScrollbar ? Gtk.PolicyType.AUTOMATIC : Gtk.PolicyType.NEVER; + + if (needsScrollbar) + this.actor.add_style_pseudo_class('scrolled'); + else + this.actor.remove_style_pseudo_class('scrolled'); + }, + + _queueScrollbar: function () { + Main.queueDeferredWork(this._scrollbarId); + }, + + _queueRedisplay: function () { + Main.queueDeferredWork(this._redisplayId); + }, + + _onScrollEvent: function(actor, event) { + // Event coordinates are relative to the stage but can be transformed + // as the actor will only receive events within his bounds. + let stage_x, stage_y, ok, event_x, event_y, actor_w, actor_h; + [stage_x, stage_y] = event.get_coords(); + [ok, event_x, event_y] = actor.transform_stage_point(stage_x, stage_y); + [actor_w, actor_h] = actor.get_size(); + + // If the scroll event is within a 1px margin from + // the relevant edge of the actor, let the event propagate. + if (event_y >= actor_h - 2) + return Clutter.EVENT_PROPAGATE; + + // reset timeout to avid conflicts with the mousehover event + if (this._ensurePreviewVisibilityTimeoutId>0) { + Mainloop.source_remove(this._ensurePreviewVisibilityTimeoutId); + this._ensurePreviewVisibilityTimeoutId = 0; + } + + // Skip to avoid double events mouse + if (event.is_pointer_emulated()) + return Clutter.EVENT_STOP; + + let adjustment, delta; + + adjustment = this.actor.get_hscroll_bar().get_adjustment(); + + let increment = adjustment.step_increment; + + switch ( event.get_scroll_direction() ) { + case Clutter.ScrollDirection.UP: + delta = -increment; + break; + case Clutter.ScrollDirection.DOWN: + delta = +increment; + break; + case Clutter.ScrollDirection.SMOOTH: + let [dx, dy] = event.get_scroll_delta(); + delta = dy*increment; + delta += dx*increment; + break; + + } + + adjustment.set_value(adjustment.get_value() + delta); + + return Clutter.EVENT_STOP; + + }, + + _createPreviewItem: function(window) { + let preview = new thumbnailPreview(window); + + + preview.actor.connect('notify::hover', Lang.bind(this, function() { + if (preview.actor.hover){ + this._ensurePreviewVisibilityTimeoutId = Mainloop.timeout_add(100, Lang.bind(this, function(){ + Taskbar.ensureActorVisibleInScrollView(this.actor, preview.actor); + this._ensurePreviewVisibilityTimeoutId = 0; + return GLib.SOURCE_REMOVE; + })); + } else { + if (this._ensurePreviewVisibilityTimeoutId>0) { + Mainloop.source_remove(this._ensurePreviewVisibilityTimeoutId); + this._ensurePreviewVisibilityTimeoutId = 0; + } + } + })); + + preview.actor.connect('key-focus-in', + Lang.bind(this, function(actor) { + + let [x_shift, y_shift] = Taskbar.ensureActorVisibleInScrollView(this.actor, actor); + })); + + return preview; + }, + + _redisplay: function () { + let windows = Taskbar.getAppInterestingWindows(this.app).sort(this.sortWindowsCompareFunction); + let children = this.box.get_children().filter(function(actor) { + return actor._delegate.window && actor._delegate.preview; + }); + // Apps currently in the taskbar + let oldWin = children.map(function(actor) { + return actor._delegate.window; + }); + // Apps supposed to be in the taskbar + let newWin = windows; + + let addedItems = []; + let removedActors = []; + + let newIndex = 0; + let oldIndex = 0; + + while (newIndex < newWin.length || oldIndex < oldWin.length) { + // No change at oldIndex/newIndex + if (oldWin[oldIndex] == newWin[newIndex]) { + oldIndex++; + newIndex++; + continue; + } + + // Window removed at oldIndex + if (oldWin[oldIndex] && + newWin.indexOf(oldWin[oldIndex]) == -1) { + removedActors.push(children[oldIndex]); + oldIndex++; + continue; + } + + // Window added at newIndex + if (newWin[newIndex] && + oldWin.indexOf(newWin[newIndex]) == -1) { + addedItems.push({ item: this._createPreviewItem(newWin[newIndex]), + pos: newIndex }); + newIndex++; + continue; + } + + // Window moved + let insertHere = newWin[newIndex + 1] && + newWin[newIndex + 1] == oldWin[oldIndex]; + let alreadyRemoved = removedActors.reduce(function(result, actor) { + let removedWin = actor.window; + return result || removedWin == newWin[newIndex]; + }, false); + + if (insertHere || alreadyRemoved) { + addedItems.push({ item: this._createPreviewItem(newWin[newIndex]), + pos: newIndex + removedActors.length }); + newIndex++; + } else { + removedActors.push(children[oldIndex]); + oldIndex++; + } + } + + for (let i = 0; i < addedItems.length; i++) + this.addMenuItem(addedItems[i].item, + addedItems[i].pos); + + for (let i = 0; i < removedActors.length; i++) { + let item = removedActors[i]; + item._delegate.animateOutAndDestroy(); + } + + // Skip animations on first run when adding the initial set + // of items, to avoid all items zooming in at once + + let animate = this._shownInitially; + + if (!this._shownInitially) + this._shownInitially = true; + + for (let i = 0; i < addedItems.length; i++) { + addedItems[i].item.show(animate); + } + + this._queueScrollbar(); + + // Workaround for https://bugzilla.gnome.org/show_bug.cgi?id=692744 + // Without it, StBoxLayout may use a stale size cache + this.box.queue_relayout(); + + if (windows.length < 1) { + this._getTopMenu().close(~0); + } + }, + + isAnimatingOut: function() { + return this.actor.get_children().reduce(function(result, actor) { + return result || actor.animatingOut; + }, false); + }, + + sortWindowsCompareFunction: function(windowA, windowB) { + return windowA.get_stable_sequence() > windowB.get_stable_sequence(); + } + +});