diff --git a/lib-core.sh b/lib-core.sh index 883eea5..82d7640 100755 --- a/lib-core.sh +++ b/lib-core.sh @@ -50,6 +50,7 @@ THEME_SRC_DIR="${REPO_DIR}/src" DASH_TO_DOCK_SRC_DIR="${REPO_DIR}/src/other/dash-to-dock" DASH_TO_DOCK_DIR_ROOT="/usr/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com" DASH_TO_DOCK_DIR_HOME="${MY_HOME}/.local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com" +GNOME_SHELL_EXTENSION_DIR="${MY_HOME}/.local/share/gnome-shell/extensions" FIREFOX_SRC_DIR="${REPO_DIR}/src/other/firefox" FIREFOX_DIR_HOME="${MY_HOME}/.mozilla/firefox" FIREFOX_THEME_DIR="${MY_HOME}/.mozilla/firefox/firefox-themes" @@ -615,7 +616,7 @@ avoid_variant_duplicates() { ############################################################################### restore_file() { - if [[ -f "${1}.bak" ]]; then + if [[ -f "${1}.bak" || -d "${1}.bak" ]]; then case "${2}" in sudo) sudo rm -rf "${1}"; sudo mv "${1}"{".bak",""} ;; @@ -628,7 +629,7 @@ restore_file() { } backup_file() { - if [[ -f "${1}" ]]; then + if [[ -f "${1}" || -d "${1}" ]]; then case "${2}" in sudo) sudo mv -n "${1}"{"",".bak"} ;; diff --git a/lib-flatpak.sh b/lib-flatpak.sh new file mode 100755 index 0000000..1479441 --- /dev/null +++ b/lib-flatpak.sh @@ -0,0 +1,102 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +[[ -n "$PAKITHEME_VERBOSE" ]] && set -x ||: + +die() { + echo "$@" >&2 + exit 1 +} + +pakitheme() { + local color="$(destify ${1})" + local opacity="$(destify ${2})" + local alt="$(destify ${3})" + local theme="$(destify ${4})" + + local FLATPAK_THEME="${name}${color}${opacity}${alt}${theme}" + + local GTK_THEME_VER=3.22 + local cache_home="${XDG_CACHE_HOME:-$HOME/.cache}" + local data_home="${XDG_DATA_HOME:-$HOME/.local/share}" + local pakitheme_cache="$cache_home/pakitheme" + local repo_dir="$pakitheme_cache/repo" + local app_id="org.gtk.Gtk3theme.$FLATPAK_THEME" + local root_dir="$pakitheme_cache/$FLATPAK_THEME" + local repo_dir="$root_dir/repo" + local build_dir="$root_dir/build" + + prompt -i "Converting theme: $FLATPAK_THEME" + + for location in "$data_home/themes" "$HOME/.themes" /usr/share/themes; do + if [[ -d "$location/$FLATPAK_THEME" ]]; then + prompt -s "Found theme located at: $location/$FLATPAK_THEME" + theme_path="$location/$FLATPAK_THEME" + break + fi + done + + [[ -n "$theme_path" ]] || die 'Could not locate theme.' + + rm -rf "$root_dir" "$repo_dir" + mkdir -p "$repo_dir" + ostree --repo="$repo_dir" init --mode=archive + ostree --repo="$repo_dir" config set core.min-free-space-percent 0 + + rm -rf "$build_dir" + mkdir -p "$build_dir/files" + + theme_gtk_version=$(ls -1d "$theme_path"/* 2>/dev/null | grep -Po 'gtk-3\.\K\d+$' | sort -nr | head -1) + [[ -n "$theme_gtk_version" ]] || \ + die "Theme directory did not contain any recognized GTK themes." + + cp -a "$theme_path/gtk-3.$theme_gtk_version/"* "$build_dir/files" + + mkdir -p "$build_dir/files/share/appdata" + cat >"$build_dir/files/share/appdata/$app_id.appdata.xml" < + + $app_id + CC0-1.0 + $flatpak_name Gtk theme + $flatpak_name Gtk theme (generated via pakitheme) + +EOF + + appstream-compose --prefix="$build_dir/files" --basename="$app_id" --origin=flatpak "$app_id" + + ostree --repo="$repo_dir" commit -b base --tree=dir="$build_dir" + + bundles=() + + while read -r arch; do + bundle="$root_dir/$app_id-$arch.flatpak" + + rm -rf "$build_dir" + ostree --repo="$repo_dir" checkout -U base "$build_dir" + + read -rd '' metadata < "$build_dir/metadata" + + ostree --repo="$repo_dir" commit -b "runtime/$app_id/$arch/$GTK_THEME_VER" \ + --add-metadata-string "xa.metadata=$(cat $build_dir/metadata)" --link-checkout-speedup "$build_dir" + flatpak build-bundle --runtime "$repo_dir" "$bundle" "$app_id" "$GTK_THEME_VER" + + trap 'rm "$bundle"' EXIT + + bundles+=("$bundle") + # Note: a pipe can't be used because it will mess with subshells and cause the append + # to bundles to fail. + done < <(flatpak list --runtime --columns=arch:f | sort -u) + + for bundle in "${bundles[@]}"; do + flatpak install -y --$install_target "${bundle}" + done +} diff --git a/lib-install.sh b/lib-install.sh index 24b9f2f..7e6e635 100755 --- a/lib-install.sh +++ b/lib-install.sh @@ -9,6 +9,7 @@ ############################################################################### source "${REPO_DIR}/lib-core.sh" +source "${REPO_DIR}/lib-flatpak.sh" WHITESUR_SOURCE+=("lib-install.sh") ############################################################################### @@ -655,6 +656,16 @@ remove_firefox_theme() { # DASH TO DOCK # ############################################################################### +install_dash_to_dock() { + if [[ -d "${DASH_TO_DOCK_DIR_HOME}" ]]; then + backup_file "${DASH_TO_DOCK_DIR_HOME}" "udo" + rm -rf "${DASH_TO_DOCK_DIR_HOME}" + fi + + udo cp -rf "${DASH_TO_DOCK_SRC_DIR}/dash-to-dock@micxgx.gmail.com" "${GNOME_SHELL_EXTENSION_DIR}" + udo dbus-launch dconf write /org/gnome/shell/extensions/dash-to-dock/apply-custom-theme true +} + install_dash_to_dock_theme() { gtk_base "${colors[0]}" "${opacities[0]}" "${themes[0]}" @@ -670,6 +681,12 @@ install_dash_to_dock_theme() { udo dbus-launch dconf write /org/gnome/shell/extensions/dash-to-dock/apply-custom-theme true } +revert_dash_to_dock() { + if [[ -d "${DASH_TO_DOCK_DIR_HOME}.bak" ]]; then + restore_file "${DASH_TO_DOCK_DIR_HOME}" "udo" + fi +} + revert_dash_to_dock_theme() { if [[ -d "${DASH_TO_DOCK_DIR_HOME}" ]]; then restore_file "${DASH_TO_DOCK_DIR_HOME}/stylesheet.css" "udo" @@ -684,12 +701,46 @@ revert_dash_to_dock_theme() { # FLATPAK & SNAP # ############################################################################### +flatpak_remove() { + local color="$(destify ${1})" + local opacity="$(destify ${2})" + local alt="$(destify ${3})" + local theme="$(destify ${4})" + + if [[ -w "/root" ]]; then + flatpak remove -y --system org.gtk.Gtk3theme.${THEME_NAME}${color}${opacity}${alt}${theme} + else + flatpak remove -y --user org.gtk.Gtk3theme.${THEME_NAME}${color}${opacity}${alt}${theme} + fi +} + connect_flatpak() { - sudo flatpak override --filesystem=~/.themes + if [[ -w "/root" ]]; then + install_target=system + else + install_target=user + fi + for opacity in "${opacities[@]}"; do + for alt in "${alts[@]}"; do + for theme in "${themes[@]}"; do + for color in "${colors[@]}"; do + pakitheme "${color}" "${opacity}" "${alt}" "${theme}" + done + done + done + done } disconnect_flatpak() { - sudo flatpak override --nofilesystem=~/.themes + for opacity in "${opacities[@]}"; do + for alt in "${alts[@]}"; do + for theme in "${themes[@]}"; do + for color in "${colors[@]}"; do + flatpak_remove "${color}" "${opacity}" "${alt}" "${theme}" + done + done + done + done } connect_snap() { diff --git a/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/COPYING b/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/COPYING new file mode 100644 index 0000000..d159169 --- /dev/null +++ b/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/COPYING @@ -0,0 +1,339 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 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 Lesser 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., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 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 Lesser General +Public License instead of this License. diff --git a/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/README.md b/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/README.md new file mode 100644 index 0000000..1c76c69 --- /dev/null +++ b/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/README.md @@ -0,0 +1,45 @@ +# Dash to Dock +![screenshot](https://github.com/micheleg/dash-to-dock/raw/master/media/screenshot.jpg) + +## A dock for the GNOME Shell +This extension enhances the dash moving it out of the overview and transforming it in a dock for an easier launching of applications and a faster switching between windows and desktops without having to leave the desktop view. + +[](https://extensions.gnome.org/extension/307/dash-to-dock) + +For additional installation instructions and more information visit [https://micheleg.github.io/dash-to-dock/](https://micheleg.github.io/dash-to-dock/). + +## Installation from source + +The extension can be installed directly from source, either for the convenience of using git or to test the latest development version. Clone the desired branch with git + +### Build Dependencies + +To compile the stylesheet you'll need an implementation of SASS. Dash to Dock supports `dart-sass` (`sass`), `sassc`, and `ruby-sass`. Every distro should have at least one of these implementations, we recommend using `dart-sass` (`sass`) or `sassc` over `ruby-sass` as `ruby-sass` is deprecated. + +By default, Dash to Dock will attempt to build with `dart-sass`. To change this behavior set the `SASS` environment variable to either `sassc` or `ruby`. + +```bash +export SASS=sassc +# or... +export SASS=ruby +``` + +### Building + +Clone the repository or download the branch from github. A simple Makefile is included. + +Next use `make` to install the extension into your home directory. A Shell reload is required `Alt+F2 r Enter` under Xorg or under Wayland you may have to logout and login. The extension has to be enabled with *gnome-extensions-app* (GNOME Extensions) or with *dconf*. + +```bash +git clone https://github.com/micheleg/dash-to-dock.git +make +make install +``` + +## Bug Reporting + +Bugs should be reported to the Github bug tracker [https://github.com/micheleg/dash-to-dock/issues](https://github.com/micheleg/dash-to-dock/issues). + +## License +Dash to Dock Gnome Shell extension is distributed under the terms of the GNU General Public License, +version 2 or later. See the COPYING file for details. diff --git a/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/Settings.ui b/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/Settings.ui new file mode 100644 index 0000000..d2560de --- /dev/null +++ b/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/Settings.ui @@ -0,0 +1,2660 @@ + + + + + 1 + 0.050000000000000003 + 0.25 + + + 0 + 12 + 12 + 12 + 12 + vertical + + + 0 + + + 0 + none + + + 100 + 80 + + + 0 + 12 + 12 + 12 + 12 + 32 + + + 0 + 1 + When set to minimize, double clicking minimizes all the windows of the application. + 1 + 40 + + + + 0 + 1 + + + + + + 0 + 1 + Shift+Click action + + + 0 + 0 + + + + + + 0 + center + + Raise window + Minimize window + Launch new instance + Cycle through windows + Minimize or overview + Show window previews + Minimize or show previews + Focus or show previews + Focus, minimize or show previews + Quit + + + 1 + 0 + 2 + + + + + + + + + + 100 + 80 + + + 0 + 12 + 12 + 12 + 12 + 32 + + + 0 + 1 + Behavior for Middle-Click. + 1 + 40 + + + + 0 + 1 + + + + + + 0 + 1 + Middle-Click action + + + 0 + 0 + + + + + + 0 + center + + Raise window + Minimize window + Launch new instance + Cycle through windows + Minimize or overview + Show window previews + Minimize or show previews + Focus or show previews + Focus, minimize or show previews + Quit + + + 1 + 0 + 2 + + + + + + + + + + 100 + 80 + + + 0 + 12 + 12 + 12 + 12 + 32 + + + 0 + 1 + Behavior for Shift+Middle-Click. + 1 + 40 + + + + 0 + 1 + + + + + + 0 + 1 + Shift+Middle-Click action + + + 0 + 0 + + + + + + 0 + center + + Raise window + Minimize window + Launch new instance + Cycle through windows + Minimize or overview + Show window previews + Minimize or show previews + Focus or show previews + Focus, minimize or show previews + Quit + + + 1 + 0 + 2 + + + + + + + + + + + + + + + + + 1 + 0.01 + 0.10 + + + 0.33 + 1 + 0.01 + 0.10 + + + 10 + 1 + 5 + + + 0 + 12 + 12 + 12 + 12 + vertical + + + 0 + + + + 0 + none + + + 100 + 80 + + + 0 + 12 + 12 + 12 + 12 + vertical + 12 + + + 0 + 32 + + + 1 + 0 + center + Enable Unity7 like glossy backlit items + + + + + + center + + + + + + + 0 + + + 1 + 0 + start + Use dominant color + + + + + + + + + + 0 + 32 + + + + 1 + 0 + + + + + + 0 + 1 + Customize indicator style + fill + + + 0 + 0 + + + + + + + + 0 + 1 + vertical + 12 + + + 0 + 32 + + + 1 + 0 + Color + + + + + + 1 + + + + + + + 0 + 32 + + + 1 + 0 + Border color + + + + + + 1 + + + + + + + 0 + 32 + + + 1 + 0 + Border width + + + + + + dot_border_width_adjustment + + + + + + + + + + + + + + + + + + + + 1 + 0.050000000000000003 + 0.25 + + + 16 + 128 + 1 + 10 + + + 6 + 6 + 6 + 6 + + + + + 0 + 24 + 24 + 24 + 24 + vertical + 24 + + + 0 + + + + 0 + none + + + + + 0 + 12 + 12 + 12 + 12 + 32 + + + 0 + 1 + start + Show the dock on + + 0 + 0 + + + + + + 0 + center + + + 1 + 0 + + + + + + Show on all monitors. + 12 + + + 0 + 2 + 2 + + + + + + + + + + + + + + + + 100 + + + 0 + 12 + 12 + 12 + 12 + 32 + + + 0 + 1 + Position on screen + + + + + + 0 + 32 + + + Left + end + center + + + + + + + Bottom + center + + position_left_button + + + + + + Top + center + + + position_left_button + + + + + + Right + center + + position_left_button + + + + + + + + + + + + + + + + + + + 0 + + + + 0 + none + + + 100 + 80 + + + 0 + 12 + 12 + 12 + 12 + 32 + + + 0 + 1 + start + Hide the dock when it obstructs a window of the current application. More refined settings are available. + 1 + + + + 0 + 1 + + + + + + 0 + 1 + start + Intelligent autohide + + + 0 + 0 + + + + + + 0 + 6 + + + 1 + center + center + + + + 0 + emblem-system-symbolic + + + + + + + + end + center + + + + 1 + 0 + 2 + + + + + + + + + + + + + + + + + 0 + + + + 0 + none + + + 100 + 80 + + + 0 + 12 + 12 + 12 + 12 + 32 + + + 0 + Dock size limit + + + 0 + 0 + + + + + + 1 + baseline + 1 + dock_size_adjustment + 0 + 2 + right + + + 1 + 0 + + + + + + Panel mode: extend to the screen edge + 12 + + + 0 + 1 + 2 + + + + + + + + + + + + 0 + 12 + 12 + 12 + 12 + 32 + + + 0 + Icon size limit + + + 0 + 0 + + + + + + 1 + baseline + 1 + icon_size_adjustment + 1 + 0 + right + + + 1 + 0 + + + + + + Fixed icon size: scroll to reveal other icons + 12 + + + 0 + 1 + 2 + + + + + + + + + + + + + + + + + + + 0 + Position and size + + + + + + + 1 + + + 0 + 24 + 24 + 24 + 24 + vertical + 24 + + + 0 + + + + 0 + none + + + + + 0 + 12 + 12 + 12 + 12 + 32 + + + end + center + + 1 + 0 + + + + + + 0 + 1 + start + Show favorite applications + + + 0 + 0 + + + + + + + + + + + + 0 + 12 + 12 + 12 + 12 + 32 + + + end + center + + 1 + 0 + + + + + + 0 + 1 + start + Show running applications + + + 0 + 0 + + + + + + Isolate workspaces. + start + 12 + + + 0 + 2 + 2 + + + + + + Isolate monitors. + start + 12 + + + 0 + 3 + 2 + + + + + + 3 + + + + 0 + Show open windows previews. + 1 + + + + 0 + 1 + 2 + + + + + + + + + + + + 0 + 12 + 12 + 12 + 12 + 32 + + + end + center + + 1 + 0 + 2 + + + + + + 0 + 1 + If disabled, these settings are accessible from gnome-tweak-tool or the extension website. + 1 + + + + 0 + 1 + + + + + + 0 + 1 + start + Show <i>Applications</i> icon + 1 + + + 0 + 0 + + + + + + Move the applications button at the beginning of the dock. + start + 12 + + + 0 + 2 + 2 + + + + + + 3 + + + + + 0 + start + Animate <i>Show Applications</i>. + 1 + + + + 0 + 3 + 2 + + + + + + + + + + + + 0 + 12 + 12 + 12 + 12 + 32 + + + end + center + + 1 + 0 + + + + + + 0 + 1 + start + Show trash can + + + 0 + 0 + + + + + + + + + + + + 0 + 12 + 12 + 12 + 12 + 32 + + + end + center + + 1 + 0 + + + + + + 0 + 1 + start + Show mounted volumes and devices + + + 0 + 0 + + + + + + + + + + + + + + + + + + + 0 + Launchers + + + + + + + 2 + + + 0 + 24 + 24 + 24 + 24 + vertical + 24 + + + 0 + + + + 0 + none + + + + + 0 + 12 + 12 + 12 + 12 + 32 + + + 0 + start + 1 + Enable Super+(0-9) as shortcuts to activate apps. It can also be used together with Shift and Ctrl. + 1 + 1 + + + + 0 + 1 + + + + + + 0 + 1 + start + Use keyboard shortcuts to activate apps + + + 0 + 0 + + + + + + 0 + 6 + + + 1 + center + center + + + + 0 + emblem-system-symbolic + + + + + + + + end + center + + + + 1 + 0 + 2 + + + + + + + + + + + + + + + + + 0 + + + + 0 + none + + + + + 0 + 12 + 12 + 12 + 12 + 32 + + + 0 + 1 + start + Behaviour when clicking on the icon of a running application. + 1 + + + + 0 + 1 + + + + + + 0 + 1 + start + Click action + + + 0 + 0 + + + + + + 0 + 6 + + + 1 + center + + + 0 + emblem-system-symbolic + + + + + + + + 0 + center + + Raise window + Minimize + Launch new instance + Cycle through windows + Minimize or overview + Show window previews + Minimize or show previews + Focus or show previews + Focus, minimize or show previews + + + + + 1 + 0 + 2 + + + + + + + + + + + + + + + + + 0 + + + + 0 + none + + + + + 0 + 12 + 12 + 12 + 12 + 32 + + + 0 + 1 + start + Behaviour when scrolling on the icon of an application. + 1 + + + + 0 + 1 + + + + + + 0 + 1 + start + Scroll action + + + 0 + 0 + + + + + + 0 + 6 + + + 0 + center + + Do nothing + Cycle through windows + Switch workspace + + + + + 1 + 0 + 2 + + + + + + + + + + + + + + + + + + + 0 + start + Behavior + + + + + + + 3 + + + 0 + 24 + 24 + 24 + 24 + vertical + 24 + + + 0 + + + + none + + + + + 0 + 12 + 12 + 12 + 12 + 32 + + + 0 + 1 + start + Few customizations meant to integrate the dock with the default GNOME theme. Alternatively, specific options can be enabled below. + 1 + + + + 0 + 1 + + + + + + 0 + 1 + start + Use built-in theme + + + 0 + 0 + + + + + + end + center + + 1 + 0 + 2 + + + + + + + + + + + + + + + + + 0 + + + + 0 + none + + + + + 0 + 12 + 12 + 12 + 12 + 32 + + + end + center + + 1 + 0 + 2 + + + + + + 0 + 1 + start + Save space reducing padding and border radius. + + + + 0 + 1 + + + + + + 0 + 1 + start + Shrink the dash + + + 0 + 0 + + + + + + + + + + + + 0 + 12 + 12 + 12 + 12 + 32 + + + 0 + 1 + start + Customize windows counter indicators + + + 0 + 0 + + + + + + 0 + 6 + + + 1 + center + center + + + + 0 + emblem-system-symbolic + + + + + + + + 0 + + Default + Dots + Squares + Dashes + Segmented + Solid + Ciliora + Metro + + + + + 1 + 0 + 2 + + + + + + + + + + + + + + + 0 + 12 + 12 + 12 + 12 + 32 + + + 0 + 1 + start + Set the background color for the dash. + 1 + + + + 0 + 1 + + + + + + 0 + 1 + start + Customize the dash color + + + 0 + 0 + + + + + + 0 + 6 + + + 1 + center + center + + + + + + end + center + + + + 1 + 0 + 2 + + + + + + + + + + + + 0 + vertical + + + 0 + 12 + 12 + 12 + 12 + 32 + + + 0 + 1 + start + Tune the dash background opacity. + + + + 0 + 1 + + + + + + 0 + 1 + start + Customize opacity + + + 0 + 0 + + + + + + 0 + 6 + + + 1 + center + center + + + 0 + emblem-system-symbolic + + + + + + + + 0 + center + + Default + Fixed + Dynamic + + + + + 1 + 0 + 2 + + + + + + + + 0 + 12 + 12 + 12 + 12 + 32 + + + 0 + Opacity + + + + + 1 + 1 + custom_opacity_adjustement + + 0 + 0 + 0 + 2 + right + + + + + + + + + + + + 100 + 80 + + + 0 + 32 + + + 1 + 0 + center + start + Force straight corner + + + + + + center + 3 + + + + + + + + + + + + + + + + + + 0 + Appearance + + + + + + + 4 + + + 0 + 0 + 24 + 24 + 1 + 1 + vertical + 5 + + + + + + 0 + <b>Dash to Dock</b> + 1 + + + + + 0 + center + + + 0 + end + version: + + + + + 0 + start + ... + + + + + + + 0 + Moves the dash out of the overview transforming it in a dock + center + 1 + + + + + 0 + center + 5 + + + 0 + Created by + + + + + Michele (<a href="mailto:micxgx@gmail.com">micxgx@gmail.com</a>) + 1 + + + + + + + Webpage + 1 + center + + https://micheleg.github.io/dash-to-dock/ + + + + + 1 + end + <span size="small">This program comes with ABSOLUTELY NO WARRANTY. +See the <a href="https://www.gnu.org/licenses/old-licenses/gpl-2.0.html">GNU General Public License, version 2 or later</a> for details.</span> + 1 + center + 1 + + + + + + + 0 + About + + + + + + + 1 + 0.01 + 0.10000000000000001 + + + 1 + 0.01 + 0.10000000000000001 + + + 0 + 12 + 12 + 12 + 12 + vertical + + + 0 + + + + 0 + none + + + 100 + 80 + + + 0 + 12 + 12 + 12 + 12 + vertical + 12 + + + 0 + 32 + + + + 1 + 0 + + + + + + 0 + 1 + Customize minimum and maximum opacity values + fill + + + 0 + 0 + + + + + + + + 0 + 12 + 12 + 12 + 12 + 32 + + + 0 + Minimum opacity + + + + + 1 + 1 + min_opacity_adjustement + + 0 + 0 + 0 + 2 + right + + + + + + + + 0 + 12 + 12 + 12 + 12 + 32 + + + 0 + Maximum opacity + + + + + 1 + 1 + max_opacity_adjustement + + 0 + 0 + 0 + 2 + right + + + + + + + + + + + + + + + + + + + 1000 + 50 + 250 + + + 10 + 0.25 + 1 + + + 0 + 12 + 12 + 12 + 12 + vertical + + + 0 + + + + 0 + none + + + 100 + 80 + + + 0 + 12 + 12 + 12 + 12 + 32 + + + end + center + + 1 + 0 + 2 + + + + + + 0 + 1 + Number overlay + + + 0 + 0 + + + + + + 0 + Temporarily show the application numbers over the icons, corresponding to the shortcut. + 1 + 40 + + + + 0 + 1 + + + + + + + + + + 100 + 80 + + + 0 + 12 + 12 + 12 + 12 + 32 + + + end + center + + 1 + 0 + 2 + + + + + + 0 + 1 + start + Show the dock if it is hidden + + + 0 + 0 + + + + + + 0 + If using autohide, the dock will appear for a short time when triggering the shortcut. + 1 + 40 + + + + 0 + 1 + + + + + + + + + + 100 + 80 + + + 0 + 12 + 12 + 12 + 12 + 32 + + + 0 + center + 12 + + 1 + 0 + + + + + + 0 + 1 + Shortcut for the options above + + + 0 + 0 + + + + + + 0 + Syntax: <Shift>, <Ctrl>, <Alt>, <Super> + 1 + 40 + + + + 0 + 1 + + + + + + + + + + + + + + + 0 + 12 + 12 + 12 + 12 + 1 + 6 + 32 + + + end + shortcut_time_adjustment + 3 + + 1 + 0 + + + + + + 0 + 1 + Hide timeout (s) + + + 0 + 0 + + + + + + + + + + + + + + + + + 1 + 0.050000000000000003 + 0.25 + + + 0 + 12 + 12 + 12 + 12 + vertical + + + 0 + + + + 0 + none + + + + + 0 + 12 + 12 + 12 + 12 + 32 + + + 0 + 1 + start + Show the dock by mouse hover on the screen edge. + 1 + + + + 0 + 1 + + + + + + 0 + 1 + Autohide + + + 0 + 0 + + + + + + end + center + + 1 + 0 + 2 + + + + + + Push to show: require pressure to show the dock + + + 0 + 3 + 2 + + + + + + Enable in fullscreen mode + 12 + + + 0 + 2 + 2 + + + + + + + + + + + + 0 + 12 + 12 + 12 + 12 + 32 + + + 0 + 1 + start + Show the dock when it doesn't obstruct application windows. + 1 + + + + 0 + 1 + + + + + + 0 + 1 + Dodge windows + + + 0 + 0 + + + + + + end + center + + 1 + 0 + 2 + + + + + + 0 + vertical + + + All windows + 12 + + 1 + + + + + + Only focused application's windows + + 1 + all_windows_radio_button + + + + + + Only maximized windows + + 1 + all_windows_radio_button + + + + + 0 + 2 + 2 + + + + + + + + + + + + 0 + 12 + 12 + 12 + 12 + 1 + 6 + 32 + + + end + animation_time_adjustment + 3 + + 1 + 0 + + + + + + 0 + 1 + Animation duration (s) + + + 0 + 0 + + + + + + end + hide_timeout_adjustment + 3 + + 1 + 1 + + + + + + end + show_timeout_adjustment + 3 + + 1 + 2 + + + + + + 0.000 + pressure_threshold_adjustment + + 1 + 3 + + + + + + 0 + 1 + Hide timeout (s) + + + 0 + 1 + + + + + + 0 + 1 + Show timeout (s) + + + 0 + 2 + + + + + + 0 + 1 + Pressure threshold + + + 0 + 3 + + + + + + + + + + + + + + + + diff --git a/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/appIconIndicators.js b/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/appIconIndicators.js new file mode 100644 index 0000000..f015a80 --- /dev/null +++ b/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/appIconIndicators.js @@ -0,0 +1,1078 @@ +const Cairo = imports.cairo; +const Clutter = imports.gi.Clutter; +const GdkPixbuf = imports.gi.GdkPixbuf +const Gio = imports.gi.Gio; +const Graphene = imports.gi.Graphene; +const Gtk = imports.gi.Gtk; +const Main = imports.ui.main; +const Pango = imports.gi.Pango; +const Shell = imports.gi.Shell; +const St = imports.gi.St; + +const Util = imports.misc.util; + +const Me = imports.misc.extensionUtils.getCurrentExtension(); +const Docking = Me.imports.docking; +const Utils = Me.imports.utils; + +let tracker = Shell.WindowTracker.get_default(); + +const RunningIndicatorStyle = { + DEFAULT: 0, + DOTS: 1, + SQUARES: 2, + DASHES: 3, + SEGMENTED: 4, + SOLID: 5, + CILIORA: 6, + METRO: 7 +}; + +const MAX_WINDOWS_CLASSES = 4; + + +/* + * This is the main indicator class to be used. The desired bahviour is + * obtained by composing the desired classes below based on the settings. + * + */ +var AppIconIndicator = class DashToDock_AppIconIndicator { + + constructor(source) { + this._indicators = []; + + // Unity indicators always enabled for now + let unityIndicator = new UnityIndicator(source); + this._indicators.push(unityIndicator); + + // Choose the style for the running indicators + let runningIndicator = null; + let runningIndicatorStyle; + + let settings = Docking.DockManager.settings; + if (settings.get_boolean('apply-custom-theme' )) { + runningIndicatorStyle = RunningIndicatorStyle.DOTS; + } else { + runningIndicatorStyle = settings.get_enum('running-indicator-style'); + } + + switch (runningIndicatorStyle) { + case RunningIndicatorStyle.DEFAULT: + runningIndicator = new RunningIndicatorDefault(source); + break; + + case RunningIndicatorStyle.DOTS: + runningIndicator = new RunningIndicatorDots(source); + break; + + case RunningIndicatorStyle.SQUARES: + runningIndicator = new RunningIndicatorSquares(source); + break; + + case RunningIndicatorStyle.DASHES: + runningIndicator = new RunningIndicatorDashes(source); + break; + + case RunningIndicatorStyle.SEGMENTED: + runningIndicator = new RunningIndicatorSegmented(source); + break; + + case RunningIndicatorStyle.SOLID: + runningIndicator = new RunningIndicatorSolid(source); + break; + + case RunningIndicatorStyle.CILIORA: + runningIndicator = new RunningIndicatorCiliora(source); + break; + + case RunningIndicatorStyle.METRO: + runningIndicator = new RunningIndicatorMetro(source); + break; + + default: + runningIndicator = new RunningIndicatorBase(source); + } + + this._indicators.push(runningIndicator); + } + + update() { + for (let i=0; i { + this._signalsHandler.destroy(); + }); + } + + update() { + } + + destroy() { + this._source.disconnect(this._sourceDestroyId); + this._signalsHandler.destroy(); + } +}; + +/* + * A base indicator class for running style, from which all other EunningIndicators should derive, + * providing some basic methods, variables definitions and their update, css style classes handling. + * + */ +var RunningIndicatorBase = class DashToDock_RunningIndicatorBase extends IndicatorBase { + + constructor(source) { + super(source) + + this._side = Utils.getPosition(); + this._nWindows = 0; + + this._dominantColorExtractor = new DominantColorExtractor(this._source.app); + + // These statuses take into account the workspace/monitor isolation + this._isFocused = false; + this._isRunning = false; + } + + update() { + // Limit to 1 to MAX_WINDOWS_CLASSES windows classes + this._nWindows = Math.min(this._source.getInterestingWindows().length, MAX_WINDOWS_CLASSES); + + // We need to check the number of windows, as the focus might be + // happening on another monitor if using isolation + if (tracker.focus_app == this._source.app && this._nWindows > 0) + this._isFocused = true; + else + this._isFocused = false; + + // In the case of workspace isolation, we need to hide the dots of apps with + // no windows in the current workspace + if ((this._source.app.state != Shell.AppState.STOPPED || this._source.isLocation()) && this._nWindows > 0) + this._isRunning = true; + else + this._isRunning = false; + + this._updateCounterClass(); + this._updateFocusClass(); + this._updateDefaultDot(); + } + + _updateCounterClass() { + for (let i = 1; i <= MAX_WINDOWS_CLASSES; i++) { + let className = 'running' + i; + if (i != this._nWindows) + this._source.remove_style_class_name(className); + else + this._source.add_style_class_name(className); + } + } + + _updateFocusClass() { + if (this._isFocused) + this._source.add_style_class_name('focused'); + else + this._source.remove_style_class_name('focused'); + } + + _updateDefaultDot() { + if (this._isRunning) + this._source._dot.show(); + else + this._source._dot.hide(); + } + + _hideDefaultDot() { + // I use opacity to hide the default dot because the show/hide function + // are used by the parent class. + this._source._dot.opacity = 0; + } + + _restoreDefaultDot() { + this._source._dot.opacity = 255; + } + + _enableBacklight() { + + let colorPalette = this._dominantColorExtractor._getColorPalette(); + + // Fallback + if (colorPalette === null) { + this._source._iconContainer.set_style( + 'border-radius: 5px;' + + 'background-gradient-direction: vertical;' + + 'background-gradient-start: #e0e0e0;' + + 'background-gradient-end: darkgray;' + ); + + return; + } + + this._source._iconContainer.set_style( + 'border-radius: 5px;' + + 'background-gradient-direction: vertical;' + + 'background-gradient-start: ' + colorPalette.original + ';' + + 'background-gradient-end: ' + colorPalette.darker + ';' + ); + + } + + _disableBacklight() { + this._source._iconContainer.set_style(null); + } + + destroy() { + this._disableBacklight(); + // Remove glossy background if the children still exists + if (this._source._iconContainer.get_children().length > 1) + this._source._iconContainer.get_children()[1].set_style(null); + this._restoreDefaultDot(); + + super.destroy(); + } +}; + +// We add a css class so third parties themes can limit their indicaor customization +// to the case we do nothing +var RunningIndicatorDefault = class DashToDock_RunningIndicatorDefault extends RunningIndicatorBase { + + constructor(source) { + super(source); + this._source.add_style_class_name('default'); + } + + destroy() { + this._source.remove_style_class_name('default'); + super.destroy(); + } +}; + +var RunningIndicatorDots = class DashToDock_RunningIndicatorDots extends RunningIndicatorBase { + + constructor(source) { + super(source) + + this._hideDefaultDot(); + + this._area = new St.DrawingArea({x_expand: true, y_expand: true}); + + // We draw for the bottom case and rotate the canvas for other placements + //set center of rotatoins to the center + this._area.set_pivot_point(0.5, 0.5); + // prepare transformation matrix + let m = new Graphene.Matrix(); + m.init_identity(); + let v = new Graphene.Vec3(); + v.init(0, 0, 1); + + switch (this._side) { + case St.Side.TOP: + m.xx = -1; + m.rotate(180, v); + break + + case St.Side.BOTTOM: + // nothing + break; + + case St.Side.LEFT: + m.yy = -1; + m.rotate(90, v); + break; + + case St.Side.RIGHT: + m.rotate(-90, v); + break + } + + this._area.set_transform(m); + + this._area.connect('repaint', this._updateIndicator.bind(this)); + this._source._iconContainer.add_child(this._area); + + let keys = ['custom-theme-running-dots-color', + 'custom-theme-running-dots-border-color', + 'custom-theme-running-dots-border-width', + 'custom-theme-customize-running-dots', + 'unity-backlit-items', + 'running-indicator-dominant-color']; + + keys.forEach(function(key) { + this._signalsHandler.add([ + Docking.DockManager.settings, + 'changed::' + key, + this.update.bind(this) + ]); + }, this); + + // Apply glossy background + // TODO: move to enable/disableBacklit to apply itonly to the running apps? + // TODO: move to css class for theming support + this._glossyBackgroundStyle = 'background-image: url(\'' + Me.path + '/media/glossy.svg\');' + + 'background-size: contain;'; + } + + update() { + super.update(); + + // Enable / Disable the backlight of running apps + if (!Docking.DockManager.settings.get_boolean('apply-custom-theme') && + Docking.DockManager.settings.get_boolean('unity-backlit-items')) { + this._source._iconContainer.get_children()[1].set_style(this._glossyBackgroundStyle); + if (this._isRunning) + this._enableBacklight(); + else + this._disableBacklight(); + } else { + this._disableBacklight(); + this._source._iconContainer.get_children()[1].set_style(null); + } + + if (this._area) + this._area.queue_repaint(); + } + + _computeStyle() { + + let [width, height] = this._area.get_surface_size(); + this._width = height; + this._height = width; + + // By defaut re-use the style - background color, and border width and color - + // of the default dot + let themeNode = this._source._dot.get_theme_node(); + this._borderColor = themeNode.get_border_color(this._side); + this._borderWidth = themeNode.get_border_width(this._side); + this._bodyColor = themeNode.get_background_color(); + + let settings = Docking.DockManager.settings; + if (!settings.get_boolean('apply-custom-theme')) { + // Adjust for the backlit case + if (settings.get_boolean('unity-backlit-items')) { + // Use dominant color for dots too if the backlit is enables + let colorPalette = this._dominantColorExtractor._getColorPalette(); + + // Slightly adjust the styling + this._borderWidth = 2; + + if (colorPalette !== null) { + this._borderColor = Clutter.color_from_string(colorPalette.lighter)[1] ; + this._bodyColor = Clutter.color_from_string(colorPalette.darker)[1]; + } else { + // Fallback + this._borderColor = Clutter.color_from_string('white')[1]; + this._bodyColor = Clutter.color_from_string('gray')[1]; + } + } + + // Apply dominant color if requested + if (settings.get_boolean('running-indicator-dominant-color')) { + let colorPalette = this._dominantColorExtractor._getColorPalette(); + if (colorPalette !== null) { + this._bodyColor = Clutter.color_from_string(colorPalette.original)[1]; + } + } + + // Finally, use customize style if requested + if (settings.get_boolean('custom-theme-customize-running-dots')) { + this._borderColor = Clutter.color_from_string(settings.get_string('custom-theme-running-dots-border-color'))[1]; + this._borderWidth = settings.get_int('custom-theme-running-dots-border-width'); + this._bodyColor = Clutter.color_from_string(settings.get_string('custom-theme-running-dots-color'))[1]; + } + } + + // Define the radius as an arbitrary size, but keep large enough to account + // for the drawing of the border. + this._radius = Math.max(this._width/22, this._borderWidth/2); + this._padding = 0; // distance from the margin + this._spacing = this._radius + this._borderWidth; // separation between the dots + } + + _updateIndicator() { + + let area = this._area; + let cr = this._area.get_context(); + + this._computeStyle(); + this._drawIndicator(cr); + cr.$dispose(); + } + + _drawIndicator(cr) { + // Draw the required numbers of dots + let n = this._nWindows; + + cr.setLineWidth(this._borderWidth); + Clutter.cairo_set_source_color(cr, this._borderColor); + + // draw for the bottom case: + cr.translate((this._width - (2*n)*this._radius - (n-1)*this._spacing)/2, this._height - this._padding); + for (let i = 0; i < n; i++) { + cr.newSubPath(); + cr.arc((2*i+1)*this._radius + i*this._spacing, -this._radius - this._borderWidth/2, this._radius, 0, 2*Math.PI); + } + + cr.strokePreserve(); + Clutter.cairo_set_source_color(cr, this._bodyColor); + cr.fill(); + } + + destroy() { + this._area.destroy(); + super.destroy(); + } +}; + +// Adapted from dash-to-panel by Jason DeRose +// https://github.com/jderose9/dash-to-panel +var RunningIndicatorCiliora = class DashToDock_RunningIndicatorCiliora extends RunningIndicatorDots { + + _drawIndicator(cr) { + if (this._isRunning) { + + let size = Math.max(this._width/20, this._borderWidth); + let spacing = size; // separation between the dots + let lineLength = this._width - (size*(this._nWindows-1)) - (spacing*(this._nWindows-1)); + let padding = this._borderWidth; + // For the backlit case here we don't want the outer border visible + if (Docking.DockManager.settings.get_boolean('unity-backlit-items') && + !Docking.DockManager.settings.get_boolean('custom-theme-customize-running-dots')) + padding = 0; + let yOffset = this._height - padding - size; + + cr.setLineWidth(this._borderWidth); + Clutter.cairo_set_source_color(cr, this._borderColor); + + cr.translate(0, yOffset); + cr.newSubPath(); + cr.rectangle(0, 0, lineLength, size); + for (let i = 1; i < this._nWindows; i++) { + cr.newSubPath(); + cr.rectangle(lineLength + (i*spacing) + ((i-1)*size), 0, size, size); + } + + cr.strokePreserve(); + Clutter.cairo_set_source_color(cr, this._bodyColor); + cr.fill(); + } + } +}; + +// Adapted from dash-to-panel by Jason DeRose +// https://github.com/jderose9/dash-to-panel +var RunningIndicatorSegmented = class DashToDock_RunningIndicatorSegmented extends RunningIndicatorDots { + + _drawIndicator(cr) { + if (this._isRunning) { + let size = Math.max(this._width/20, this._borderWidth); + let spacing = Math.ceil(this._width/18); // separation between the dots + let dashLength = Math.ceil((this._width - ((this._nWindows-1)*spacing))/this._nWindows); + let lineLength = this._width - (size*(this._nWindows-1)) - (spacing*(this._nWindows-1)); + let padding = this._borderWidth; + // For the backlit case here we don't want the outer border visible + if (Docking.DockManager.settings.get_boolean('unity-backlit-items') && + !Docking.DockManager.settings.get_boolean('custom-theme-customize-running-dots')) + padding = 0; + let yOffset = this._height - padding - size; + + cr.setLineWidth(this._borderWidth); + Clutter.cairo_set_source_color(cr, this._borderColor); + + cr.translate(0, yOffset); + for (let i = 0; i < this._nWindows; i++) { + cr.newSubPath(); + cr.rectangle(i*dashLength + i*spacing, 0, dashLength, size); + } + + cr.strokePreserve(); + Clutter.cairo_set_source_color(cr, this._bodyColor); + cr.fill() + } + } +}; + +// Adapted from dash-to-panel by Jason DeRose +// https://github.com/jderose9/dash-to-panel +var RunningIndicatorSolid = class DashToDock_RunningIndicatorSolid extends RunningIndicatorDots { + + _drawIndicator(cr) { + if (this._isRunning) { + + let size = Math.max(this._width/20, this._borderWidth); + let padding = this._borderWidth; + // For the backlit case here we don't want the outer border visible + if (Docking.DockManager.settings.get_boolean('unity-backlit-items') && + !Docking.DockManager.settings.get_boolean('custom-theme-customize-running-dots')) + padding = 0; + let yOffset = this._height - padding - size; + + cr.setLineWidth(this._borderWidth); + Clutter.cairo_set_source_color(cr, this._borderColor); + + cr.translate(0, yOffset); + cr.newSubPath(); + cr.rectangle(0, 0, this._width, size); + + cr.strokePreserve(); + Clutter.cairo_set_source_color(cr, this._bodyColor); + cr.fill(); + + } + } +}; + +// Adapted from dash-to-panel by Jason DeRose +// https://github.com/jderose9/dash-to-panel +var RunningIndicatorSquares = class DashToDock_RunningIndicatorSquares extends RunningIndicatorDots { + + _drawIndicator(cr) { + if (this._isRunning) { + let size = Math.max(this._width/11, this._borderWidth); + let padding = this._borderWidth; + let spacing = Math.ceil(this._width/18); // separation between the dots + let yOffset = this._height - padding - size; + + cr.setLineWidth(this._borderWidth); + Clutter.cairo_set_source_color(cr, this._borderColor); + + cr.translate(Math.floor((this._width - this._nWindows*size - (this._nWindows-1)*spacing)/2), yOffset); + for (let i = 0; i < this._nWindows; i++) { + cr.newSubPath(); + cr.rectangle(i*size + i*spacing, 0, size, size); + } + cr.strokePreserve(); + Clutter.cairo_set_source_color(cr, this._bodyColor); + cr.fill(); + } + } +} + +// Adapted from dash-to-panel by Jason DeRose +// https://github.com/jderose9/dash-to-panel +var RunningIndicatorDashes = class DashToDock_RunningIndicatorDashes extends RunningIndicatorDots { + + _drawIndicator(cr) { + if (this._isRunning) { + let size = Math.max(this._width/20, this._borderWidth); + let padding = this._borderWidth; + let spacing = Math.ceil(this._width/18); // separation between the dots + let dashLength = Math.floor(this._width/4) - spacing; + let yOffset = this._height - padding - size; + + cr.setLineWidth(this._borderWidth); + Clutter.cairo_set_source_color(cr, this._borderColor); + + cr.translate(Math.floor((this._width - this._nWindows*dashLength - (this._nWindows-1)*spacing)/2), yOffset); + for (let i = 0; i < this._nWindows; i++) { + cr.newSubPath(); + cr.rectangle(i*dashLength + i*spacing, 0, dashLength, size); + } + + cr.strokePreserve(); + Clutter.cairo_set_source_color(cr, this._bodyColor); + cr.fill(); + } + } +} + +// Adapted from dash-to-panel by Jason DeRose +// https://github.com/jderose9/dash-to-panel +var RunningIndicatorMetro = class DashToDock_RunningIndicatorMetro extends RunningIndicatorDots { + + constructor(source) { + super(source); + this._source.add_style_class_name('metro'); + } + + destroy() { + this._source.remove_style_class_name('metro'); + super.destroy(); + } + + _drawIndicator(cr) { + if (this._isRunning) { + let size = Math.max(this._width/20, this._borderWidth); + let padding = 0; + // For the backlit case here we don't want the outer border visible + if (Docking.DockManager.settings.get_boolean('unity-backlit-items') && + !Docking.DockManager.settings.get_boolean('custom-theme-customize-running-dots')) + padding = 0; + let yOffset = this._height - padding - size; + + let n = this._nWindows; + if(n <= 1) { + cr.translate(0, yOffset); + Clutter.cairo_set_source_color(cr, this._bodyColor); + cr.newSubPath(); + cr.rectangle(0, 0, this._width, size); + cr.fill(); + } else { + let blackenedLength = (1/48)*this._width; // need to scale with the SVG for the stacked highlight + let darkenedLength = this._isFocused ? (2/48)*this._width : (10/48)*this._width; + let blackenedColor = this._bodyColor.shade(.3); + let darkenedColor = this._bodyColor.shade(.7); + + cr.translate(0, yOffset); + + Clutter.cairo_set_source_color(cr, this._bodyColor); + cr.newSubPath(); + cr.rectangle(0, 0, this._width - darkenedLength - blackenedLength, size); + cr.fill(); + Clutter.cairo_set_source_color(cr, blackenedColor); + cr.newSubPath(); + cr.rectangle(this._width - darkenedLength - blackenedLength, 0, 1, size); + cr.fill(); + Clutter.cairo_set_source_color(cr, darkenedColor); + cr.newSubPath(); + cr.rectangle(this._width - darkenedLength, 0, darkenedLength, size); + cr.fill(); + } + } + } +} + +/* + * Unity like notification and progress indicators + */ +var UnityIndicator = class DashToDock_UnityIndicator extends IndicatorBase { + + constructor(source) { + + super(source); + + this._notificationBadgeLabel = new St.Label(); + this._notificationBadgeBin = new St.Bin({ + child: this._notificationBadgeLabel, + x_align: Clutter.ActorAlign.END, + y_align: Clutter.ActorAlign.START, + x_expand: true, y_expand: true + }); + this._notificationBadgeLabel.add_style_class_name('notification-badge'); + this._notificationBadgeLabel.clutter_text.ellipsize = Pango.EllipsizeMode.MIDDLE; + this._notificationBadgeBin.hide(); + + this._source._iconContainer.add_child(this._notificationBadgeBin); + this.updateNotificationBadgeStyle(); + + const remoteEntry = this._source.remoteModel.lookupById(this._source.app.id); + this._signalsHandler.add([ + remoteEntry, + ['count-changed', 'count-visible-changed'], + (sender, { count, count_visible }) => + this.setNotificationCount(count_visible ? count : 0) + ], [ + remoteEntry, + ['progress-changed', 'progress-visible-changed'], + (sender, { progress, progress_visible }) => + this.setProgress(progress_visible ? progress : -1) + ], [ + remoteEntry, + 'urgent-changed', + (sender, { urgent }) => this.setUrgent(urgent) + ], [ + St.ThemeContext.get_for_stage(global.stage), + 'changed', + this.updateNotificationBadgeStyle.bind(this) + ], [ + this._source._iconContainer, + 'notify::size', + this.updateNotificationBadgeStyle.bind(this) + ]); + + this._isUrgent = false; + } + + updateNotificationBadgeStyle() { + let themeContext = St.ThemeContext.get_for_stage(global.stage); + let fontDesc = themeContext.get_font(); + let defaultFontSize = fontDesc.get_size() / 1024; + let fontSize = defaultFontSize * 0.9; + let iconSize = Main.overview.dash.iconSize; + let defaultIconSize = Docking.DockManager.settings.get_default_value( + 'dash-max-icon-size').unpack(); + + if (!fontDesc.get_size_is_absolute()) { + // fontSize was exprimed in points, so convert to pixel + fontSize /= 0.75; + } + + fontSize = Math.round((iconSize / defaultIconSize) * fontSize); + let leftMargin = Math.round((iconSize / defaultIconSize) * 3); + + this._notificationBadgeLabel.set_style( + 'font-size: ' + fontSize + 'px;' + + 'margin-left: ' + leftMargin + 'px' + ); + } + + _notificationBadgeCountToText(count) { + if (count <= 9999) { + return count.toString(); + } else if (count < 1e5) { + let thousands = count / 1e3; + return thousands.toFixed(1).toString() + "k"; + } else if (count < 1e6) { + let thousands = count / 1e3; + return thousands.toFixed(0).toString() + "k"; + } else if (count < 1e8) { + let millions = count / 1e6; + return millions.toFixed(1).toString() + "M"; + } else if (count < 1e9) { + let millions = count / 1e6; + return millions.toFixed(0).toString() + "M"; + } else { + let billions = count / 1e9; + return billions.toFixed(1).toString() + "B"; + } + } + + setNotificationCount(count) { + if (count > 0) { + let text = this._notificationBadgeCountToText(count); + this._notificationBadgeLabel.set_text(text); + this._notificationBadgeBin.show(); + } else { + this._notificationBadgeBin.hide(); + } + } + + _showProgressOverlay() { + if (this._progressOverlayArea) { + this._updateProgressOverlay(); + return; + } + + this._progressOverlayArea = new St.DrawingArea({x_expand: true, y_expand: true}); + this._progressOverlayArea.add_style_class_name('progress-bar'); + this._progressOverlayArea.connect('repaint', () => { + this._drawProgressOverlay(this._progressOverlayArea); + }); + + this._source._iconContainer.add_child(this._progressOverlayArea); + let node = this._progressOverlayArea.get_theme_node(); + + let [hasColor, color] = node.lookup_color('-progress-bar-background', false); + if (hasColor) + this._progressbar_background = color + else + this._progressbar_background = new Clutter.Color({red: 204, green: 204, blue: 204, alpha: 255}); + + [hasColor, color] = node.lookup_color('-progress-bar-border', false); + if (hasColor) + this._progressbar_border = color; + else + this._progressbar_border = new Clutter.Color({red: 230, green: 230, blue: 230, alpha: 255}); + + this._updateProgressOverlay(); + } + + _hideProgressOverlay() { + if (this._progressOverlayArea) + this._progressOverlayArea.destroy(); + this._progressOverlayArea = null; + this._progressbar_background = null; + this._progressbar_border = null; + } + + _updateProgressOverlay() { + if (this._progressOverlayArea) + this._progressOverlayArea.queue_repaint(); + } + + _drawProgressOverlay(area) { + let scaleFactor = St.ThemeContext.get_for_stage(global.stage).scale_factor; + let [surfaceWidth, surfaceHeight] = area.get_surface_size(); + let cr = area.get_context(); + + let iconSize = this._source.icon.iconSize * scaleFactor; + + let x = Math.floor((surfaceWidth - iconSize) / 2); + let y = Math.floor((surfaceHeight - iconSize) / 2); + + let lineWidth = Math.floor(1.0 * scaleFactor); + let padding = Math.floor(iconSize * 0.05); + let width = iconSize - 2.0*padding; + let height = Math.floor(Math.min(18.0*scaleFactor, 0.20*iconSize)); + x += padding; + y += iconSize - height - padding; + + cr.setLineWidth(lineWidth); + + // Draw the outer stroke + let stroke = new Cairo.LinearGradient(0, y, 0, y + height); + let fill = null; + stroke.addColorStopRGBA(0.5, 0.5, 0.5, 0.5, 0.1); + stroke.addColorStopRGBA(0.9, 0.8, 0.8, 0.8, 0.4); + Utils.drawRoundedLine(cr, x + lineWidth/2.0, y + lineWidth/2.0, width, height, true, true, stroke, fill); + + // Draw the background + x += lineWidth; + y += lineWidth; + width -= 2.0*lineWidth; + height -= 2.0*lineWidth; + + stroke = Cairo.SolidPattern.createRGBA(0.20, 0.20, 0.20, 0.9); + fill = new Cairo.LinearGradient(0, y, 0, y + height); + fill.addColorStopRGBA(0.4, 0.25, 0.25, 0.25, 1.0); + fill.addColorStopRGBA(0.9, 0.35, 0.35, 0.35, 1.0); + Utils.drawRoundedLine(cr, x + lineWidth/2.0, y + lineWidth/2.0, width, height, true, true, stroke, fill); + + // Draw the finished bar + x += lineWidth; + y += lineWidth; + width -= 2.0*lineWidth; + height -= 2.0*lineWidth; + + let finishedWidth = Math.ceil(this._progress * width); + + let bg = this._progressbar_background; + let bd = this._progressbar_border; + + stroke = Cairo.SolidPattern.createRGBA(bd.red/255, bd.green/255, bd.blue/255, bd.alpha/255); + fill = Cairo.SolidPattern.createRGBA(bg.red/255, bg.green/255, bg.blue/255, bg.alpha/255); + + if (Clutter.get_default_text_direction() == Clutter.TextDirection.RTL) + Utils.drawRoundedLine(cr, x + lineWidth/2.0 + width - finishedWidth, y + lineWidth/2.0, finishedWidth, height, true, true, stroke, fill); + else + Utils.drawRoundedLine(cr, x + lineWidth/2.0, y + lineWidth/2.0, finishedWidth, height, true, true, stroke, fill); + + cr.$dispose(); + } + + setProgress(progress) { + if (progress < 0) { + this._hideProgressOverlay(); + } else { + this._progress = Math.min(progress, 1.0); + this._showProgressOverlay(); + } + } + + setUrgent(urgent) { + const icon = this._source.icon._iconBin; + if (urgent) { + if (!this._isUrgent) { + icon.set_pivot_point(0.5, 0.5); + this._source.iconAnimator.addAnimation(icon, 'dance'); + this._isUrgent = true; + } + } else { + if (this._isUrgent) { + this._source.iconAnimator.removeAnimation(icon, 'dance'); + this._isUrgent = false; + } + icon.rotation_angle_z = 0; + } + } +} + + +// We need an icons theme object, this is the only way I managed to get +// pixel buffers that can be used for calculating the backlight color +let themeLoader = null; + +// Global icon cache. Used for Unity7 styling. +let iconCacheMap = new Map(); +// Max number of items to store +// We don't expect to ever reach this number, but let's put an hard limit to avoid +// even the remote possibility of the cached items to grow indefinitely. +const MAX_CACHED_ITEMS = 1000; +// When the size exceed it, the oldest 'n' ones are deleted +const BATCH_SIZE_TO_DELETE = 50; +// The icon size used to extract the dominant color +const DOMINANT_COLOR_ICON_SIZE = 64; + +// Compute dominant color frim the app icon. +// The color is cached for efficiency. +var DominantColorExtractor = class DashToDock_DominantColorExtractor { + + constructor(app) { + this._app = app; + } + + /** + * Try to get the pixel buffer for the current icon, if not fail gracefully + */ + _getIconPixBuf() { + let iconTexture = this._app.create_icon_texture(16); + + if (themeLoader === null) { + let ifaceSettings = new Gio.Settings({ schema: "org.gnome.desktop.interface" }); + + themeLoader = new Gtk.IconTheme(), + themeLoader.set_custom_theme(ifaceSettings.get_string('icon-theme')); // Make sure the correct theme is loaded + } + + // Unable to load the icon texture, use fallback + if (iconTexture instanceof St.Icon === false) { + return null; + } + + iconTexture = iconTexture.get_gicon(); + + // Unable to load the icon texture, use fallback + if (iconTexture === null) { + return null; + } + + if (iconTexture instanceof Gio.FileIcon) { + // Use GdkPixBuf to load the pixel buffer from the provided file path + return GdkPixbuf.Pixbuf.new_from_file(iconTexture.get_file().get_path()); + } + + // Get the pixel buffer from the icon theme + let icon_info = themeLoader.lookup_icon(iconTexture.get_names()[0], DOMINANT_COLOR_ICON_SIZE, 0); + if (icon_info !== null) + return icon_info.load_icon(); + else + return null; + } + + /** + * The backlight color choosing algorithm was mostly ported to javascript from the + * Unity7 C++ source of Canonicals: + * https://bazaar.launchpad.net/~unity-team/unity/trunk/view/head:/launcher/LauncherIcon.cpp + * so it more or less works the same way. + */ + _getColorPalette() { + if (iconCacheMap.get(this._app.get_id())) { + // We already know the answer + return iconCacheMap.get(this._app.get_id()); + } + + let pixBuf = this._getIconPixBuf(); + if (pixBuf == null) + return null; + + let pixels = pixBuf.get_pixels(), + offset = 0; + + let total = 0, + rTotal = 0, + gTotal = 0, + bTotal = 0; + + let resample_y = 1, + resample_x = 1; + + // Resampling of large icons + // We resample icons larger than twice the desired size, as the resampling + // to a size s + // DOMINANT_COLOR_ICON_SIZE < s < 2*DOMINANT_COLOR_ICON_SIZE, + // most of the case exactly DOMINANT_COLOR_ICON_SIZE as the icon size is tipycally + // a multiple of it. + let width = pixBuf.get_width(); + let height = pixBuf.get_height(); + + // Resample + if (height >= 2* DOMINANT_COLOR_ICON_SIZE) + resample_y = Math.floor(height/DOMINANT_COLOR_ICON_SIZE); + + if (width >= 2* DOMINANT_COLOR_ICON_SIZE) + resample_x = Math.floor(width/DOMINANT_COLOR_ICON_SIZE); + + if (resample_x !==1 || resample_y !== 1) + pixels = this._resamplePixels(pixels, resample_x, resample_y); + + // computing the limit outside the for (where it would be repeated at each iteration) + // for performance reasons + let limit = pixels.length; + for (let offset = 0; offset < limit; offset+=4) { + let r = pixels[offset], + g = pixels[offset + 1], + b = pixels[offset + 2], + a = pixels[offset + 3]; + + let saturation = (Math.max(r,g, b) - Math.min(r,g, b)); + let relevance = 0.1 * 255 * 255 + 0.9 * a * saturation; + + rTotal += r * relevance; + gTotal += g * relevance; + bTotal += b * relevance; + + total += relevance; + } + + total = total * 255; + + let r = rTotal / total, + g = gTotal / total, + b = bTotal / total; + + let hsv = Utils.ColorUtils.RGBtoHSV(r * 255, g * 255, b * 255); + + if (hsv.s > 0.15) + hsv.s = 0.65; + hsv.v = 0.90; + + let rgb = Utils.ColorUtils.HSVtoRGB(hsv.h, hsv.s, hsv.v); + + // Cache the result. + let backgroundColor = { + lighter: Utils.ColorUtils.ColorLuminance(rgb.r, rgb.g, rgb.b, 0.2), + original: Utils.ColorUtils.ColorLuminance(rgb.r, rgb.g, rgb.b, 0), + darker: Utils.ColorUtils.ColorLuminance(rgb.r, rgb.g, rgb.b, -0.5) + }; + + if (iconCacheMap.size >= MAX_CACHED_ITEMS) { + //delete oldest cached values (which are in order of insertions) + let ctr=0; + for (let key of iconCacheMap.keys()) { + if (++ctr > BATCH_SIZE_TO_DELETE) + break; + iconCacheMap.delete(key); + } + } + + iconCacheMap.set(this._app.get_id(), backgroundColor); + + return backgroundColor; + } + + /** + * Downsample large icons before scanning for the backlight color to + * improve performance. + * + * @param pixBuf + * @param pixels + * @param resampleX + * @param resampleY + * + * @return []; + */ + _resamplePixels (pixels, resampleX, resampleY) { + let resampledPixels = []; + // computing the limit outside the for (where it would be repeated at each iteration) + // for performance reasons + let limit = pixels.length / (resampleX * resampleY) / 4; + for (let i = 0; i < limit; i++) { + let pixel = i * resampleX * resampleY; + + resampledPixels.push(pixels[pixel * 4]); + resampledPixels.push(pixels[pixel * 4 + 1]); + resampledPixels.push(pixels[pixel * 4 + 2]); + resampledPixels.push(pixels[pixel * 4 + 3]); + } + + return resampledPixels; + } +}; diff --git a/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/appIcons.js b/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/appIcons.js new file mode 100644 index 0000000..e10fb46 --- /dev/null +++ b/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/appIcons.js @@ -0,0 +1,1273 @@ +// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- + +const Clutter = imports.gi.Clutter; +const GdkPixbuf = imports.gi.GdkPixbuf +const Gio = imports.gi.Gio; +const GLib = imports.gi.GLib; +const GObject = imports.gi.GObject; +const Signals = imports.signals; +const Meta = imports.gi.Meta; +const Shell = imports.gi.Shell; +const St = imports.gi.St; + +// Use __ () and N__() for the extension gettext domain, and reuse +// the shell domain with the default _() and N_() +const Gettext = imports.gettext.domain('dashtodock'); +const __ = Gettext.gettext; +const N__ = function(e) { return e }; + +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 Util = imports.misc.util; +const Workspace = imports.ui.workspace; + +const ExtensionUtils = imports.misc.extensionUtils; +const Me = ExtensionUtils.getCurrentExtension(); +const Docking = Me.imports.docking; +const Utils = Me.imports.utils; +const WindowPreview = Me.imports.windowPreview; +const AppIconIndicators = Me.imports.appIconIndicators; +const DbusmenuUtils = Me.imports.dbusmenuUtils; + +let tracker = Shell.WindowTracker.get_default(); + +const clickAction = { + SKIP: 0, + MINIMIZE: 1, + LAUNCH: 2, + CYCLE_WINDOWS: 3, + MINIMIZE_OR_OVERVIEW: 4, + PREVIEWS: 5, + MINIMIZE_OR_PREVIEWS: 6, + FOCUS_OR_PREVIEWS: 7, + FOCUS_MINIMIZE_OR_PREVIEWS: 8, + QUIT: 9 +}; + +const scrollAction = { + DO_NOTHING: 0, + CYCLE_WINDOWS: 1, + SWITCH_WORKSPACE: 2 +}; + +let recentlyClickedAppLoopId = 0; +let recentlyClickedApp = null; +let recentlyClickedAppWindows = null; +let recentlyClickedAppIndex = 0; +let recentlyClickedAppMonitor = -1; + +/** + * Extend AppIcon + * + * - Apply a css class based on the number of windows of each application (#N); + * - Customized indicators for running applications in place of 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 + * - Update menu if open on windows change + */ +var MyAppIcon = GObject.registerClass( +class MyAppIcon extends Dash.DashIcon { + // settings are required inside. + _init(remoteModel, app, monitorIndex, iconAnimator) { + super._init(app); + + // a prefix is required to avoid conflicting with the parent class variable + this.monitorIndex = monitorIndex; + this._signalsHandler = new Utils.GlobalSignalsHandler(); + this.remoteModel = remoteModel; + this.iconAnimator = iconAnimator; + this._indicator = null; + + let appInfo = app.get_app_info(); + this._location = appInfo ? appInfo.get_string('XdtdUri') : null; + + this._updateIndicatorStyle(); + + // 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._windowsChangedId = this.app.connect('windows-changed', + this.onWindowsChanged.bind(this)); + this._focusAppChangeId = tracker.connect('notify::focus-app', + this._onFocusAppChanged.bind(this)); + + // In Wayland sessions, this signal is needed to track the state of windows dragged + // from one monitor to another. As this is triggered quite often (whenever a new winow + // of any application opened or moved to a different desktop), + // we restrict this signal to the case when 'isolate-monitors' is true, + // and if there are at least 2 monitors. + if (Docking.DockManager.settings.get_boolean('isolate-monitors') && + Main.layoutManager.monitors.length > 1) { + this._signalsHandler.removeWithLabel('isolate-monitors'); + this._signalsHandler.addWithLabel('isolate-monitors', [ + global.display, + 'window-entered-monitor', + this._onWindowEntered.bind(this) + ]); + } + + this._progressOverlayArea = null; + this._progress = 0; + + let keys = ['apply-custom-theme', + 'running-indicator-style', + ]; + + keys.forEach(function(key) { + this._signalsHandler.add([ + Docking.DockManager.settings, + 'changed::' + key, + this._updateIndicatorStyle.bind(this) + ]); + }, this); + + if (this._location) { + this._signalsHandler.add([ + Docking.DockManager.getDefault().fm1Client, + 'windows-changed', + this.onWindowsChanged.bind(this) + ]); + } + + this._numberOverlay(); + + this._previewMenuManager = null; + this._previewMenu = null; + } + + _onDestroy() { + super._onDestroy(); + + // This is necessary due to an upstream bug + // https://bugzilla.gnome.org/show_bug.cgi?id=757556 + // It can be safely removed once it get solved upstrea. + if (this._menu) + this._menu.close(false); + + // Disconect global signals + + if (this._windowsChangedId > 0) + this.app.disconnect(this._windowsChangedId); + this._windowsChangedId = 0; + + if (this._focusAppChangeId > 0) { + tracker.disconnect(this._focusAppChangeId); + this._focusAppChangeId = 0; + } + + this._signalsHandler.destroy(); + } + + // TOOD Rename this function + _updateIndicatorStyle() { + + if (this._indicator !== null) { + this._indicator.destroy(); + this._indicator = null; + } + this._indicator = new AppIconIndicators.AppIconIndicator(this); + this._indicator.update(); + } + + _onWindowEntered(metaScreen, monitorIndex, metaWin) { + let app = Shell.WindowTracker.get_default().get_window_app(metaWin); + if (app && app.get_id() == this.app.get_id()) + this.onWindowsChanged(); + } + + vfunc_scroll_event(scrollEvent) { + let settings = Docking.DockManager.settings; + let isEnabled = settings.get_enum('scroll-action') === scrollAction.CYCLE_WINDOWS; + if (!isEnabled) + return Clutter.EVENT_PROPAGATE; + + // We only activate windows of running applications, i.e. we never open new windows + // We check if the app is running, and that the # of windows is > 0 in + // case we use workspace isolation, + let appIsRunning = this.app.state == Shell.AppState.RUNNING + && this.getInterestingWindows().length > 0; + + if (!appIsRunning) + return Clutter.EVENT_PROPAGATE; + + if (this._optionalScrollCycleWindowsDeadTimeId) + return Clutter.EVENT_PROPAGATE; + else + this._optionalScrollCycleWindowsDeadTimeId = GLib.timeout_add( + GLib.PRIORITY_DEFAULT, 250, () => { + this._optionalScrollCycleWindowsDeadTimeId = 0; + }); + + let direction = null; + + switch (scrollEvent.direction) { + case Clutter.ScrollDirection.UP: + direction = Meta.MotionDirection.UP; + break; + case Clutter.ScrollDirection.DOWN: + direction = Meta.MotionDirection.DOWN; + break; + case Clutter.ScrollDirection.SMOOTH: + let [, dy] = Clutter.get_current_event().get_scroll_delta(); + if (dy < 0) + direction = Meta.MotionDirection.UP; + else if (dy > 0) + direction = Meta.MotionDirection.DOWN; + break; + } + + let focusedApp = tracker.focus_app; + if (!Main.overview._shown) { + let reversed = direction === Meta.MotionDirection.UP; + if (this.app == focusedApp) + this._cycleThroughWindows(reversed); + else { + // Activate the first window + let windows = this.getInterestingWindows(); + if (windows.length > 0) { + let w = windows[0]; + Main.activateWindow(w); + } + } + } + else + this.app.activate(); + return Clutter.EVENT_STOP; + } + + onWindowsChanged() { + + if (this._menu && this._menu.isOpen) + this._menu.update(); + + this._indicator.update(); + this.updateIconGeometry(); + } + + /** + * Update taraget for minimization animation + */ + updateIconGeometry() { + // 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.get_stage() == null) + return; + + let rect = new Meta.Rectangle(); + + [rect.x, rect.y] = this.get_transformed_position(); + [rect.width, rect.height] = this.get_transformed_size(); + + let windows = this.getWindows(); + if (Docking.DockManager.settings.get_boolean('multi-monitor')) { + let monitorIndex = this.monitorIndex; + windows = windows.filter(function(w) { + return w.get_monitor() == monitorIndex; + }); + } + windows.forEach(function(w) { + w.set_icon_geometry(rect); + }); + } + + _updateRunningStyle() { + // The logic originally in this function has been moved to + // AppIconIndicatorBase._updateDefaultDot(). However it cannot be removed as + // it called by the parent constructor. + } + + popupMenu() { + this._removeMenuTimeout(); + this.fake_release(); + this._draggable.fakeRelease(); + + if (!this._menu) { + this._menu = new MyAppIconMenu(this, this.remoteModel); + this._menu.connect('activate-window', (menu, window) => { + this.activateWindow(window); + }); + this._menu.connect('open-state-changed', (menu, isPoppedUp) => { + if (!isPoppedUp) + this._onMenuPoppedDown(); + else { + // Setting the max-height is s useful if part of the menu is + // scrollable so the minimum height is smaller than the natural height. + let monitor_index = Main.layoutManager.findIndexForActor(this); + let workArea = Main.layoutManager.getWorkAreaForMonitor(monitor_index); + let position = Utils.getPosition(); + this._isHorizontal = ( position == St.Side.TOP || + position == St.Side.BOTTOM); + // If horizontal also remove the height of the dash + let fixedDock = Docking.DockManager.settings.get_boolean('dock-fixed'); + let additional_margin = this._isHorizontal && !fixedDock ? Main.overview.dash.height : 0; + let verticalMargins = this._menu.actor.margin_top + this._menu.actor.margin_bottom; + // Also set a max width to the menu, so long labels (long windows title) get truncated + this._menu.actor.style = ('max-height: ' + Math.round(workArea.height - additional_margin - verticalMargins) + 'px;' + + 'max-width: 400px'); + } + }); + let id = Main.overview.connect('hiding', () => { + 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.set_hover(true); + this._menu.popup(); + this._menuManager.ignoreRelease(); + this.emit('sync-tooltip'); + + return false; + } + + _onFocusAppChanged() { + this._indicator.update(); + } + + activate(button) { + let event = Clutter.get_current_event(); + let modifiers = event ? event.get_state() : 0; + let focusedApp = tracker.focus_app; + + // Only consider SHIFT and CONTROL as modifiers (exclude SUPER, CAPS-LOCK, etc.) + modifiers = modifiers & (Clutter.ModifierType.SHIFT_MASK | Clutter.ModifierType.CONTROL_MASK); + + // We don't change the CTRL-click behaviour: in such case we just chain + // up the parent method and return. + 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 + super.activate(button); + return; + } + + // We check what type of click we have and if the modifier SHIFT is + // being used. We then define what buttonAction should be for this + // event. + let buttonAction = 0; + let settings = Docking.DockManager.settings; + if (button && button == 2 ) { + if (modifiers & Clutter.ModifierType.SHIFT_MASK) + buttonAction = settings.get_enum('shift-middle-click-action'); + else + buttonAction = settings.get_enum('middle-click-action'); + } + else if (button && button == 1) { + if (modifiers & Clutter.ModifierType.SHIFT_MASK) + buttonAction = settings.get_enum('shift-click-action'); + else + buttonAction = settings.get_enum('click-action'); + } + + // We check if the app is running, and that the # of windows is > 0 in + // case we use workspace isolation. + let windows = this.getInterestingWindows(); + let appIsRunning = (this.app.state == Shell.AppState.RUNNING || this.isLocation()) + && windows.length > 0; + + // Some action modes (e.g. MINIMIZE_OR_OVERVIEW) require overview to remain open + // This variable keeps track of this + let shouldHideOverview = true; + + // We customize the action only when the application is already running + if (appIsRunning) { + switch (buttonAction) { + case clickAction.MINIMIZE: + // In overview just activate the app, unless the acion is explicitely + // requested with a keyboard modifier + if (!Main.overview._shown || modifiers){ + // If we have button=2 or a modifier, allow minimization even if + // the app is not focused + if (this.app == focusedApp || button == 2 || modifiers & Clutter.ModifierType.SHIFT_MASK) { + // minimize all windows on double click and always in the case of primary click without + // additional modifiers + let click_count = 0; + if (Clutter.EventType.CLUTTER_BUTTON_PRESS) + click_count = event.get_click_count(); + let all_windows = (button == 1 && ! modifiers) || click_count > 1; + this._minimizeWindow(all_windows); + } + else + this._activateAllWindows(); + } + else { + let w = windows[0]; + Main.activateWindow(w); + } + break; + + case clickAction.MINIMIZE_OR_OVERVIEW: + // When a single window is present, toggle minimization + // If only one windows is present toggle minimization, but only when trigggered with the + // simple click action (no modifiers, no middle click). + if (windows.length == 1 && !modifiers && button == 1) { + let w = windows[0]; + if (this.app == focusedApp) { + // Window is raised, minimize it + this._minimizeWindow(w); + } else { + // Window is minimized, raise it + Main.activateWindow(w); + } + // Launch overview when multiple windows are present + // TODO: only show current app windows when gnome shell API will allow it + } else { + shouldHideOverview = false; + Main.overview.toggle(); + } + break; + + case clickAction.CYCLE_WINDOWS: + if (!Main.overview._shown){ + if (this.app == focusedApp) + this._cycleThroughWindows(); + else { + // Activate the first window + let w = windows[0]; + Main.activateWindow(w); + } + } + else + this.app.activate(); + break; + + case clickAction.FOCUS_OR_PREVIEWS: + if (this.app == focusedApp && + (windows.length > 1 || modifiers || button != 1)) { + this._windowPreviews(); + } else { + // Activate the first window + let w = windows[0]; + Main.activateWindow(w); + } + break; + + case clickAction.FOCUS_MINIMIZE_OR_PREVIEWS: + if (this.app == focusedApp) { + if (windows.length > 1 || modifiers || button != 1) + this._windowPreviews(); + else if (!Main.overview.visible) + this._minimizeWindow(); + } else { + // Activate the first window + let w = windows[0]; + Main.activateWindow(w); + } + break; + + case clickAction.LAUNCH: + this.launchNewWindow(); + break; + + case clickAction.PREVIEWS: + if (!Main.overview._shown) { + // If only one windows is present just switch to it, but only when trigggered with the + // simple click action (no modifiers, no middle click). + if (windows.length == 1 && !modifiers && button == 1) { + let w = windows[0]; + Main.activateWindow(w); + } else + this._windowPreviews(); + } + else { + this.app.activate(); + } + break; + + case clickAction.MINIMIZE_OR_PREVIEWS: + // When a single window is present, toggle minimization + // If only one windows is present toggle minimization, but only when trigggered with the + // simple click action (no modifiers, no middle click). + if (!Main.overview._shown){ + if (windows.length == 1 && !modifiers && button == 1) { + let w = windows[0]; + if (this.app == focusedApp) { + // Window is raised, minimize it + this._minimizeWindow(w); + } else { + // Window is minimized, raise it + Main.activateWindow(w); + } + } else { + // Launch previews when multiple windows are present + this._windowPreviews(); + } + } else { + this.app.activate(); + } + break; + + case clickAction.QUIT: + this.closeAllWindows(); + break; + + case clickAction.SKIP: + let w = windows[0]; + Main.activateWindow(w); + break; + } + } + else { + this.launchNewWindow(); + } + + // Hide overview except when action mode requires it + if(shouldHideOverview) { + Main.overview.hide(); + } + } + + shouldShowTooltip() { + return this.hover && (!this._menu || !this._menu.isOpen) && + (!this._previewMenu || !this._previewMenu.isOpen); + } + + _windowPreviews() { + if (!this._previewMenu) { + this._previewMenuManager = new PopupMenu.PopupMenuManager(this); + + this._previewMenu = new WindowPreview.WindowPreviewMenu(this); + + this._previewMenuManager.addMenu(this._previewMenu); + + this._previewMenu.connect('open-state-changed', (menu, isPoppedUp) => { + if (!isPoppedUp) + this._onMenuPoppedDown(); + }); + let id = Main.overview.connect('hiding', () => { + this._previewMenu.close(); + }); + this._previewMenu.actor.connect('destroy', function() { + Main.overview.disconnect(id); + }); + + } + + if (this._previewMenu.isOpen) + this._previewMenu.close(); + else + this._previewMenu.popup(); + + return false; + } + + // Try to do the right thing when attempting to launch a new window of an app. In + // particular, if the application doens't allow to launch a new window, activate + // the existing window instead. + launchNewWindow(p) { + let appInfo = this.app.get_app_info(); + let actions = appInfo.list_actions(); + if (this.app.can_open_new_window()) { + this.animateLaunch(); + // This is used as a workaround for a bug resulting in no new windows being opened + // for certain running applications when calling open_new_window(). + // + // https://bugzilla.gnome.org/show_bug.cgi?id=756844 + // + // Similar to what done when generating the popupMenu entries, if the application provides + // a "New Window" action, use it instead of directly requesting a new window with + // open_new_window(), which fails for certain application, notably Nautilus. + if (actions.indexOf('new-window') == -1) { + this.app.open_new_window(-1); + } + else { + let i = actions.indexOf('new-window'); + if (i !== -1) + this.app.launch_action(actions[i], global.get_current_time(), -1); + } + } + else { + // Try to manually activate the first window. Otherwise, when the app is activated by + // switching to a different workspace, a launch spinning icon is shown and disappers only + // after a timeout. + let windows = this.getWindows(); + if (windows.length > 0) + Main.activateWindow(windows[0]) + else + this.app.activate(); + } + } + + _numberOverlay() { + // Add label for a Hot-Key visual aid + this._numberOverlayLabel = new St.Label(); + this._numberOverlayBin = new St.Bin({ + child: this._numberOverlayLabel, + x_align: Clutter.ActorAlign.START, + y_align: Clutter.ActorAlign.START, + x_expand: true, y_expand: true + }); + this._numberOverlayLabel.add_style_class_name('number-overlay'); + this._numberOverlayOrder = -1; + this._numberOverlayBin.hide(); + + this._iconContainer.add_child(this._numberOverlayBin); + + } + + updateNumberOverlay() { + // We apply an overall scale factor that might come from a HiDPI monitor. + // Clutter dimensions are in physical pixels, but CSS measures are in logical + // pixels, so make sure to consider the scale. + let scaleFactor = St.ThemeContext.get_for_stage(global.stage).scale_factor; + // Set the font size to something smaller than the whole icon so it is + // still visible. The border radius is large to make the shape circular + let [minWidth, natWidth] = this._iconContainer.get_preferred_width(-1); + let font_size = Math.round(Math.max(12, 0.3*natWidth) / scaleFactor); + let size = Math.round(font_size*1.2); + this._numberOverlayLabel.set_style( + 'font-size: ' + font_size + 'px;' + + 'border-radius: ' + this.icon.iconSize + 'px;' + + 'width: ' + size + 'px; height: ' + size +'px;' + ); + } + + setNumberOverlay(number) { + this._numberOverlayOrder = number; + this._numberOverlayLabel.set_text(number.toString()); + } + + toggleNumberOverlay(activate) { + if (activate && this._numberOverlayOrder > -1) { + this.updateNumberOverlay(); + this._numberOverlayBin.show(); + } + else + this._numberOverlayBin.hide(); + } + + _minimizeWindow(param) { + // Param true make all app windows minimize + let windows = this.getInterestingWindows(); + let current_workspace = global.workspace_manager.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. + _activateAllWindows() { + // First activate first window so workspace is switched if needed. + // We don't do this if isolation is on! + if (!Docking.DockManager.settings.get_boolean('isolate-workspaces') && + !Docking.DockManager.settings.get_boolean('isolate-monitors')) + this.app.activate(); + + // then activate all other app windows in the current workspace + let windows = this.getInterestingWindows(); + let activeWorkspace = global.workspace_manager.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++; + } + } + } + + //This closes all windows of the app. + closeAllWindows() { + let windows = this.getInterestingWindows(); + for (let i = 0; i < windows.length; i++) + windows[i].delete(global.get_current_time()); + } + + _cycleThroughWindows(reversed) { + // Store for a little amount of time last clicked app and its windows + // since the order changes upon window interaction + let MEMORY_TIME=3000; + + let app_windows = this.getInterestingWindows(); + + if (app_windows.length <1) + return + + if (recentlyClickedAppLoopId > 0) + GLib.source_remove(recentlyClickedAppLoopId); + recentlyClickedAppLoopId = GLib.timeout_add( + GLib.PRIORITY_DEFAULT, MEMORY_TIME, this._resetRecentlyClickedApp); + + // If there isn't already a list of windows for the current app, + // or the stored list is outdated, use the current windows list. + let monitorIsolation = Docking.DockManager.settings.get_boolean('isolate-monitors'); + if (!recentlyClickedApp || + recentlyClickedApp.get_id() != this.app.get_id() || + recentlyClickedAppWindows.length != app_windows.length || + (recentlyClickedAppMonitor != this.monitorIndex && monitorIsolation)) { + recentlyClickedApp = this.app; + recentlyClickedAppWindows = app_windows; + recentlyClickedAppMonitor = this.monitorIndex; + recentlyClickedAppIndex = 0; + } + + if (reversed) { + recentlyClickedAppIndex--; + if (recentlyClickedAppIndex < 0) recentlyClickedAppIndex = recentlyClickedAppWindows.length - 1; + } else { + recentlyClickedAppIndex++; + } + let index = recentlyClickedAppIndex % recentlyClickedAppWindows.length; + let window = recentlyClickedAppWindows[index]; + + Main.activateWindow(window); + } + + _resetRecentlyClickedApp() { + if (recentlyClickedAppLoopId > 0) + GLib.source_remove(recentlyClickedAppLoopId); + recentlyClickedAppLoopId=0; + recentlyClickedApp =null; + recentlyClickedAppWindows = null; + recentlyClickedAppIndex = 0; + recentlyClickedAppMonitor = -1; + + return false; + } + + getWindows() { + return getWindows(this.app, this._location); + } + + // Filter out unnecessary windows, for instance + // nautilus desktop window. + getInterestingWindows() { + return getInterestingWindows(this.app, this.monitorIndex, this._location); + } + + // Does the Icon represent a location rather than an App + isLocation() { + return this._location != null; + } +}); +/** + * Extend AppIconMenu + * + * - set popup arrow side based on dash orientation + * - Add close windows option based on quitfromdash extension + * (https://github.com/deuill/shell-extension-quitfromdash) + * - Add open windows thumbnails instead of list + * - update menu when application windows change + */ +const MyAppIconMenu = class DashToDock_MyAppIconMenu extends AppDisplay.AppIconMenu { + + constructor(source, remoteModel) { + let side = Utils.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 + super(source); + + // Change the initialized side where required. + this._arrowSide = side; + this._boxPointer._arrowSide = side; + this._boxPointer._userArrowSide = side; + + this._signalsHandler = new Utils.GlobalSignalsHandler(); + + if (remoteModel && DbusmenuUtils.haveDBusMenu()) { + const [onQuicklist, onDynamicSection] = Utils.splitHandler((sender, { quicklist }, dynamicSection) => { + dynamicSection.removeAll(); + if (quicklist) { + quicklist.get_children().forEach(remoteItem => + dynamicSection.addMenuItem(DbusmenuUtils.makePopupMenuItem(remoteItem, false))); + } + }); + + this._signalsHandler.add([ + remoteModel.lookupById(this._source.app.id), + 'quicklist-changed', + onQuicklist + ], [ + this, + 'dynamic-section-changed', + onDynamicSection + ]); + } + } + + destroy() { + this._signalsHandler.destroy(); + super.destroy(); + } + + _rebuildMenu() { + this.removeAll(); + + if (Docking.DockManager.settings.get_boolean('show-windows-preview')) { + // Display the app windows menu items and the separator between windows + // of the current desktop and other windows. + + this._allWindowsMenuItem = new PopupMenu.PopupSubMenuMenuItem(__('All Windows'), false); + this._allWindowsMenuItem.hide(); + this.addMenuItem(this._allWindowsMenuItem); + + if (!this._source.app.is_window_backed()) { + this._appendSeparator(); + + let appInfo = this._source.app.get_app_info(); + let actions = appInfo.list_actions(); + if (this._source.app.can_open_new_window() && + actions.indexOf('new-window') == -1) { + this._newWindowMenuItem = this._appendMenuItem(_('New Window')); + this._newWindowMenuItem.connect('activate', () => { + if (this._source.app.state == Shell.AppState.STOPPED) + this._source.animateLaunch(); + + this._source.app.open_new_window(-1); + this.emit('activate-window', null); + }); + this._appendSeparator(); + } + + + if (AppDisplay.discreteGpuAvailable && + this._source.app.state == Shell.AppState.STOPPED && + actions.indexOf('activate-discrete-gpu') == -1) { + this._onDiscreteGpuMenuItem = this._appendMenuItem(_('Launch using Dedicated Graphics Card')); + this._onDiscreteGpuMenuItem.connect('activate', () => { + if (this._source.app.state == Shell.AppState.STOPPED) + this._source.animateLaunch(); + + this._source.app.launch(0, -1, true); + this.emit('activate-window', null); + }); + } + + for (let i = 0; i < actions.length; i++) { + let action = actions[i]; + let item = this._appendMenuItem(appInfo.get_action_name(action)); + item.connect('activate', (emitter, event) => { + this._source.app.launch_action(action, event.get_time(), -1); + this.emit('activate-window', null); + }); + } + + let canFavorite = global.settings.is_writable('favorite-apps') && + !this._source.isLocation(); + + if (canFavorite) { + this._appendSeparator(); + + let isFavorite = AppFavorites.getAppFavorites().isFavorite(this._source.app.get_id()); + + if (isFavorite) { + let item = this._appendMenuItem(_('Remove from Favorites')); + item.connect('activate', () => { + let favs = AppFavorites.getAppFavorites(); + favs.removeFavorite(this._source.app.get_id()); + }); + } else { + let item = this._appendMenuItem(_('Add to Favorites')); + item.connect('activate', () => { + let favs = AppFavorites.getAppFavorites(); + favs.addFavorite(this._source.app.get_id()); + }); + } + } + + if (Shell.AppSystem.get_default().lookup_app('org.gnome.Software.desktop') && + !this._source.isLocation()) { + this._appendSeparator(); + let item = this._appendMenuItem(_('Show Details')); + item.connect('activate', () => { + let id = this._source.app.get_id(); + let args = GLib.Variant.new('(ss)', [id, '']); + Gio.DBus.get(Gio.BusType.SESSION, null, + function(o, res) { + let bus = Gio.DBus.get_finish(res); + bus.call('org.gnome.Software', + '/org/gnome/Software', + 'org.gtk.Actions', 'Activate', + GLib.Variant.new('(sava{sv})', + ['details', [args], null]), + null, 0, -1, null, null); + Main.overview.hide(); + }); + }); + } + } + + } else { + if (super._rebuildMenu) + super._rebuildMenu(); + else + super._redisplay(); + } + + // dynamic menu + const items = this._getMenuItems(); + let i = items.length; + if (Shell.AppSystem.get_default().lookup_app('org.gnome.Software.desktop')) { + i -= 2; + } + if (global.settings.is_writable('favorite-apps')) { + i -= 2; + } + if (i < 0) { + i = 0; + } + const dynamicSection = new PopupMenu.PopupMenuSection(); + this.addMenuItem(dynamicSection, i); + this.emit('dynamic-section-changed', dynamicSection); + + // quit menu + this._appendSeparator(); + this._quitfromDashMenuItem = this._appendMenuItem(_('Quit')); + this._quitfromDashMenuItem.connect('activate', () => { + this._source.closeAllWindows(); + }); + + this.update(); + } + + // update menu content when application windows change. This is desirable as actions + // acting on windows (closing) are performed while the menu is shown. + update() { + + let windows = this._source.getInterestingWindows(); + + // update, show or hide the quit menu + if ( windows.length > 0) { + let quitFromDashMenuText = ""; + if (windows.length == 1) + this._quitfromDashMenuItem.label.set_text(_('Quit')); + else + this._quitfromDashMenuItem.label.set_text(__('Quit %d Windows').format(windows.length)); + + this._quitfromDashMenuItem.actor.show(); + + } else { + this._quitfromDashMenuItem.actor.hide(); + } + + if(Docking.DockManager.settings.get_boolean('show-windows-preview')){ + + // update, show, or hide the allWindows menu + // Check if there are new windows not already displayed. In such case, repopulate the allWindows + // menu. Windows removal is already handled by each preview being connected to the destroy signal + let old_windows = this._allWindowsMenuItem.menu._getMenuItems().map(function(item){ + return item._window; + }); + + let new_windows = windows.filter(function(w) {return old_windows.indexOf(w) < 0;}); + if (new_windows.length > 0) { + this._populateAllWindowMenu(windows); + + // Try to set the width to that of the submenu. + // TODO: can't get the actual size, getting a bit less. + // Temporary workaround: add 15px to compensate + this._allWindowsMenuItem.width = this._allWindowsMenuItem.menu.actor.width + 15; + + } + + // The menu is created hidden and never hidded after being shown. Instead, a singlal + // connected to its items destroy will set is insensitive if no more windows preview are shown. + if (windows.length > 0){ + this._allWindowsMenuItem.show(); + this._allWindowsMenuItem.setSensitive(true); + } + } + + // Update separators + this._getMenuItems().forEach(item => { + if ('label' in item) { + this._updateSeparatorVisibility(item); + } + }); + } + + _populateAllWindowMenu(windows) { + + this._allWindowsMenuItem.menu.removeAll(); + + if (windows.length > 0) { + + let activeWorkspace = global.workspace_manager.get_active_workspace(); + let separatorShown = windows[0].get_workspace() != activeWorkspace; + + for (let i = 0; i < windows.length; i++) { + let window = windows[i]; + if (!separatorShown && window.get_workspace() != activeWorkspace) { + this._allWindowsMenuItem.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem()); + separatorShown = true; + } + + let item = new WindowPreview.WindowPreviewMenuItem(window); + this._allWindowsMenuItem.menu.addMenuItem(item); + item.connect('activate', () => { + this.emit('activate-window', window); + }); + + // This is to achieve a more gracefull transition when the last windows is closed. + item.connect('destroy', () => { + if(this._allWindowsMenuItem.menu._getMenuItems().length == 1) // It's still counting the item just going to be destroyed + this._allWindowsMenuItem.setSensitive(false); + }); + } + } + } +}; +Signals.addSignalMethods(MyAppIconMenu.prototype); + +function getWindows(app, location) { + if (location != null && Docking.DockManager.getDefault().fm1Client) { + return Docking.DockManager.getDefault().fm1Client.getWindows(location); + } else { + return app.get_windows(); + } +} + +// Filter out unnecessary windows, for instance +// nautilus desktop window. +function getInterestingWindows(app, monitorIndex, location) { + let windows = getWindows(app, location).filter(function(w) { + return !w.skip_taskbar; + }); + + let settings = Docking.DockManager.settings; + + // When using workspace isolation, we filter out windows + // that are not in the current workspace + if (settings.get_boolean('isolate-workspaces')) + windows = windows.filter(function(w) { + return w.get_workspace().index() == global.workspace_manager.get_active_workspace_index(); + }); + + if (settings.get_boolean('isolate-monitors')) + windows = windows.filter(function(w) { + return w.get_monitor() == monitorIndex; + }); + + return windows; +} + +/** + * A ShowAppsIcon improved class. + * + * - set label position based on dash orientation (Note, I am reusing most machinery of the appIcon class) + * - implement a popupMenu based on the AppIcon code (Note, I am reusing most machinery of the appIcon class) + * + */ + +var MyShowAppsIcon = GObject.registerClass({ + Signals: { + 'menu-state-changed': { param_types: [GObject.TYPE_BOOLEAN] }, + 'sync-tooltip': {} + } +} +, class DashToDock_MyShowAppsIcon extends Dash.ShowAppsIcon { + _init() { + super._init(); + + // Re-use appIcon methods + let appIconPrototype = AppDisplay.AppIcon.prototype; + this.toggleButton.y_expand = false; + this.toggleButton.connect('popup-menu', + appIconPrototype._onKeyboardPopupMenu.bind(this)); + this.toggleButton.connect('clicked', + this._removeMenuTimeout.bind(this)); + + this.reactive = true; + this.toggleButton.popupMenu = () => this.popupMenu.call(this); + this.toggleButton._removeMenuTimeout = () => this._removeMenuTimeout.call(this); + + this._menu = null; + this._menuManager = new PopupMenu.PopupMenuManager(this); + this._menuTimeoutId = 0; + } + + vfunc_leave_event(leaveEvent) + { + return AppDisplay.AppIcon.prototype.vfunc_leave_event.call( + this.toggleButton, leaveEvent); + } + + vfunc_button_press_event(buttonPressEvent) + { + return AppDisplay.AppIcon.prototype.vfunc_button_press_event.call( + this.toggleButton, buttonPressEvent); + } + + vfunc_touch_event(touchEvent) + { + return AppDisplay.AppIcon.prototype.vfunc_touch_event.call( + this.toggleButton, touchEvent); + } + + showLabel() { + itemShowLabel.call(this); + } + + _onMenuPoppedDown() { + AppDisplay.AppIcon.prototype._onMenuPoppedDown.apply(this, arguments); + } + + _setPopupTimeout() { + AppDisplay.AppIcon.prototype._onMenuPoppedDown.apply(this, arguments); + } + + _removeMenuTimeout() { + AppDisplay.AppIcon.prototype._removeMenuTimeout.apply(this, arguments); + } + + popupMenu() { + this._removeMenuTimeout(); + this.toggleButton.fake_release(); + + if (!this._menu) { + this._menu = new MyShowAppsIconMenu(this); + this._menu.connect('open-state-changed', (menu, isPoppedUp) => { + if (!isPoppedUp) + this._onMenuPoppedDown(); + }); + let id = Main.overview.connect('hiding', () => { + 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.toggleButton.set_hover(true); + this._menu.popup(); + this._menuManager.ignoreRelease(); + this.emit('sync-tooltip'); + + return false; + } +}); + + +/** + * A menu for the showAppsIcon + */ +var MyShowAppsIconMenu = class DashToDock_MyShowAppsIconMenu extends MyAppIconMenu { + _rebuildMenu() { + this.removeAll(); + + /* Translators: %s is "Settings", which is automatically translated. You + can also translate the full message if this fits better your language. */ + let name = __('Dash to Dock %s').format(_('Settings')) + let item = this._appendMenuItem(name); + + item.connect('activate', function () { + if (typeof ExtensionUtils.openPrefs === 'function') { + ExtensionUtils.openPrefs(); + } else { + Util.spawn(["gnome-shell-extension-prefs", Me.metadata.uuid]); + } + }); + } +}; + +/** + * This function is used for both extendShowAppsIcon and extendDashItemContainer + */ +function itemShowLabel() { + // Check if the label is still present at all. When switching workpaces, the + // item might have been destroyed in between. + if (!this._labelText || this.label.get_stage() == null) + 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 = Utils.getPosition(); + this._isHorizontal = ((position == St.Side.TOP) || (position == St.Side.BOTTOM)); + let labelOffset = node.get_length('-x-offset'); + + switch (position) { + case St.Side.LEFT: + yOffset = Math.floor((itemHeight - labelHeight) / 2); + y = stageY + yOffset; + xOffset = labelOffset; + x = stageX + this.get_width() + xOffset; + break; + case St.Side.RIGHT: + yOffset = Math.floor((itemHeight - labelHeight) / 2); + y = stageY + yOffset; + xOffset = labelOffset; + x = Math.round(stageX) - labelWidth - xOffset; + break; + 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 fot the x coordinate. + + // Leave a few pixel gap + let gap = 5; + 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.remove_all_transitions(); + this.label.set_position(x, y); + this.label.ease({ + opacity: 255, + duration: Dash.DASH_ITEM_LABEL_SHOW_TIME, + mode: Clutter.AnimationMode.EASE_OUT_QUAD + }); +} diff --git a/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/dash.js b/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/dash.js new file mode 100644 index 0000000..bac49c2 --- /dev/null +++ b/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/dash.js @@ -0,0 +1,1072 @@ +// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- + +const Clutter = imports.gi.Clutter; +const Gio = imports.gi.Gio; +const GLib = imports.gi.GLib; +const GObject = imports.gi.GObject; +const Meta = imports.gi.Meta; +const Shell = imports.gi.Shell; +const St = imports.gi.St; + +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 Util = imports.misc.util; +const Workspace = imports.ui.workspace; + +const Me = imports.misc.extensionUtils.getCurrentExtension(); +const Docking = Me.imports.docking; +const Utils = Me.imports.utils; +const AppIcons = Me.imports.appIcons; +const Locations = Me.imports.locations; + +const DASH_ANIMATION_TIME = Dash.DASH_ANIMATION_TIME; +const DASH_ITEM_LABEL_HIDE_TIME = Dash.DASH_ITEM_LABEL_HIDE_TIME; +const DASH_ITEM_HOVER_TIMEOUT = Dash.DASH_ITEM_HOVER_TIMEOUT; + +/** + * Extend DashItemContainer + * + * - set label position based on dash orientation + * + */ +let MyDashItemContainer = GObject.registerClass( +class DashToDock_MyDashItemContainer extends Dash.DashItemContainer { + + showLabel() { + return AppIcons.itemShowLabel.call(this); + } +}); + +const MyDashIconsVerticalLayout = GObject.registerClass( + class DashToDock_MyDashIconsVerticalLayout extends Clutter.BoxLayout { + _init() { + super._init({ + orientation: Clutter.Orientation.VERTICAL, + }); + } + + vfunc_get_preferred_height(container, forWidth) { + const [natHeight] = super.vfunc_get_preferred_height(container, forWidth); + return [natHeight, 0]; + } +}); + + +const baseIconSizes = [16, 22, 24, 32, 48, 64, 96, 128]; + +/** + * 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 + * - hide showApps label when the custom menu is shown. + * - add scrollview + * ensure actor is visible on keyfocus inseid the scrollview + * - add 128px icon size, might be usefull for hidpi display + * - sync minimization application target position. + * - keep running apps ordered. + */ +var MyDash = GObject.registerClass({ + Signals: { + 'menu-closed': {}, + 'icon-size-changed': {}, + } +}, class DashToDock_MyDash extends St.Widget { + + _init(remoteModel, monitorIndex) { + // Initialize icon variables and size + this._maxWidth = -1; + this._maxHeight = -1; + this.iconSize = Docking.DockManager.settings.get_int('dash-max-icon-size'); + this._availableIconSizes = baseIconSizes; + this._shownInitially = false; + this._initializeIconSize(this.iconSize); + + this._separator = null; + + this._remoteModel = remoteModel; + this._monitorIndex = monitorIndex; + this._position = Utils.getPosition(); + this._isHorizontal = ((this._position == St.Side.TOP) || + (this._position == St.Side.BOTTOM)); + this._signalsHandler = new Utils.GlobalSignalsHandler(); + + this._dragPlaceholder = null; + this._dragPlaceholderPos = -1; + this._animatingPlaceholdersCount = 0; + this._showLabelTimeoutId = 0; + this._resetHoverTimeoutId = 0; + this._labelShowing = false; + + super._init({ + name: 'dash', + offscreen_redirect: Clutter.OffscreenRedirect.ALWAYS, + layout_manager: new Clutter.BinLayout() + }); + + this._dashContainer = new St.BoxLayout({ + x_align: Clutter.ActorAlign.CENTER, + y_align: this._isHorizontal ? Clutter.ActorAlign.CENTER: Clutter.ActorAlign.START, + vertical: !this._isHorizontal, + y_expand: this._isHorizontal, + x_expand: !this._isHorizontal, + pack_start: Docking.DockManager.settings.get_boolean('show-apps-at-top') + }); + + this._scrollView = new St.ScrollView({ + name: 'dashtodockDashScrollview', + // TODO: Fix scrolling + hscrollbar_policy: this._isHorizontal ? St.PolicyType.EXTERNAL : St.PolicyType.NEVER, + vscrollbar_policy: this._isHorizontal ? St.PolicyType.NEVER : St.PolicyType.EXTERNAL, + x_expand: this._isHorizontal, + y_expand: !this._isHorizontal, + enable_mouse_scrolling: false + }); + + if (Docking.DockManager.settings.get_boolean('extend-height')) { + if (!this._isHorizontal) { + this._scrollView.y_align = Clutter.ActorAlign.START; + } else { + this._scrollView.x_align = Clutter.ActorAlign.START; + } + } + + this._scrollView.connect('scroll-event', this._onScrollEvent.bind(this)); + + let rtl = Clutter.get_default_text_direction() == Clutter.TextDirection.RTL; + this._box = new St.BoxLayout({ + vertical: !this._isHorizontal, + clip_to_allocation: false, + ...(!this._isHorizontal ? { layout_manager: new MyDashIconsVerticalLayout() } : {}), + x_align: rtl ? Clutter.ActorAlign.END : Clutter.ActorAlign.START, + y_align: this._isHorizontal ? Clutter.ActorAlign.CENTER: Clutter.ActorAlign.START, + y_expand: !this._isHorizontal, + x_expand: this._isHorizontal + }); + this._box._delegate = this; + this._dashContainer.add_actor(this._scrollView); + this._scrollView.add_actor(this._box); + + this._showAppsIcon = new AppIcons.MyShowAppsIcon(); + this._showAppsIcon.show(false); + this._showAppsIcon.icon.setIconSize(this.iconSize); + this._showAppsIcon.x_expand = false; + this._showAppsIcon.y_expand = false; + if (!this._isHorizontal) + this._showAppsIcon.y_align = Clutter.ActorAlign.START; + this._hookUpLabel(this._showAppsIcon); + this._showAppsIcon.connect('menu-state-changed', (_icon, opened) => { + this._itemMenuStateChanged(this._showAppsIcon, opened); + }); + + this._dashContainer.add_child(this._showAppsIcon); + + this._background = new St.Widget({ + style_class: 'dash-background', + y_expand: this._isHorizontal, + x_expand: !this._isHorizontal, + }); + + const sizerBox = new Clutter.Actor(); + sizerBox.add_constraint(new Clutter.BindConstraint({ + source: this._isHorizontal ? this._showAppsIcon.icon : this._dashContainer, + coordinate: Clutter.BindCoordinate.HEIGHT, + })); + sizerBox.add_constraint(new Clutter.BindConstraint({ + source: this._isHorizontal ? this._dashContainer : this._showAppsIcon.icon, + coordinate: Clutter.BindCoordinate.WIDTH, + })); + this._background.add_child(sizerBox); + + this.add_child(this._background); + this.add_child(this._dashContainer); + + this._workId = Main.initializeDeferredWork(this._box, this._redisplay.bind(this)); + + this._shellSettings = new Gio.Settings({ + schema_id: 'org.gnome.shell' + }); + + this._appSystem = Shell.AppSystem.get_default(); + + this.iconAnimator = new Docking.IconAnimator(this); + + this._signalsHandler.add([ + this._appSystem, + 'installed-changed', + () => { + AppFavorites.getAppFavorites().reload(); + this._queueRedisplay(); + } + ], [ + AppFavorites.getAppFavorites(), + 'changed', + this._queueRedisplay.bind(this) + ], [ + this._appSystem, + 'app-state-changed', + this._queueRedisplay.bind(this) + ], [ + Main.overview, + 'item-drag-begin', + this._onItemDragBegin.bind(this) + ], [ + Main.overview, + 'item-drag-end', + this._onItemDragEnd.bind(this) + ], [ + Main.overview, + 'item-drag-cancelled', + this._onItemDragCancelled.bind(this) + ], [ + Main.overview, + 'window-drag-begin', + this._onWindowDragBegin.bind(this) + ], [ + Main.overview, + 'window-drag-cancelled', + this._onWindowDragEnd.bind(this) + ], [ + Main.overview, + 'window-drag-end', + this._onWindowDragEnd.bind(this) + ]); + + this.connect('destroy', this._onDestroy.bind(this)); + } + + vfunc_get_preferred_height(forWidth) { + let [minHeight, natHeight] = super.vfunc_get_preferred_height.call(this, forWidth); + if (!this._isHorizontal && this._maxHeight !== -1 && natHeight > this._maxHeight) + return [minHeight, this._maxHeight] + else + return [minHeight, natHeight] + } + + vfunc_get_preferred_width(forHeight) { + let [minWidth, natWidth] = super.vfunc_get_preferred_width.call(this, forHeight); + if (this._isHorizontal && this._maxWidth !== -1 && natWidth > this._maxWidth) + return [minWidth, this._maxWidth] + else + return [minWidth, natWidth] + } + + get _container() { + return this._dashContainer; + } + + _onDestroy() { + this.iconAnimator.destroy(); + this._signalsHandler.destroy(); + } + + + _onItemDragBegin() { + return Dash.Dash.prototype._onItemDragBegin.call(this, ...arguments); + } + + _onItemDragCancelled() { + return Dash.Dash.prototype._onItemDragCancelled.call(this, ...arguments); + } + + _onItemDragEnd() { + return Dash.Dash.prototype._onItemDragEnd.call(this, ...arguments); + } + + _endItemDrag() { + return Dash.Dash.prototype._endItemDrag.call(this, ...arguments); + } + + _onItemDragMotion() { + return Dash.Dash.prototype._onItemDragMotion.call(this, ...arguments); + } + + _appIdListToHash() { + return Dash.Dash.prototype._appIdListToHash.call(this, ...arguments); + } + + _queueRedisplay() { + return Dash.Dash.prototype._queueRedisplay.call(this, ...arguments); + } + + _hookUpLabel() { + return Dash.Dash.prototype._hookUpLabel.call(this, ...arguments); + } + + _syncLabel() { + return Dash.Dash.prototype._syncLabel.call(this, ...arguments); + } + + _clearDragPlaceholder() { + return Dash.Dash.prototype._clearDragPlaceholder.call(this, ...arguments); + } + + _clearEmptyDropTarget() { + return Dash.Dash.prototype._clearEmptyDropTarget.call(this, ...arguments); + } + + handleDragOver(source, actor, x, y, time) { + let ret; + if (this._isHorizontal) { + ret = Dash.Dash.prototype.handleDragOver.call(this, source, actor, x, y, time); + + if (ret == DND.DragMotionResult.CONTINUE) + return ret; + } else { + Object.defineProperty(this._box, 'height', { + configurable: true, + get: () => this._box.get_children().reduce((a, c) => a + c.height, 0), + }); + + let replacedPlaceholderWidth = false; + if (this._dragPlaceholder) { + replacedPlaceholderWidth = true; + Object.defineProperty(this._dragPlaceholder, 'width', { + configurable: true, + get: () => this._dragPlaceholder.height, + }); + } + + ret = Dash.Dash.prototype.handleDragOver.call(this, source, actor, y, x, time); + + delete this._box.height; + if (replacedPlaceholderWidth && this._dragPlaceholder) + delete this._dragPlaceholder.width; + + if (ret == DND.DragMotionResult.CONTINUE) + return ret; + + if (this._dragPlaceholder) { + this._dragPlaceholder.child.set_width(this.iconSize / 2); + this._dragPlaceholder.child.set_height(this.iconSize); + + let pos = this._dragPlaceholderPos; + if (this._isHorizontal && (Clutter.get_default_text_direction() == Clutter.TextDirection.RTL)) + pos = this._box.get_children() - 1 - pos; + + if (pos != this._dragPlaceholderPos) { + this._dragPlaceholderPos = pos; + this._box.set_child_at_index(this._dragPlaceholder, + this._dragPlaceholderPos) + } + } + } + + if (this._dragPlaceholder) { + // Ensure the next and previous icon are visible when moving the placeholder + // (I assume there's room for both of them) + if (this._dragPlaceholderPos > 0) + 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]); + } + + return ret; + } + + acceptDrop() { + return Dash.Dash.prototype.acceptDrop.call(this, ...arguments); + } + + _onWindowDragBegin() { + return Dash.Dash.prototype._onWindowDragBegin.call(this, ...arguments); + } + + _onWindowDragEnd() { + return Dash.Dash.prototype._onWindowDragEnd.call(this, ...arguments); + } + + _onScrollEvent(actor, event) { + // If scroll is not used because the icon is resized, let the scroll event propagate. + if (!Docking.DockManager.settings.get_boolean('icon-size-fixed')) + return Clutter.EVENT_PROPAGATE; + + // reset timeout to avid conflicts with the mousehover event + if (this._ensureAppIconVisibilityTimeoutId > 0) { + GLib.source_remove(this._ensureAppIconVisibilityTimeoutId); + this._ensureAppIconVisibilityTimeoutId = 0; + } + + // Skip to avoid double events mouse + // TODO: Horizontal events are emulated, potentially due to a conflict + // with the workspace switching gesture. + if (!this._isHorizontal && event.is_pointer_emulated()) { + return Clutter.EVENT_STOP; + } + + let adjustment, delta = 0; + + if (this._isHorizontal) + adjustment = this._scrollView.get_hscroll_bar().get_adjustment(); + else + adjustment = this._scrollView.get_vscroll_bar().get_adjustment(); + + let increment = adjustment.step_increment; + + if (this._isHorizontal) { + switch (event.get_scroll_direction()) { + case Clutter.ScrollDirection.LEFT: + delta = -increment; + break; + case Clutter.ScrollDirection.RIGHT: + delta = +increment; + break; + case Clutter.ScrollDirection.SMOOTH: + let [dx, dy] = event.get_scroll_delta(); + // TODO: Handle y + //delta = dy * increment; + // Also consider horizontal component, for instance touchpad + delta = dx * increment; + break; + } + } else { + switch (event.get_scroll_direction()) { + case Clutter.ScrollDirection.UP: + delta = -increment; + break; + case Clutter.ScrollDirection.DOWN: + delta = +increment; + break; + case Clutter.ScrollDirection.SMOOTH: + let [, dy] = event.get_scroll_delta(); + delta = dy * increment; + break; + } + } + + const value = adjustment.get_value(); + + // TODO: Remove this if possible. + if (Number.isNaN(value)) { + adjustment.set_value(delta); + } else { + adjustment.set_value(value + delta); + } + + return Clutter.EVENT_STOP; + } + + _createAppItem(app) { + let appIcon = new AppIcons.MyAppIcon(this._remoteModel, app, + this._monitorIndex, this.iconAnimator); + + if (appIcon._draggable) { + appIcon._draggable.connect('drag-begin', () => { + appIcon.opacity = 50; + }); + appIcon._draggable.connect('drag-end', () => { + appIcon.opacity = 255; + }); + } + + appIcon.connect('menu-state-changed', (appIcon, opened) => { + this._itemMenuStateChanged(item, opened); + }); + + let item = new MyDashItemContainer(); + item.setChild(appIcon); + + appIcon.connect('notify::hover', () => { + if (appIcon.hover) { + this._ensureAppIconVisibilityTimeoutId = GLib.timeout_add( + GLib.PRIORITY_DEFAULT, 100, () => { + ensureActorVisibleInScrollView(this._scrollView, appIcon); + this._ensureAppIconVisibilityTimeoutId = 0; + return GLib.SOURCE_REMOVE; + }); + } + else { + if (this._ensureAppIconVisibilityTimeoutId > 0) { + GLib.source_remove(this._ensureAppIconVisibilityTimeoutId); + this._ensureAppIconVisibilityTimeoutId = 0; + } + } + }); + + appIcon.connect('clicked', (actor) => { + ensureActorVisibleInScrollView(this._scrollView, actor); + }); + + appIcon.connect('key-focus-in', (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.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 dash + */ + getAppIcons() { + // 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.icon && + !actor.animatingOut; + }); + + let appIcons = iconChildren.map(function(actor) { + return actor.child; + }); + + return appIcons; + } + + _updateAppsIconGeometry() { + let appIcons = this.getAppIcons(); + appIcons.forEach(function(icon) { + icon.updateIconGeometry(); + }); + } + + _itemMenuStateChanged(item, opened) { + Dash.Dash.prototype._itemMenuStateChanged.call(this, item, opened); + + if (!opened) { + // 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'); + } + } + + _adjustIconSize() { + // 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(actor => { + return actor.child && + actor.child._delegate && + actor.child._delegate.icon && + !actor.animatingOut; + }); + + iconChildren.push(this._showAppsIcon); + + if (this._maxWidth === -1 && this._maxHeight === -1) + return; + + // Check if the container is present in the stage. This avoids critical + // errors when unlocking the screen + if (!this._container.get_stage()) + return; + + const themeNode = this.get_theme_node(); + const maxAllocation = new Clutter.ActorBox({ + x1: 0, + y1: 0, + x2: this._isHorizontal ? this._maxWidth : 42 /* whatever */, + y2: this._isHorizontal ? 42 : this._maxHeight + }); + let maxContent = themeNode.get_content_box(maxAllocation); + let availWidth; + if (this._isHorizontal) + availWidth = maxContent.x2 - maxContent.x1; + else + availWidth = maxContent.y2 - maxContent.y1; + let spacing = themeNode.get_length('spacing'); + + let firstButton = iconChildren[0].child; + let firstIcon = firstButton._delegate.icon; + + // Enforce valid spacings during the size request + firstIcon.icon.ensure_style(); + const [, , iconWidth, iconHeight] = firstIcon.icon.get_preferred_size(); + const [, , buttonWidth, buttonHeight] = firstButton.get_preferred_size(); + + // Subtract icon padding and box spacing from the available height + if (this._isHorizontal) + // Subtract icon padding and box spacing from the available width + availWidth -= iconChildren.length * (buttonWidth - iconWidth) + + (iconChildren.length - 1) * spacing; + else + availWidth -= iconChildren.length * (buttonHeight - iconHeight) + + (iconChildren.length - 1) * spacing; + + // let availHeight = this._maxHeight; + // availHeight -= this._background.get_theme_node().get_vertical_padding(); + // availHeight -= themeNode.get_vertical_padding(); + // availHeight -= buttonHeight - iconHeight; + + const maxIconSize = // TODO: Math.min( + availWidth / iconChildren.length // ); , availHeight); + let scaleFactor = St.ThemeContext.get_for_stage(global.stage).scale_factor; + let iconSizes = this._availableIconSizes.map(s => s * scaleFactor); + + let newIconSize = this._availableIconSizes[0]; + for (let i = 0; i < iconSizes.length; i++) { + if (iconSizes[i] <= maxIconSize) + 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, not visible or when initially filling + // the dash + if (!Main.overview.visible || 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); + + icon.icon.ease({ + width: targetWidth, + height: targetHeight, + duration: DASH_ANIMATION_TIME, + mode: Clutter.AnimationMode.EASE_OUT_QUAD, + }); + } + + if (this._separator) { + if (this._isHorizontal) { + this._separator.ease({ + height: this.iconSize, + duration: DASH_ANIMATION_TIME, + mode: Clutter.AnimationMode.EASE_OUT_QUAD, + }); + } else { + this._separator.ease({ + width: this.iconSize, + duration: DASH_ANIMATION_TIME, + mode: Clutter.AnimationMode.EASE_OUT_QUAD, + }); + } + } + } + + _redisplay() { + let favorites = AppFavorites.getAppFavorites().getFavoriteMap(); + + let running = this._appSystem.get_running(); + let settings = Docking.DockManager.settings; + + if (settings.get_boolean('isolate-workspaces') || + settings.get_boolean('isolate-monitors')) { + // When using isolation, we filter out apps that have no windows in + // the current workspace + let monitorIndex = this._monitorIndex; + running = running.filter(function(_app) { + return AppIcons.getInterestingWindows(_app, monitorIndex).length != 0; + }); + } + + let children = this._box.get_children().filter(actor => { + return actor.child && + actor.child._delegate && + actor.child._delegate.app; + }); + // Apps currently in the dash + let oldApps = children.map(actor => actor.child._delegate.app); + // Apps supposed to be in the dash + let newApps = []; + + if (settings.get_boolean('show-favorites')) { + for (let id in favorites) + newApps.push(favorites[id]); + } + + if (settings.get_boolean('show-running')) { + for (let i = 0; i < running.length; i++) { + let app = running[i]; + if (settings.get_boolean('show-favorites') && app.get_id() in favorites) + continue; + newApps.push(app); + } + } + + if (settings.get_boolean('show-mounts')) { + if (!this._removables) { + this._removables = new Locations.Removables(); + this._signalsHandler.addWithLabel('show-mounts', + [ this._removables, + 'changed', + this._queueRedisplay.bind(this) ]); + } + Array.prototype.push.apply(newApps, this._removables.getApps()); + } else if (this._removables) { + this._signalsHandler.removeWithLabel('show-mounts'); + this._removables.destroy(); + this._removables = null; + } + + if (settings.get_boolean('show-trash')) { + if (!this._trash) { + this._trash = new Locations.Trash(); + this._signalsHandler.addWithLabel('show-trash', + [ this._trash, + 'changed', + this._queueRedisplay.bind(this) ]); + } + newApps.push(this._trash.getApp()); + } else if (this._trash) { + this._signalsHandler.removeWithLabel('show-trash'); + this._trash.destroy(); + this._trash = null; + } + + // Figure out the actual changes to the list of items; we iterate + // over both the list of items currently in the dash 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) { + let oldApp = oldApps.length > oldIndex ? oldApps[oldIndex] : null; + let newApp = newApps.length > newIndex ? newApps[newIndex] : null; + + // No change at oldIndex/newIndex + if (oldApp == newApp) { + oldIndex++; + newIndex++; + continue; + } + + // App removed at oldIndex + if (oldApp && !newApps.includes(oldApp)) { + removedActors.push(children[oldIndex]); + oldIndex++; + continue; + } + + // App added at newIndex + if (newApp && !oldApps.includes(newApp)) { + addedItems.push({ app: newApp, + item: this._createAppItem(newApp), + pos: newIndex }); + newIndex++; + continue; + } + + // App moved + let nextApp = newApps.length > newIndex + 1 + ? newApps[newIndex + 1] : null; + let insertHere = nextApp && nextApp == oldApp; + let alreadyRemoved = removedActors.reduce((result, actor) => { + let removedApp = actor.child._delegate.app; + return result || removedApp == newApp; + }, false); + + if (insertHere || alreadyRemoved) { + let newItem = this._createAppItem(newApp); + addedItems.push({ app: newApp, + 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]; + + // Don't animate item removal when the overview is transitioning + // or hidden + if (!Main.overview.animationInProgress) + item.animateOutAndDestroy(); + else + item.destroy(); + } + + this._adjustIconSize(); + + // Skip animations on first run when adding the initial set + // of items, to avoid all items zooming in at once + + let animate = this._shownInitially && + !Main.overview.animationInProgress; + + if (!this._shownInitially) + this._shownInitially = true; + + for (let i = 0; i < addedItems.length; i++) + addedItems[i].item.show(animate); + + // Update separator + const nFavorites = Object.keys(favorites).length; + const nIcons = children.length + addedItems.length - removedActors.length; + if (nFavorites > 0 && nFavorites < nIcons) { + if (!this._separator) { + if (!this._isHorizontal) { + this._separator = new St.Widget({ + style_class: 'vertical-dash-separator', + x_align: Clutter.ActorAlign.CENTER, + width: this.iconSize, + }); + } else { + this._separator = new St.Widget({ + style_class: 'dash-separator', + y_align: Clutter.ActorAlign.CENTER, + height: this.iconSize, + }); + } + + this._box.add_child(this._separator); + } + let pos = nFavorites; + if (this._dragPlaceholder) + pos++; + this._box.set_child_at_index(this._separator, pos); + } else if (this._separator) { + this._separator.destroy(); + this._separator = null; + } + + // Workaround for https://bugzilla.gnome.org/show_bug.cgi?id=692744 + // Without it, StBoxLayout may use a stale size cache + this._box.queue_relayout(); + // TODO + // This is required for icon reordering when the scrollview is used. + this._updateAppsIconGeometry(); + + // This will update the size, and the corresponding number for each icon + this._updateNumberOverlay(); + } + + _updateNumberOverlay() { + let appIcons = this.getAppIcons(); + let counter = 1; + appIcons.forEach(function(icon) { + if (counter < 10){ + icon.setNumberOverlay(counter); + counter++; + } else if (counter == 10) { + icon.setNumberOverlay(0); + counter++; + } else { + // No overlay after 10 + icon.setNumberOverlay(-1); + } + icon.updateNumberOverlay(); + }); + + } + + toggleNumberOverlay(activate) { + let appIcons = this.getAppIcons(); + appIcons.forEach(function(icon) { + icon.toggleNumberOverlay(activate); + }); + } + + _initializeIconSize(max_size) { + let max_allowed = baseIconSizes[baseIconSizes.length-1]; + max_size = Math.min(max_size, max_allowed); + + if (Docking.DockManager.settings.get_boolean('icon-size-fixed')) + this._availableIconSizes = [max_size]; + else { + this._availableIconSizes = baseIconSizes.filter(function(val) { + return (val 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) { + vadjustment.ease(vvalue, { + mode: Clutter.AnimationMode.EASE_OUT_QUAD, + duration: Util.SCROLL_TIME + }); + } + + if (hvalue !== hvalue0) { + hadjustment.ease(hvalue, { + mode: Clutter.AnimationMode.EASE_OUT_QUAD, + duration: Util.SCROLL_TIME + }); + } + + return [hvalue- hvalue0, vvalue - vvalue0]; +} diff --git a/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/dbusmenuUtils.js b/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/dbusmenuUtils.js new file mode 100644 index 0000000..0d2793d --- /dev/null +++ b/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/dbusmenuUtils.js @@ -0,0 +1,274 @@ +// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- + +const Atk = imports.gi.Atk; +const Clutter = imports.gi.Clutter; +let Dbusmenu = null; /* Dynamically imported */ +const Gio = imports.gi.Gio; +const GLib = imports.gi.GLib; +const St = imports.gi.St; + +const PopupMenu = imports.ui.popupMenu; + +const Me = imports.misc.extensionUtils.getCurrentExtension(); +const Utils = Me.imports.utils; + +// Dbusmenu features not (yet) supported: +// +// * The CHILD_DISPLAY property +// +// This seems to have only one possible value in the Dbusmenu API, so +// there's little point in depending on it--the code in libdbusmenu sets it +// if and only if an item has children, so for our purposes it's simpler +// and more intuitive to just check children.length. (This does ignore the +// possibility of a program not using libdbusmenu and setting CHILD_DISPLAY +// independently, perhaps to indicate that an childless menu item should +// nevertheless be displayed like a submenu.) +// +// * Children more than two levels deep +// +// PopupMenu doesn't seem to support submenus in submenus. +// +// * Shortcut keys +// +// If these keys are supposed to be installed as global shortcuts, we'd +// have to query these aggressively and not wait for the DBus menu to be +// mapped to a popup menu. A shortcut key that only works once the popup +// menu is open and has key focus is possibly of marginal value. + +function haveDBusMenu() { + if (Dbusmenu) + return Dbusmenu; + + try { + Dbusmenu = imports.gi.Dbusmenu; + return Dbusmenu; + } catch (e) { + log(`Failed to import DBusMenu, quicklists are not avaialble: ${e}`); + return null; + } +} + + +function makePopupMenuItem(dbusmenuItem, deep) { + // These are the only properties guaranteed to be available when the root + // item is first announced. Other properties might be loaded already, but + // be sure to connect to Dbusmenu.MENUITEM_SIGNAL_PROPERTY_CHANGED to get + // the most up-to-date values in case they aren't. + const itemType = dbusmenuItem.property_get(Dbusmenu.MENUITEM_PROP_TYPE); + const label = dbusmenuItem.property_get(Dbusmenu.MENUITEM_PROP_LABEL); + const visible = dbusmenuItem.property_get_bool(Dbusmenu.MENUITEM_PROP_VISIBLE); + const enabled = dbusmenuItem.property_get_bool(Dbusmenu.MENUITEM_PROP_ENABLED); + const accessibleDesc = dbusmenuItem.property_get(Dbusmenu.MENUITEM_PROP_ACCESSIBLE_DESC); + //const childDisplay = dbusmenuItem.property_get(Dbusmenu.MENUITEM_PROP_CHILD_DISPLAY); + + let item; + const signalsHandler = new Utils.GlobalSignalsHandler(); + const wantIcon = itemType === Dbusmenu.CLIENT_TYPES_IMAGE; + + // If the basic type of the menu item needs to change, call this. + const recreateItem = () => { + const newItem = makePopupMenuItem(dbusmenuItem, deep); + const parentMenu = item._parent; + parentMenu.addMenuItem(newItem); + // Reminder: Clutter thinks of later entries in the child list as + // "above" earlier ones, so "above" here means "below" in terms of the + // menu's vertical order. + parentMenu.actor.set_child_above_sibling(newItem.actor, item.actor); + if (newItem.menu) { + parentMenu.actor.set_child_above_sibling(newItem.menu.actor, newItem.actor); + } + parentMenu.actor.remove_child(item.actor); + item.destroy(); + item = null; + }; + + const updateDisposition = () => { + const disposition = dbusmenuItem.property_get(Dbusmenu.MENUITEM_PROP_DISPOSITION); + let icon_name = null; + switch (disposition) { + case Dbusmenu.MENUITEM_DISPOSITION_ALERT: + case Dbusmenu.MENUITEM_DISPOSITION_WARNING: + icon_name = 'dialog-warning-symbolic'; + break; + case Dbusmenu.MENUITEM_DISPOSITION_INFORMATIVE: + icon_name = 'dialog-information-symbolic'; + break; + } + if (icon_name) { + item._dispositionIcon = new St.Icon({ + icon_name, + style_class: 'popup-menu-icon', + y_align: Clutter.ActorAlign.CENTER, + y_expand: true, + }); + let expander; + for (let child = item.label.get_next_sibling();; child = child.get_next_sibling()) { + if (!child) { + expander = new St.Bin({ + style_class: 'popup-menu-item-expander', + x_expand: true, + }); + item.actor.add_child(expander); + break; + } else if (child instanceof St.Widget && child.has_style_class_name('popup-menu-item-expander')) { + expander = child; + break; + } + } + item.actor.insert_child_above(item._dispositionIcon, expander); + } else if (item._dispositionIcon) { + item.actor.remove_child(item._dispositionIcon); + item._dispositionIcon = null; + } + }; + + const updateIcon = () => { + if (!wantIcon) { + return; + } + const iconData = dbusmenuItem.property_get_byte_array(Dbusmenu.MENUITEM_PROP_ICON_DATA); + const iconName = dbusmenuItem.property_get(Dbusmenu.MENUITEM_PROP_ICON_NAME); + if (iconName) { + item.icon.icon_name = iconName; + } else if (iconData.length) { + item.icon.gicon = Gio.BytesIcon.new(iconData); + } + }; + + const updateOrnament = () => { + const toggleType = dbusmenuItem.property_get(Dbusmenu.MENUITEM_PROP_TOGGLE_TYPE); + switch (toggleType) { + case Dbusmenu.MENUITEM_TOGGLE_CHECK: + item.actor.accessible_role = Atk.Role.CHECK_MENU_ITEM; + break; + case Dbusmenu.MENUITEM_TOGGLE_RADIO: + item.actor.accessible_role = Atk.Role.RADIO_MENU_ITEM; + break; + default: + item.actor.accessible_role = Atk.Role.MENU_ITEM; + } + let ornament = PopupMenu.Ornament.NONE; + const state = dbusmenuItem.property_get_int(Dbusmenu.MENUITEM_PROP_TOGGLE_STATE); + if (state === Dbusmenu.MENUITEM_TOGGLE_STATE_UNKNOWN) { + // PopupMenu doesn't natively support an "unknown" ornament, but we + // can hack one in: + item.setOrnament(ornament); + item.actor.add_accessible_state(Atk.StateType.INDETERMINATE); + item._ornamentLabel.text = '\u2501'; + item.actor.remove_style_pseudo_class('checked'); + } else { + item.actor.remove_accessible_state(Atk.StateType.INDETERMINATE); + if (state === Dbusmenu.MENUITEM_TOGGLE_STATE_CHECKED) { + if (toggleType === Dbusmenu.MENUITEM_TOGGLE_CHECK) { + ornament = PopupMenu.Ornament.CHECK; + } else if (toggleType === Dbusmenu.MENUITEM_TOGGLE_RADIO) { + ornament = PopupMenu.Ornament.DOT; + } + item.actor.add_style_pseudo_class('checked'); + } else { + item.actor.remove_style_pseudo_class('checked'); + } + item.setOrnament(ornament); + } + }; + + const onPropertyChanged = (dbusmenuItem, name, value) => { + // `value` is null when a property is cleared, so handle those cases + // with sensible defaults. + switch (name) { + case Dbusmenu.MENUITEM_PROP_TYPE: + recreateItem(); + break; + case Dbusmenu.MENUITEM_PROP_ENABLED: + item.setSensitive(value ? value.unpack() : false); + break; + case Dbusmenu.MENUITEM_PROP_LABEL: + item.label.text = value ? value.unpack() : ''; + break; + case Dbusmenu.MENUITEM_PROP_VISIBLE: + item.actor.visible = value ? value.unpack() : false; + break; + case Dbusmenu.MENUITEM_PROP_DISPOSITION: + updateDisposition(); + break; + case Dbusmenu.MENUITEM_PROP_ACCESSIBLE_DESC: + item.actor.get_accessible().accessible_description = value && value.unpack() || ''; + break; + case Dbusmenu.MENUITEM_PROP_ICON_DATA: + case Dbusmenu.MENUITEM_PROP_ICON_NAME: + updateIcon(); + break; + case Dbusmenu.MENUITEM_PROP_TOGGLE_TYPE: + case Dbusmenu.MENUITEM_PROP_TOGGLE_STATE: + updateOrnament(); + break; + } + }; + + + // Start actually building the menu item. + const children = dbusmenuItem.get_children(); + if (children.length && !deep) { + // Make a submenu. + item = new PopupMenu.PopupSubMenuMenuItem(label, wantIcon); + const updateChildren = () => { + const children = dbusmenuItem.get_children(); + if (!children.length) { + return recreateItem(); + } + item.menu.removeAll(); + children.forEach(remoteChild => + item.menu.addMenuItem(makePopupMenuItem(remoteChild, true))); + }; + updateChildren(); + signalsHandler.add( + [dbusmenuItem, Dbusmenu.MENUITEM_SIGNAL_CHILD_ADDED, updateChildren], + [dbusmenuItem, Dbusmenu.MENUITEM_SIGNAL_CHILD_MOVED, updateChildren], + [dbusmenuItem, Dbusmenu.MENUITEM_SIGNAL_CHILD_REMOVED, updateChildren]); + + } else { + // Don't make a submenu. + if (!deep) { + // We only have the potential to get a submenu if we aren't deep. + signalsHandler.add( + [dbusmenuItem, Dbusmenu.MENUITEM_SIGNAL_CHILD_ADDED, recreateItem], + [dbusmenuItem, Dbusmenu.MENUITEM_SIGNAL_CHILD_MOVED, recreateItem], + [dbusmenuItem, Dbusmenu.MENUITEM_SIGNAL_CHILD_REMOVED, recreateItem]); + } + + if (itemType === Dbusmenu.CLIENT_TYPES_SEPARATOR) { + item = new PopupMenu.PopupSeparatorMenuItem(); + } else if (wantIcon) { + item = new PopupMenu.PopupImageMenuItem(label, null); + item.icon = item._icon; + } else { + item = new PopupMenu.PopupMenuItem(label); + } + } + + // Set common initial properties. + item.actor.visible = visible; + item.setSensitive(enabled); + if (accessibleDesc) { + item.actor.get_accessible().accessible_description = accessibleDesc; + } + updateDisposition(); + updateIcon(); + updateOrnament(); + + // Prevent an initial resize flicker. + if (wantIcon) { + item.icon.icon_size = 16; + } + + signalsHandler.add([dbusmenuItem, Dbusmenu.MENUITEM_SIGNAL_PROPERTY_CHANGED, onPropertyChanged]); + + // Connections on item will be lost when item is disposed; there's no need + // to add them to signalsHandler. + item.connect('activate', () => { + dbusmenuItem.handle_event(Dbusmenu.MENUITEM_EVENT_ACTIVATED, new GLib.Variant('i', 0), Math.floor(Date.now()/1000)); + }); + item.connect('destroy', () => signalsHandler.destroy()); + + return item; +} diff --git a/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/docking.js b/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/docking.js new file mode 100644 index 0000000..daa9de5 --- /dev/null +++ b/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/docking.js @@ -0,0 +1,1967 @@ +// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- + +const Clutter = imports.gi.Clutter; +const GLib = imports.gi.GLib; +const GObject = imports.gi.GObject; +const Meta = imports.gi.Meta; +const Shell = imports.gi.Shell; +const St = imports.gi.St; +const Params = imports.misc.params; + +const Main = imports.ui.main; +const Dash = imports.ui.dash; +const IconGrid = imports.ui.iconGrid; +const Overview = imports.ui.overview; +const OverviewControls = imports.ui.overviewControls; +const PointerWatcher = imports.ui.pointerWatcher; +const Signals = imports.signals; +const SearchController = imports.ui.searchController; +const WorkspaceSwitcherPopup= imports.ui.workspaceSwitcherPopup; +const Layout = imports.ui.layout; +const LayoutManager = imports.ui.main.layoutManager; + +const ExtensionUtils = imports.misc.extensionUtils; +const Me = ExtensionUtils.getCurrentExtension(); +const Utils = Me.imports.utils; +const Intellihide = Me.imports.intellihide; +const Theming = Me.imports.theming; +const MyDash = Me.imports.dash; +const LauncherAPI = Me.imports.launcherAPI; +const FileManager1API = Me.imports.fileManager1API; + +const DOCK_DWELL_CHECK_INTERVAL = 100; + +var State = { + HIDDEN: 0, + SHOWING: 1, + SHOWN: 2, + HIDING: 3 +}; + +const scrollAction = { + DO_NOTHING: 0, + CYCLE_WINDOWS: 1, + SWITCH_WORKSPACE: 2 +}; + +/** + * Ported from GNOME Shell 3.38 + * + * In GNOME Shell 40+ the dash is always visible, + * we need to re-include a spacer because our dash + * is not always visible. + */ +var DashSpacer = GObject.registerClass( + class DashSpacer extends Clutter.Actor { + _init(source) { + super._init(); + + this._bindConstraint = new Clutter.BindConstraint({ + source, + coordinate: Clutter.BindCoordinate.SIZE, + }); + this.add_constraint(this._bindConstraint); + } + + setMaxSize(size) { + // Handles overview controls trying to set the dash' max size. + } + + vfunc_get_preferred_width(forHeight) { + if (this._bindConstraint) + return this._bindConstraint.source.get_preferred_width(forHeight); + return super.vfunc_get_preferred_width(forHeight); + } + + vfunc_get_preferred_height(forWidth) { + if (this._bindConstraint) + return this._bindConstraint.source.get_preferred_height(forWidth); + return super.vfunc_get_preferred_height(forWidth); + } + } +); + + +/** + * A simple St.Widget with one child whose allocation takes into account the + * slide out of its child via the _slidex parameter ([0:1]). + * + * Required since I want to track the input region of this container which is + * based on its allocation even if the child overlows the parent actor. By doing + * this the region of the dash that is slideout is not steling anymore the input + * regions making the extesion usable when the primary monitor is the right one. + * + * The slidex parameter can be used to directly animate the sliding. The parent + * must have a WEST (SOUTH) anchor_point to achieve the sliding to the RIGHT (BOTTOM) + * side. +*/ +var DashSlideContainer = GObject.registerClass({ + Properties: { + 'side': GObject.ParamSpec.enum( + 'side', 'side', 'side', + GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY, + St.Side, St.Side.LEFT), + 'slidex': GObject.ParamSpec.double( + 'slidex', 'slidex', 'slidex', + GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT, + 0, 1, 1), + } +}, class DashToDock_DashSlideContainer extends St.Bin { + + _init(params = {}) { + super._init(params); + + // slide parameter: 1 = visible, 0 = hidden. + this._slidex = params.slidex || 1; + this._slideoutSize = 0; // minimum size when slided out + } + + vfunc_allocate(box, flags) { + let contentBox = this.get_theme_node().get_content_box(box); + + this.set_allocation(box); + + if (this.child == null) + return; + + let availWidth = contentBox.x2 - contentBox.x1; + let availHeight = contentBox.y2 - contentBox.y1; + let [, , natChildWidth, natChildHeight] = + this.child.get_preferred_size(); + + let childWidth = natChildWidth; + let childHeight = natChildHeight; + + let childBox = new Clutter.ActorBox(); + + let slideoutSize = this._slideoutSize; + + if (this.side == St.Side.LEFT) { + childBox.x1 = (this._slidex -1) * (childWidth - slideoutSize); + childBox.x2 = slideoutSize + this._slidex*(childWidth - slideoutSize); + childBox.y1 = 0; + childBox.y2 = childBox.y1 + childHeight; + } + else if ((this.side == St.Side.RIGHT) || (this.side == St.Side.BOTTOM)) { + childBox.x1 = 0; + childBox.x2 = childWidth; + childBox.y1 = 0; + childBox.y2 = childBox.y1 + childHeight; + } + else if (this.side == St.Side.TOP) { + childBox.x1 = 0; + childBox.x2 = childWidth; + childBox.y1 = (this._slidex -1) * (childHeight - slideoutSize); + childBox.y2 = slideoutSize + this._slidex * (childHeight - slideoutSize); + } + + this.child.allocate(childBox); + + this.child.set_clip(-childBox.x1, -childBox.y1, + -childBox.x1+availWidth, -childBox.y1 + availHeight); + } + + /** + * Just the child width but taking into account the slided out part + */ + vfunc_get_preferred_width(forHeight) { + let [minWidth, natWidth] = super.vfunc_get_preferred_width(forHeight); + if ((this.side == St.Side.LEFT) || (this.side == St.Side.RIGHT)) { + minWidth = (minWidth - this._slideoutSize) * this._slidex + this._slideoutSize; + natWidth = (natWidth - this._slideoutSize) * this._slidex + this._slideoutSize; + } + return [minWidth, natWidth]; + } + + /** + * Just the child height but taking into account the slided out part + */ + vfunc_get_preferred_height(forWidth) { + let [minHeight, natHeight] = super.vfunc_get_preferred_height(forWidth); + if ((this.side == St.Side.TOP) || (this.side == St.Side.BOTTOM)) { + minHeight = (minHeight - this._slideoutSize) * this._slidex + this._slideoutSize; + natHeight = (natHeight - this._slideoutSize) * this._slidex + this._slideoutSize; + } + return [minHeight, natHeight]; + } + + set slidex(value) { + if (value == this._slidex) + return; + + this._slidex = value; + this.notify('slidex'); + + this.queue_relayout(); + } + + get slidex() { + return this._slidex; + } +}); + +var DockedDash = GObject.registerClass({ + Signals: { + 'showing': {}, + 'hiding': {}, + } +}, class DashToDock extends St.Bin { + + _init(remoteModel, monitorIndex) { + this._rtl = (Clutter.get_default_text_direction() == Clutter.TextDirection.RTL); + + // Load settings + let settings = DockManager.settings; + this._remoteModel = remoteModel; + this._monitorIndex = monitorIndex; + // Connect global signals + this._signalsHandler = new Utils.GlobalSignalsHandler(); + + this._bindSettingsChanges(); + + this._position = Utils.getPosition(); + this._isHorizontal = ((this._position == St.Side.TOP) || (this._position == St.Side.BOTTOM)); + + // Temporary ignore hover events linked to autohide for whatever reason + this._ignoreHover = false; + this._oldignoreHover = null; + // This variables are linked to the settings regardles of autohide or intellihide + // being temporary disable. Get set by _updateVisibilityMode; + this._autohideIsEnabled = null; + this._intellihideIsEnabled = null; + this._fixedIsEnabled = null; + + // Create intellihide object to monitor windows overlapping + this._intellihide = new Intellihide.Intellihide(this._monitorIndex); + + // initialize dock state + this._dockState = State.HIDDEN; + + // Put dock on the required monitor + this._monitor = Main.layoutManager.monitors[this._monitorIndex]; + + // this store size and the position where the dash is shown; + // used by intellihide module to check window overlap. + this.staticBox = new Clutter.ActorBox(); + + // Initialize pressure barrier variables + this._canUsePressure = false; + this._pressureBarrier = null; + this._barrier = null; + this._removeBarrierTimeoutId = 0; + + // Initialize dwelling system variables + this._dockDwelling = false; + this._dockWatch = null; + this._dockDwellUserTime = 0; + this._dockDwellTimeoutId = 0 + + // Create a new dash object + this.dash = new MyDash.MyDash(this._remoteModel, this._monitorIndex); + + if (Main.overview.isDummy || !settings.get_boolean('show-show-apps-button')) + this.dash.hideShowAppsButton(); + + // Create the main actor and the containers for sliding in and out and + // centering, turn on track hover + + let positionStyleClass = ['top', 'right', 'bottom', 'left']; + // This is the centering actor + super._init({ + name: 'dashtodockContainer', + reactive: false, + style_class: positionStyleClass[this._position], + }); + + // This is the sliding actor whose allocation is to be tracked for input regions + this._slider = new DashSlideContainer({ + side: this._position, + slidex: 0, + ...(this._isHorizontal ? { + x_align: Clutter.ActorAlign.CENTER, + } : { + y_align: Clutter.ActorAlign.CENTER, + }) + }); + + // This is the actor whose hover status us tracked for autohide + this._box = new St.BoxLayout({ + name: 'dashtodockBox', + reactive: true, + track_hover: true + }); + this._box.connect('notify::hover', this._hoverChanged.bind(this)); + + this._signalsHandler.add([ + // update when workarea changes, for instance if other extensions modify the struts + //(like moving th panel at the bottom) + global.display, + 'workareas-changed', + this._resetPosition.bind(this) + ], [ + global.display, + 'in-fullscreen-changed', + this._updateBarrier.bind(this) + ], [ + // Monitor windows overlapping + this._intellihide, + 'status-changed', + this._updateDashVisibility.bind(this) + ], [ + // sync hover after a popupmenu is closed + this.dash, + 'menu-closed', + () => { this._box.sync_hover() } + ]); + + if (!Main.overview.isDummy) { + this._signalsHandler.add([ + Main.overview, + 'item-drag-begin', + this._onDragStart.bind(this) + ], [ + Main.overview, + 'item-drag-end', + this._onDragEnd.bind(this) + ], [ + Main.overview, + 'item-drag-cancelled', + this._onDragEnd.bind(this) + ], [ + Main.overview, + 'showing', + this._onOverviewShowing.bind(this) + ], [ + Main.overview, + 'hiding', + this._onOverviewHiding.bind(this) + ], + [ + Main.overview, + 'hidden', + this._onOverviewHidden.bind(this) + ]); + } + + this._injectionsHandler = new Utils.InjectionsHandler(); + this._themeManager = new Theming.ThemeManager(this); + + // Since the actor is not a topLevel child and its parent is now not added to the Chrome, + // the allocation change of the parent container (slide in and slideout) doesn't trigger + // anymore an update of the input regions. Force the update manually. + this.connect('notify::allocation', + Main.layoutManager._queueUpdateRegions.bind(Main.layoutManager)); + + + // Since Clutter has no longer ClutterAllocationFlags, + // "allocation-changed" signal has been removed. MR !1245 + this.dash._container.connect('notify::allocation', this._updateStaticBox.bind(this)); + this._slider.connect(this._isHorizontal ? 'notify::x' : 'notify::y', this._updateStaticBox.bind(this)); + + // Load optional features that need to be activated for one dock only + if (this._monitorIndex == settings.get_int('preferred-monitor')) + this._enableExtraFeatures(); + // Load optional features that need to be activated once per dock + this._optionalScrollWorkspaceSwitch(); + + // Delay operations that require the shell to be fully loaded and with + // user theme applied. + + this._paintId = global.stage.connect('after-paint', this._initialize.bind(this)); + + // Reserve space for the dash in the overview. + this._dashSpacer = new DashSpacer(this._box); + + // Add dash container actor and the container to the Chrome. + this.set_child(this._slider); + this._slider.set_child(this._box); + this._box.add_actor(this.dash); + + // Add aligning container without tracking it for input region + Main.uiGroup.add_child(this); + if (Main.uiGroup.contains(global.top_window_group)) + Main.uiGroup.set_child_below_sibling(this, global.top_window_group); + + if (settings.get_boolean('dock-fixed')) { + // Note: tracking the fullscreen directly on the slider actor causes some hiccups when fullscreening + // windows of certain applications + Main.layoutManager._trackActor(this, {affectsInputRegion: false, trackFullscreen: true}); + Main.layoutManager._trackActor(this._slider, {affectsStruts: true}); + } + else + Main.layoutManager._trackActor(this._slider); + + // Create and apply height/width constraint to the dash. + if (this._isHorizontal) { + this.connect('notify::width', () => { + this.dash.setMaxSize(this.width, this.height); + }); + } else { + this.connect('notify::height', () => { + this.dash.setMaxSize(this.width, this.height) + }); + } + + if (this._position == St.Side.RIGHT) + this.connect('notify::width', () => this.translation_x = -this.width); + else if (this._position == St.Side.BOTTOM) + this.connect('notify::height', () => this.translation_y = -this.height); + + // Set initial position + this._resetPosition(); + + this.connect('destroy', this._onDestroy.bind(this)); + } + + _untrackDock() { + Main.layoutManager._untrackActor(this); + Main.layoutManager._untrackActor(this._slider); + + } + + _trackDock() { + if (DockManager.settings.get_boolean('dock-fixed')) { + if (Main.layoutManager._findActor(this) == -1) + Main.layoutManager._trackActor(this, { affectsInputRegion: false, trackFullscreen: true }); + if (Main.layoutManager._findActor(this._slider) == -1) + Main.layoutManager._trackActor(this._slider, { affectsStruts: true }); + } else { + if (Main.layoutManager._findActor(this._slider) == -1) + Main.layoutManager._trackActor(this._slider); + } + } + + _initialize() { + log('[dash-to-dock] initializing...'); + + if (this._paintId > 0) { + global.stage.disconnect(this._paintId); + this._paintId = 0; + } + + // Apply custome css class according to the settings + this._themeManager.updateCustomTheme(); + + this._updateVisibilityMode(); + + // In case we are already inside the overview when the extension is loaded, + // for instance on unlocking the screen if it was locked with the overview open. + if (Main.overview.visibleTarget) { + this._onOverviewShowing(); + } + + // Setup pressure barrier (GS38+ only) + this._updatePressureBarrier(); + this._updateBarrier(); + + // setup dwelling system if pressure barriers are not available + this._setupDockDwellIfNeeded(); + } + + _onDestroy() { + // Disconnect global signals + this._signalsHandler.destroy(); + // The dash, intellihide and themeManager have global signals as well internally + this.dash.destroy(); + this._intellihide.destroy(); + this._themeManager.destroy(); + + this._injectionsHandler.destroy(); + + if (this._marginLater) { + Meta.later_remove(this._marginLater); + delete this._marginLater; + } + + // Remove barrier timeout + if (this._removeBarrierTimeoutId > 0) + GLib.source_remove(this._removeBarrierTimeoutId); + + // Remove existing barrier + this._removeBarrier(); + + // Remove pointer watcher + if (this._dockWatch) { + PointerWatcher.getPointerWatcher()._removeWatch(this._dockWatch); + this._dockWatch = null; + } + } + + _bindSettingsChanges() { + let settings = DockManager.settings; + this._signalsHandler.add([ + settings, + 'changed::scroll-action', + () => { this._optionalScrollWorkspaceSwitch(); } + ], [ + settings, + 'changed::dash-max-icon-size', + () => { this.dash.setIconSize(settings.get_int('dash-max-icon-size')); } + ], [ + settings, + 'changed::icon-size-fixed', + () => { this.dash.setIconSize(settings.get_int('dash-max-icon-size')); } + ], [ + settings, + 'changed::show-favorites', + () => { this.dash.resetAppIcons(); } + ], [ + settings, + 'changed::show-trash', + () => { this.dash.resetAppIcons(); }, + Utils.SignalsHandlerFlags.CONNECT_AFTER, + ], [ + settings, + 'changed::show-mounts', + () => { this.dash.resetAppIcons(); }, + Utils.SignalsHandlerFlags.CONNECT_AFTER + ], [ + settings, + 'changed::show-running', + () => { this.dash.resetAppIcons(); } + ], [ + settings, + 'changed::show-apps-at-top', + () => { this.dash.updateShowAppsButton(); } + ], [ + settings, + 'changed::show-show-apps-button', + () => { + if (!Main.overview.isDummy && + settings.get_boolean('show-show-apps-button')) + this.dash.showShowAppsButton(); + else + this.dash.hideShowAppsButton(); + } + ], [ + settings, + 'changed::dock-fixed', + () => { + this._untrackDock(); + this._trackDock(); + + this._resetPosition(); + + // Add or remove barrier depending on if dock-fixed + this._updateBarrier(); + + this._updateVisibilityMode(); + } + ], [ + settings, + 'changed::intellihide', + this._updateVisibilityMode.bind(this) + ], [ + settings, + 'changed::intellihide-mode', + () => { this._intellihide.forceUpdate(); } + ], [ + settings, + 'changed::autohide', + () => { + this._updateVisibilityMode(); + this._updateBarrier(); + } + ], [ + settings, + 'changed::autohide-in-fullscreen', + this._updateBarrier.bind(this) + ], + [ + settings, + 'changed::extend-height', + this._resetPosition.bind(this) + ], [ + settings, + 'changed::height-fraction', + this._resetPosition.bind(this) + ], [ + settings, + 'changed::require-pressure-to-show', + () => { + // Remove pointer watcher + if (this._dockWatch) { + PointerWatcher.getPointerWatcher()._removeWatch(this._dockWatch); + this._dockWatch = null; + } + this._setupDockDwellIfNeeded(); + this._updateBarrier(); + } + ], [ + settings, + 'changed::pressure-threshold', + () => { + this._updatePressureBarrier(); + this._updateBarrier(); + } + ]); + + } + + /** + * This is call when visibility settings change + */ + _updateVisibilityMode() { + let settings = DockManager.settings; + if (settings.get_boolean('dock-fixed')) { + this._fixedIsEnabled = true; + this._autohideIsEnabled = false; + this._intellihideIsEnabled = false; + } + else { + this._fixedIsEnabled = false; + this._autohideIsEnabled = settings.get_boolean('autohide') + this._intellihideIsEnabled = settings.get_boolean('intellihide') + } + + if (this._autohideIsEnabled) + this.add_style_class_name('autohide'); + else + this.remove_style_class_name('autohide'); + + if (this._intellihideIsEnabled) + this._intellihide.enable(); + else + this._intellihide.disable(); + + this._updateDashVisibility(); + } + + /** + * Show/hide dash based on, in order of priority: + * overview visibility + * fixed mode + * intellihide + * autohide + * overview visibility + */ + _updateDashVisibility() { + if (Main.overview.visibleTarget) + return; + + let settings = DockManager.settings; + + if (this._fixedIsEnabled) { + this._removeAnimations(); + this._animateIn(settings.get_double('animation-time'), 0); + } + else if (this._intellihideIsEnabled) { + if (this._intellihide.getOverlapStatus()) { + this._ignoreHover = false; + // Do not hide if autohide is enabled and mouse is hover + if (!this._box.hover || !this._autohideIsEnabled) + this._animateOut(settings.get_double('animation-time'), 0); + } + else { + this._ignoreHover = true; + this._removeAnimations(); + this._animateIn(settings.get_double('animation-time'), 0); + } + } + else { + if (this._autohideIsEnabled) { + this._ignoreHover = false; + + if (this._box.hover) + this._animateIn(settings.get_double('animation-time'), 0); + else + this._animateOut(settings.get_double('animation-time'), 0); + } + else + this._animateOut(settings.get_double('animation-time'), 0); + } + } + + _onOverviewShowing() { + this.add_style_class_name('overview'); + + this._ignoreHover = true; + this._intellihide.disable(); + this._removeAnimations(); + // The overview uses the monitor's work area to calculate background size. + // If our dock is fixed, it will shrink the monitor's work area unexpectedly. + this._untrackDock(); + this._animateIn(DockManager.settings.get_double('animation-time'), 0); + } + + _onOverviewHiding() { + this._ignoreHover = false; + this._intellihide.enable(); + this._trackDock(); + this._updateDashVisibility(); + } + + _onOverviewHidden() { + this.remove_style_class_name('overview'); + } + + _hoverChanged() { + if (!this._ignoreHover) { + // Skip if dock is not in autohide mode for instance because it is shown + // by intellihide. + if (this._autohideIsEnabled) { + if (this._box.hover) + this._show(); + else + this._hide(); + } + } + } + + getDockState() { + return this._dockState; + } + + _show() { + this._delayedHide = false; + if ((this._dockState == State.HIDDEN) || (this._dockState == State.HIDING)) { + if (this._dockState == State.HIDING) + // suppress all potential queued transitions - i.e. added but not started, + // always give priority to show + this._removeAnimations(); + + this.emit('showing'); + this._animateIn(DockManager.settings.get_double('animation-time'), 0); + } + } + + _hide() { + // If no hiding animation is running or queued + if ((this._dockState == State.SHOWN) || (this._dockState == State.SHOWING)) { + let settings = DockManager.settings; + let delay = settings.get_double('hide-delay'); + + if (this._dockState == State.SHOWING) { + // if a show already started, let it finish; queue hide without removing the show. + // to obtain this, we wait for the animateIn animation to be completed + this._delayedHide = true; + return; + } + + this.emit('hiding'); + this._animateOut(settings.get_double('animation-time'), delay); + } + } + + _animateIn(time, delay) { + this._dockState = State.SHOWING; + this.dash.iconAnimator.start(); + this._delayedHide = false; + + this._slider.ease_property('slidex', 1, { + duration: time * 1000, + delay: delay * 1000, + mode: Clutter.AnimationMode.EASE_OUT_QUAD, + onComplete: () => { + this._dockState = State.SHOWN; + // Remove barrier so that mouse pointer is released and can access monitors on other side of dock + // NOTE: Delay needed to keep mouse from moving past dock and re-hiding dock immediately. This + // gives users an opportunity to hover over the dock + if (this._removeBarrierTimeoutId > 0) + GLib.source_remove(this._removeBarrierTimeoutId); + + if (!this._delayedHide) { + this._removeBarrierTimeoutId = GLib.timeout_add( + GLib.PRIORITY_DEFAULT, 100, this._removeBarrier.bind(this)); + } else { + this._hide(); + } + } + }); + } + + _animateOut(time, delay) { + this._dockState = State.HIDING; + + this._slider.ease_property('slidex', 0, { + duration: time * 1000, + delay: delay * 1000, + mode: Clutter.AnimationMode.EASE_OUT_QUAD, + onComplete: () => { + this._dockState = State.HIDDEN; + // Remove queued barried removal if any + if (this._removeBarrierTimeoutId > 0) + GLib.source_remove(this._removeBarrierTimeoutId); + this._updateBarrier(); + this.dash.iconAnimator.pause(); + } + }); + } + + /** + * Dwelling system based on the GNOME Shell 3.14 messageTray code. + */ + _setupDockDwellIfNeeded() { + // If we don't have extended barrier features, then we need + // to support the old tray dwelling mechanism. + if (!global.display.supports_extended_barriers() || + !DockManager.settings.get_boolean('require-pressure-to-show')) { + let pointerWatcher = PointerWatcher.getPointerWatcher(); + this._dockWatch = pointerWatcher.addWatch(DOCK_DWELL_CHECK_INTERVAL, this._checkDockDwell.bind(this)); + this._dockDwelling = false; + this._dockDwellUserTime = 0; + } + } + + _checkDockDwell(x, y) { + + let workArea = Main.layoutManager.getWorkAreaForMonitor(this._monitor.index) + let shouldDwell; + // Check for the correct screen edge, extending the sensitive area to the whole workarea, + // minus 1 px to avoid conflicting with other active corners. + if (this._position == St.Side.LEFT) + shouldDwell = (x == this._monitor.x) && (y > workArea.y) && (y < workArea.y + workArea.height); + else if (this._position == St.Side.RIGHT) + shouldDwell = (x == this._monitor.x + this._monitor.width - 1) && (y > workArea.y) && (y < workArea.y + workArea.height); + else if (this._position == St.Side.TOP) + shouldDwell = (y == this._monitor.y) && (x > workArea.x) && (x < workArea.x + workArea.width); + else if (this._position == St.Side.BOTTOM) + shouldDwell = (y == this._monitor.y + this._monitor.height - 1) && (x > workArea.x) && (x < workArea.x + workArea.width); + + if (shouldDwell) { + // We only set up dwell timeout when the user is not hovering over the dock + // already (!this._box.hover). + // The _dockDwelling variable is used so that we only try to + // fire off one dock dwell - if it fails (because, say, the user has the mouse down), + // we don't try again until the user moves the mouse up and down again. + if (!this._dockDwelling && !this._box.hover && (this._dockDwellTimeoutId == 0)) { + // Save the interaction timestamp so we can detect user input + let focusWindow = global.display.focus_window; + this._dockDwellUserTime = focusWindow ? focusWindow.user_time : 0; + + this._dockDwellTimeoutId = GLib.timeout_add( + GLib.PRIORITY_DEFAULT, + DockManager.settings.get_double('show-delay') * 1000, + this._dockDwellTimeout.bind(this)); + GLib.Source.set_name_by_id(this._dockDwellTimeoutId, '[dash-to-dock] this._dockDwellTimeout'); + } + this._dockDwelling = true; + } + else { + this._cancelDockDwell(); + this._dockDwelling = false; + } + } + + _cancelDockDwell() { + if (this._dockDwellTimeoutId != 0) { + GLib.source_remove(this._dockDwellTimeoutId); + this._dockDwellTimeoutId = 0; + } + } + + _dockDwellTimeout() { + this._dockDwellTimeoutId = 0; + + if (!DockManager.settings.get_boolean('autohide-in-fullscreen') && + this._monitor.inFullscreen) + return GLib.SOURCE_REMOVE; + + // We don't want to open the tray when a modal dialog + // is up, so we check the modal count for that. When we are in the + // overview we have to take the overview's modal push into account + if (Main.modalCount > (Main.overview.visible ? 1 : 0)) + return GLib.SOURCE_REMOVE; + + // If the user interacted with the focus window since we started the tray + // dwell (by clicking or typing), don't activate the message tray + let focusWindow = global.display.focus_window; + let currentUserTime = focusWindow ? focusWindow.user_time : 0; + if (currentUserTime != this._dockDwellUserTime) + return GLib.SOURCE_REMOVE; + + // Reuse the pressure version function, the logic is the same + this._onPressureSensed(); + return GLib.SOURCE_REMOVE; + } + + _updatePressureBarrier() { + let settings = DockManager.settings; + this._canUsePressure = global.display.supports_extended_barriers(); + let pressureThreshold = settings.get_double('pressure-threshold'); + + // Remove existing pressure barrier + if (this._pressureBarrier) { + this._pressureBarrier.destroy(); + this._pressureBarrier = null; + } + + if (this._barrier) { + this._barrier.destroy(); + this._barrier = null; + } + + // Create new pressure barrier based on pressure threshold setting + if (this._canUsePressure) { + this._pressureBarrier = new Layout.PressureBarrier(pressureThreshold, settings.get_double('show-delay')*1000, + Shell.ActionMode.NORMAL | Shell.ActionMode.OVERVIEW); + this._pressureBarrier.connect('trigger', (barrier) => { + if (!settings.get_boolean('autohide-in-fullscreen') && this._monitor.inFullscreen) + return; + this._onPressureSensed(); + }); + } + } + + /** + * handler for mouse pressure sensed + */ + _onPressureSensed() { + if (Main.overview.visibleTarget) + return; + + // In case the mouse move away from the dock area before hovering it, in such case the leave event + // would never be triggered and the dock would stay visible forever. + let triggerTimeoutId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 250, () => { + triggerTimeoutId = 0; + + let [x, y, mods] = global.get_pointer(); + let shouldHide = true; + switch (this._position) { + case St.Side.LEFT: + if (x <= this.staticBox.x2 && + x >= this._monitor.x && + y >= this._monitor.y && + y <= this._monitor.y + this._monitor.height) { + shouldHide = false; + } + break; + case St.Side.RIGHT: + if (x >= this.staticBox.x1 && + x <= this._monitor.x + this._monitor.width && + y >= this._monitor.y && + y <= this._monitor.y + this._monitor.height) { + shouldHide = false; + } + break; + case St.Side.TOP: + if (x >= this._monitor.x && + x <= this._monitor.x + this._monitor.width && + y <= this.staticBox.y2 && + y >= this._monitor.y) { + shouldHide = false; + } + break; + case St.Side.BOTTOM: + if (x >= this._monitor.x && + x <= this._monitor.x + this._monitor.width && + y >= this.staticBox.y1 && + y <= this._monitor.y + this._monitor.height) { + shouldHide = false; + } + } + if (shouldHide) { + this._hoverChanged(); + return GLib.SOURCE_REMOVE; + } + else { + return GLib.SOURCE_CONTINUE; + } + + }); + + this._show(); + } + + /** + * Remove pressure barrier + */ + _removeBarrier() { + if (this._barrier) { + if (this._pressureBarrier) + this._pressureBarrier.removeBarrier(this._barrier); + this._barrier.destroy(); + this._barrier = null; + } + this._removeBarrierTimeoutId = 0; + return false; + } + + /** + * Update pressure barrier size + */ + _updateBarrier() { + // Remove existing barrier + this._removeBarrier(); + + // The barrier needs to be removed in fullscreen with autohide disabled, otherwise the mouse can + // get trapped on monitor. + if (this._monitor.inFullscreen && + !DockManager.settings.get_boolean('autohide-in-fullscreen')) + return + + // Manually reset pressure barrier + // This is necessary because we remove the pressure barrier when it is triggered to show the dock + if (this._pressureBarrier) { + this._pressureBarrier._reset(); + this._pressureBarrier._isTriggered = false; + } + + // Create new barrier + // The barrier extends to the whole workarea, minus 1 px to avoid conflicting with other active corners + // Note: dash in fixed position doesn't use pressure barrier. + if (this._canUsePressure && this._autohideIsEnabled && + DockManager.settings.get_boolean('require-pressure-to-show')) { + let x1, x2, y1, y2, direction; + let workArea = Main.layoutManager.getWorkAreaForMonitor(this._monitor.index) + + if (this._position == St.Side.LEFT) { + x1 = this._monitor.x + 1; + x2 = x1; + y1 = workArea.y + 1; + y2 = workArea.y + workArea.height - 1; + direction = Meta.BarrierDirection.POSITIVE_X; + } + else if (this._position == St.Side.RIGHT) { + x1 = this._monitor.x + this._monitor.width - 1; + x2 = x1; + y1 = workArea.y + 1; + y2 = workArea.y + workArea.height - 1; + direction = Meta.BarrierDirection.NEGATIVE_X; + } + else if (this._position == St.Side.TOP) { + x1 = workArea.x + 1; + x2 = workArea.x + workArea.width - 1; + y1 = this._monitor.y; + y2 = y1; + direction = Meta.BarrierDirection.POSITIVE_Y; + } + else if (this._position == St.Side.BOTTOM) { + x1 = workArea.x + 1; + x2 = workArea.x + workArea.width - 1; + y1 = this._monitor.y + this._monitor.height; + y2 = y1; + direction = Meta.BarrierDirection.NEGATIVE_Y; + } + + if (this._pressureBarrier && this._dockState == State.HIDDEN) { + this._barrier = new Meta.Barrier({ + display: global.display, + x1: x1, + x2: x2, + y1: y1, + y2: y2, + directions: direction + }); + this._pressureBarrier.addBarrier(this._barrier); + } + } + } + + _isPrimaryMonitor() { + return (this._monitorIndex == Main.layoutManager.primaryIndex); + } + + _resetPosition() { + // Ensure variables linked to settings are updated. + this._updateVisibilityMode(); + + let extendHeight = DockManager.settings.get_boolean('extend-height'); + + // Note: do not use the workarea coordinates in the direction on which the dock is placed, + // to avoid a loop [position change -> workArea change -> position change] with + // fixed dock. + let workArea = Main.layoutManager.getWorkAreaForMonitor(this._monitorIndex); + + + let fraction = DockManager.settings.get_double('height-fraction'); + + if (extendHeight) + fraction = 1; + else if ((fraction < 0) || (fraction > 1)) + fraction = 0.95; + + if (this._isHorizontal) { + this.width = Math.round(fraction * workArea.width); + + let pos_y = this._monitor.y; + if (this._position == St.Side.BOTTOM) + pos_y += this._monitor.height; + + this.x = workArea.x + Math.round((1 - fraction) / 2 * workArea.width); + this.y = pos_y; + + if (extendHeight) { + this.dash._container.set_width(this.width); + this.add_style_class_name('extended'); + } else { + this.dash._container.set_width(-1); + this.remove_style_class_name('extended'); + } + } else { + this.height = Math.round(fraction * workArea.height); + + let pos_x = this._monitor.x; + if (this._position == St.Side.RIGHT) + pos_x += this._monitor.width; + + this.x = pos_x; + this.y = workArea.y + Math.round((1 - fraction) / 2 * workArea.height); + + this._signalsHandler.removeWithLabel('verticalOffsetChecker'); + + if (extendHeight) { + this.dash._container.set_height(this.height); + this.add_style_class_name('extended'); + } else { + this.dash._container.set_height(-1); + this.remove_style_class_name('extended'); + } + } + } + + _updateStaticBox() { + this.staticBox.init_rect( + this.x + this._slider.x - (this._position == St.Side.RIGHT ? this._box.width : 0), + this.y + this._slider.y - (this._position == St.Side.BOTTOM ? this._box.height : 0), + this._box.width, + this._box.height + ); + + this._intellihide.updateTargetBox(this.staticBox); + } + + _removeAnimations() { + this._slider.remove_all_transitions(); + } + + _onDragStart() { + this._oldignoreHover = this._ignoreHover; + this._ignoreHover = true; + this._animateIn(DockManager.settings.get_double('animation-time'), 0); + } + + _onDragEnd() { + if (this._oldignoreHover !== null) + this._ignoreHover = this._oldignoreHover; + this._oldignoreHover = null; + this._box.sync_hover(); + } + + /** + * Show dock and give key focus to it + */ + _onAccessibilityFocus() { + this._box.navigate_focus(null, St.DirectionType.TAB_FORWARD, false); + this._animateIn(DockManager.settings.get_double('animation-time'), 0); + } + + // Optional features to be enabled only for the main Dock + _enableExtraFeatures() { + // Restore dash accessibility + Main.ctrlAltTabManager.addGroup( + this.dash, _('Dash'), 'user-bookmarks-symbolic', + {focusCallback: this._onAccessibilityFocus.bind(this)}); + } + + /** + * Switch workspace by scrolling over the dock + */ + _optionalScrollWorkspaceSwitch() { + let label = 'optionalScrollWorkspaceSwitch'; + + function isEnabled() { + return DockManager.settings.get_enum('scroll-action') === scrollAction.SWITCH_WORKSPACE; + } + + DockManager.settings.connect('changed::scroll-action', () => { + if (isEnabled.bind(this)()) + enable.bind(this)(); + else + disable.bind(this)(); + }); + + if (isEnabled.bind(this)()) + enable.bind(this)(); + + function enable() { + this._signalsHandler.removeWithLabel(label); + + this._signalsHandler.addWithLabel(label, [ + this._box, + 'scroll-event', + onScrollEvent.bind(this) + ]); + } + + function disable() { + this._signalsHandler.removeWithLabel(label); + + if (this._optionalScrollWorkspaceSwitchDeadTimeId) { + GLib.source_remove(this._optionalScrollWorkspaceSwitchDeadTimeId); + this._optionalScrollWorkspaceSwitchDeadTimeId = 0; + } + } + + // This was inspired to desktop-scroller@obsidien.github.com + function onScrollEvent(actor, event) { + // When in overview change workspace only in windows view + if (Main.overview.visible) + return false; + + let activeWs = global.workspace_manager.get_active_workspace(); + let direction = null; + + switch (event.get_scroll_direction()) { + case Clutter.ScrollDirection.UP: + direction = Meta.MotionDirection.LEFT; + break; + case Clutter.ScrollDirection.DOWN: + direction = Meta.MotionDirection.RIGHT; + break; + case Clutter.ScrollDirection.SMOOTH: + let [dx, dy] = event.get_scroll_delta(); + if (dy < 0) + direction = Meta.MotionDirection.LEFT; + else if (dy > 0) + direction = Meta.MotionDirection.RIGHT; + break; + } + + if (direction !== null) { + // Prevent scroll events from triggering too many workspace switches + // by adding a 250ms deadtime between each scroll event. + // Usefull on laptops when using a touchpad. + + // During the deadtime do nothing + if (this._optionalScrollWorkspaceSwitchDeadTimeId) + return false; + else + this._optionalScrollWorkspaceSwitchDeadTimeId = GLib.timeout_add( + GLib.PRIORITY_DEFAULT, 250, () => { + this._optionalScrollWorkspaceSwitchDeadTimeId = 0; + }); + + let ws; + + ws = activeWs.get_neighbor(direction) + + if (Main.wm._workspaceSwitcherPopup == null) + // Support Workspace Grid extension showing their custom Grid Workspace Switcher + if (global.workspace_manager.workspace_grid !== undefined) { + Main.wm._workspaceSwitcherPopup = + global.workspace_manager.workspace_grid.getWorkspaceSwitcherPopup(); + } else { + Main.wm._workspaceSwitcherPopup = new WorkspaceSwitcherPopup.WorkspaceSwitcherPopup(); + } + // Set the actor non reactive, so that it doesn't prevent the + // clicks events from reaching the dash actor. I can't see a reason + // why it should be reactive. + Main.wm._workspaceSwitcherPopup.reactive = false; + Main.wm._workspaceSwitcherPopup.connect('destroy', function() { + Main.wm._workspaceSwitcherPopup = null; + }); + + // If Workspace Grid is installed, let them handle the scroll behaviour. + if (global.workspace_manager.workspace_grid !== undefined) + ws = global.workspace_manager.workspace_grid.actionMoveWorkspace(direction); + else + Main.wm.actionMoveWorkspace(ws); + + // Do not show workspaceSwitcher in overview + if (!Main.overview.visible) + Main.wm._workspaceSwitcherPopup.display(direction, ws.index()); + + return true; + } + else + return false; + } + } + + _activateApp(appIndex) { + let children = this.dash._box.get_children().filter(function(actor) { + return actor.child && + actor.child.app; + }); + + // Apps currently in the dash + let apps = children.map(function(actor) { + return actor.child; + }); + + // Activate with button = 1, i.e. same as left click + let button = 1; + if (appIndex < apps.length) + apps[appIndex].activate(button); + } +}); + +/* + * Handle keybaord shortcuts + */ +const DashToDock_KeyboardShortcuts_NUM_HOTKEYS = 10; + +var KeyboardShortcuts = class DashToDock_KeyboardShortcuts { + + constructor() { + this._signalsHandler = new Utils.GlobalSignalsHandler(); + + this._hotKeysEnabled = false; + if (DockManager.settings.get_boolean('hot-keys')) + this._enableHotKeys(); + + this._signalsHandler.add([ + DockManager.settings, + 'changed::hot-keys', + () => { + if (DockManager.settings.get_boolean('hot-keys')) + this._enableHotKeys.bind(this)(); + else + this._disableHotKeys.bind(this)(); + } + ]); + + this._optionalNumberOverlay(); + } + + destroy() { + // Remove keybindings + this._disableHotKeys(); + this._disableExtraShortcut(); + this._signalsHandler.destroy(); + } + + _enableHotKeys() { + if (this._hotKeysEnabled) + return; + + // Setup keyboard bindings for dash elements + let keys = ['app-hotkey-', 'app-shift-hotkey-', 'app-ctrl-hotkey-']; + keys.forEach( function(key) { + for (let i = 0; i < DashToDock_KeyboardShortcuts_NUM_HOTKEYS; i++) { + let appNum = i; + Main.wm.addKeybinding(key + (i + 1), DockManager.settings, + Meta.KeyBindingFlags.IGNORE_AUTOREPEAT, + Shell.ActionMode.NORMAL | Shell.ActionMode.OVERVIEW, + () => { + DockManager.getDefault().mainDock._activateApp(appNum); + this._showOverlay(); + }); + } + }, this); + + this._hotKeysEnabled = true; + } + + _disableHotKeys() { + if (!this._hotKeysEnabled) + return; + + let keys = ['app-hotkey-', 'app-shift-hotkey-', 'app-ctrl-hotkey-']; + keys.forEach( function(key) { + for (let i = 0; i < DashToDock_KeyboardShortcuts_NUM_HOTKEYS; i++) + Main.wm.removeKeybinding(key + (i + 1)); + }, this); + + this._hotKeysEnabled = false; + } + + _optionalNumberOverlay() { + let settings = DockManager.settings; + this._shortcutIsSet = false; + // Enable extra shortcut if either 'overlay' or 'show-dock' are true + if (settings.get_boolean('hot-keys') && + (settings.get_boolean('hotkeys-overlay') || settings.get_boolean('hotkeys-show-dock'))) + this._enableExtraShortcut(); + + this._signalsHandler.add([ + settings, + 'changed::hot-keys', + this._checkHotkeysOptions.bind(this) + ], [ + settings, + 'changed::hotkeys-overlay', + this._checkHotkeysOptions.bind(this) + ], [ + settings, + 'changed::hotkeys-show-dock', + this._checkHotkeysOptions.bind(this) + ]); + } + + _checkHotkeysOptions() { + let settings = DockManager.settings; + + if (settings.get_boolean('hot-keys') && + (settings.get_boolean('hotkeys-overlay') || settings.get_boolean('hotkeys-show-dock'))) + this._enableExtraShortcut(); + else + this._disableExtraShortcut(); + } + + _enableExtraShortcut() { + if (!this._shortcutIsSet) { + Main.wm.addKeybinding('shortcut', DockManager.settings, + Meta.KeyBindingFlags.IGNORE_AUTOREPEAT, + Shell.ActionMode.NORMAL | Shell.ActionMode.OVERVIEW, + this._showOverlay.bind(this)); + this._shortcutIsSet = true; + } + } + + _disableExtraShortcut() { + if (this._shortcutIsSet) { + Main.wm.removeKeybinding('shortcut'); + this._shortcutIsSet = false; + } + } + + _showOverlay() { + for (let dock of DockManager.allDocks) { + if (DockManager.settings.get_boolean('hotkeys-overlay')) + dock.dash.toggleNumberOverlay(true); + + // Restart the counting if the shortcut is pressed again + if (dock._numberOverlayTimeoutId) { + GLib.source_remove(dock._numberOverlayTimeoutId); + dock._numberOverlayTimeoutId = 0; + } + + // Hide the overlay/dock after the timeout + let timeout = DockManager.settings.get_double('shortcut-timeout') * 1000; + dock._numberOverlayTimeoutId = GLib.timeout_add( + GLib.PRIORITY_DEFAULT, timeout, () => { + dock._numberOverlayTimeoutId = 0; + dock.dash.toggleNumberOverlay(false); + // Hide the dock again if necessary + dock._updateDashVisibility(); + }); + + // Show the dock if it is hidden + if (DockManager.settings.get_boolean('hotkeys-show-dock')) { + let showDock = (dock._intellihideIsEnabled || dock._autohideIsEnabled); + if (showDock) + dock._show(); + } + } + } +}; + +/** + * Isolate overview to open new windows for inactive apps + * Note: the future implementaion is not fully contained here. Some bits are around in other methods of other classes. + * This class just take care of enabling/disabling the option. + */ +var WorkspaceIsolation = class DashToDock_WorkspaceIsolation { + + constructor() { + + let settings = DockManager.settings; + + this._signalsHandler = new Utils.GlobalSignalsHandler(); + this._injectionsHandler = new Utils.InjectionsHandler(); + + this._signalsHandler.add([ + settings, + 'changed::isolate-workspaces', + () => { + DockManager.allDocks.forEach((dock) => + dock.dash.resetAppIcons()); + if (settings.get_boolean('isolate-workspaces') || + settings.get_boolean('isolate-monitors')) + this._enable.bind(this)(); + else + this._disable.bind(this)(); + } + ],[ + settings, + 'changed::isolate-monitors', + () => { + DockManager.allDocks.forEach((dock) => + dock.dash.resetAppIcons()); + if (settings.get_boolean('isolate-workspaces') || + settings.get_boolean('isolate-monitors')) + this._enable.bind(this)(); + else + this._disable.bind(this)(); + } + ]); + + if (settings.get_boolean('isolate-workspaces') || + settings.get_boolean('isolate-monitors')) + this._enable(); + + } + + _enable() { + + // ensure I never double-register/inject + // although it should never happen + this._disable(); + + DockManager.allDocks.forEach((dock) => { + this._signalsHandler.addWithLabel('isolation', [ + global.display, + 'restacked', + dock.dash._queueRedisplay.bind(dock.dash) + ], [ + global.window_manager, + 'switch-workspace', + dock.dash._queueRedisplay.bind(dock.dash) + ]); + + // This last signal is only needed for monitor isolation, as windows + // might migrate from one monitor to another without triggering 'restacked' + if (DockManager.settings.get_boolean('isolate-monitors')) + this._signalsHandler.addWithLabel('isolation', [ + global.display, + 'window-entered-monitor', + dock.dash._queueRedisplay.bind(dock.dash) + ]); + + }, this); + + // here this is the Shell.App + function IsolatedOverview() { + // These lines take care of Nautilus for icons on Desktop + let windows = this.get_windows().filter(function(w) { + return w.get_workspace().index() == global.workspace_manager.get_active_workspace_index(); + }); + if (windows.length == 1) + if (windows[0].skip_taskbar) + return this.open_new_window(-1); + + if (this.is_on_workspace(global.workspace_manager.get_active_workspace())) + return Main.activateWindow(windows[0]); + return this.open_new_window(-1); + } + + this._injectionsHandler.addWithLabel('isolation', [ + Shell.App.prototype, + 'activate', + IsolatedOverview + ]); + } + + _disable () { + this._signalsHandler.removeWithLabel('isolation'); + this._injectionsHandler.removeWithLabel('isolation'); + } + + destroy() { + this._signalsHandler.destroy(); + this._injectionsHandler.destroy(); + } +}; + + +var DockManager = class DashToDock_DockManager { + + constructor() { + if (Me.imports.extension.dockManager) + throw new Error('DashToDock has been already initialized'); + + Me.imports.extension.dockManager = this; + + this._remoteModel = new LauncherAPI.LauncherEntryRemoteModel(); + this._signalsHandler = new Utils.GlobalSignalsHandler(); + this._settings = ExtensionUtils.getSettings('org.gnome.shell.extensions.dash-to-dock'); + this._oldDash = Main.overview.isDummy ? null : Main.overview.dash; + + this._ensureFileManagerClient(); + + /* Array of all the docks created */ + this._allDocks = []; + this._createDocks(); + + // status variable: true when the overview is shown through the dash + // applications button. + this._forcedOverview = false; + + // Connect relevant signals to the toggling function + this._bindSettingsChanges(); + } + + static getDefault() { + return Me.imports.extension.dockManager + } + + static get allDocks() { + return DockManager.getDefault()._allDocks; + } + + static get settings() { + return DockManager.getDefault()._settings; + } + + get fm1Client() { + return this._fm1Client; + } + + get mainDock() { + return this._allDocks.length ? this._allDocks[0] : null; + } + + _ensureFileManagerClient() { + let supportsLocations = ['show-trash', 'show-mounts'].some((s) => { + return this._settings.get_boolean(s); + }); + + if (supportsLocations) { + if (!this._fm1Client) + this._fm1Client = new FileManager1API.FileManager1Client(); + } else if (this._fm1Client) { + this._fm1Client.destroy(); + this._fm1Client = null; + } + } + + _toggle() { + if (this._toggleLater) + Meta.later_remove(this._toggleLater); + + this._toggleLater = Meta.later_add(Meta.LaterType.BEFORE_REDRAW, () => { + delete this._toggleLater; + this._restoreDash(); + this._deleteDocks(); + this._createDocks(); + this.emit('toggled'); + }); + } + + _bindSettingsChanges() { + // Connect relevant signals to the toggling function + this._signalsHandler.add([ + Meta.MonitorManager.get(), + 'monitors-changed', + this._toggle.bind(this) + ], [ + Main.sessionMode, + 'updated', + this._toggle.bind(this) + ], [ + this._settings, + 'changed::multi-monitor', + this._toggle.bind(this) + ], [ + this._settings, + 'changed::preferred-monitor', + this._toggle.bind(this) + ], [ + this._settings, + 'changed::dock-position', + this._toggle.bind(this) + ], [ + this._settings, + 'changed::extend-height', + this._adjustPanelCorners.bind(this) + ], [ + this._settings, + 'changed::dock-fixed', + this._adjustPanelCorners.bind(this) + ], [ + this._settings, + 'changed::show-trash', + () => this._ensureFileManagerClient() + ], [ + this._settings, + 'changed::show-mounts', + () => this._ensureFileManagerClient() + ]); + } + + _createDocks() { + + // If there are no monitors (headless configurations, but it can also happen temporary while disconnecting + // and reconnecting monitors), just do nothing. When a monitor will be connected we we'll be notified and + // and thus create the docks. This prevents pointing trying to access monitors throughout the code, were we + // are assuming that at least the primary monitor is present. + if (Main.layoutManager.monitors.length <= 0) { + return; + } + + this._preferredMonitorIndex = this._settings.get_int('preferred-monitor'); + // In case of multi-monitor, we consider the dock on the primary monitor to be the preferred (main) one + // regardless of the settings + // The dock goes on the primary monitor also if the settings are incosistent (e.g. desired monitor not connected). + if (this._settings.get_boolean('multi-monitor') || + this._preferredMonitorIndex < 0 || this._preferredMonitorIndex > Main.layoutManager.monitors.length - 1 + ) { + this._preferredMonitorIndex = Main.layoutManager.primaryIndex; + } else { + // Gdk and shell monitors numbering differ at least under wayland: + // While the primary monitor appears to be always index 0 in Gdk, + // the shell can assign a different number (Main.layoutManager.primaryMonitor) + // This ensure the indexing in the settings (Gdk) and in the shell are matched, + // i.e. that we start counting from the primaryMonitorIndex + this._preferredMonitorIndex = (Main.layoutManager.primaryIndex + this._preferredMonitorIndex) % Main.layoutManager.monitors.length ; + } + + // First we create the main Dock, to get the extra features to bind to this one + let dock = new DockedDash(this._remoteModel, this._preferredMonitorIndex); + this._allDocks.push(dock); + + // connect app icon into the view selector + dock.dash.showAppsButton.connect('notify::checked', this._onShowAppsButtonToggled.bind(this)); + + // Make the necessary changes to Main.overview.dash + this._prepareMainDash(); + + // Adjust corners if necessary + this._adjustPanelCorners(); + + if (this._settings.get_boolean('multi-monitor')) { + let nMon = Main.layoutManager.monitors.length; + for (let iMon = 0; iMon < nMon; iMon++) { + if (iMon == this._preferredMonitorIndex) + continue; + let dock = new DockedDash(this._remoteModel, iMon); + this._allDocks.push(dock); + // connect app icon into the view selector + dock.dash.showAppsButton.connect('notify::checked', this._onShowAppsButtonToggled.bind(this)); + } + } + + // Load optional features. We load *after* the docks are created, since + // we need to connect the signals to all dock instances. + this._workspaceIsolation = new WorkspaceIsolation(); + this._keyboardShortcuts = new KeyboardShortcuts(); + } + + _prepareMainDash() { + // Ensure Main.overview.dash is set to our dash in dummy mode + // while just use the default getter otherwise. + // The getter must be dynamic and not set only when we've a dummy + // overview because the mode can change dynamically. + let defaultDashGetter = Object.getOwnPropertyDescriptor( + Main.overview.constructor.prototype, 'dash').get; + Object.defineProperty(Main.overview, 'dash', { + configurable: true, + get: () => Main.overview.isDummy ? + this.mainDock.dash : defaultDashGetter.call(Main.overview), + }); + + if (Main.overview.isDummy) + return; + + this._signalsHandler.removeWithLabel('old-dash-changes'); + + // Hide usual Dash + this._oldDash.hide(); + + // Also set dash width to 1, so it's almost not taken into account by code + // calculaing the reserved space in the overview. The reason to keep it at 1 is + // to allow its visibility change to trigger an allocaion of the appGrid which + // in turn is triggergin the appsIcon spring animation, required when no other + // actors has this effect, i.e in horizontal mode and without the workspaceThumnails + // 1 static workspace only) + this._oldDash.set_height(1); + + this._signalsHandler.addWithLabel('old-dash-changes', [ + this._oldDash, + 'notify::visible', + () => this._prepareMainDash() + ], [ + this._oldDash, + 'notify::width', + () => this._prepareMainDash() + ]); + + // Pretend I'm the dash: meant to make appgrid swarm animation come from + // the right position of the appShowButton. + let overviewControls = Main.overview._overview._controls; + overviewControls.dash = this.mainDock.dash; + overviewControls._searchController._showAppsButton = this.mainDock.dash.showAppsButton; + + if (this.mainDock.dash._isHorizontal) { + overviewControls._dashSpacer = this.mainDock._dashSpacer; + Main.overview._overview._controls.add_child(this.mainDock._dashSpacer); + Main.overview._overview._controls.layout_manager._dash = this.mainDock._dashSpacer; + } + } + + _deleteDocks() { + if (!this._allDocks.length) + return; + + // Remove extra features + this._workspaceIsolation.destroy(); + this._keyboardShortcuts.destroy(); + + // Delete all docks + this._allDocks.forEach(d => d.destroy()); + this._allDocks = []; + } + + _restoreDash() { + Object.defineProperty(Main.overview, 'dash', + Object.getOwnPropertyDescriptor( + Main.overview.constructor.prototype, 'dash')); + + if (!this._oldDash) + return; + + this._signalsHandler.removeWithLabel('old-dash-changes'); + + let overviewControls = Main.overview._overview._controls; + Main.overview._overview._controls.layout_manager._dash = this._oldDash; + if (this.mainDock._dashSpacer) { + Main.overview._overview._controls.remove_child(this.mainDock._dashSpacer); + } + + overviewControls.dash = this._oldDash; + overviewControls._searchController._showAppsButton = this._oldDash.showAppsButton; + Main.overview.dash.show(); + Main.overview.dash.set_height(-1); // reset default dash size + // This force the recalculation of the icon size + Main.overview.dash._maxHeight = -1; + } + + get searchController() { + return Main.overview._overview.controls._searchController; + } + + _onShowAppsButtonToggled(button) { + const checked = button.checked; + const overviewControls = Main.overview._overview.controls; + + if (!Main.overview.visible) { + this.mainDock.dash.showAppsButton._fromDesktop = true; + Main.overview.show(OverviewControls.ControlsState.APP_GRID); + } else { + if (!checked && this.mainDock.dash.showAppsButton._fromDesktop) { + Main.overview.hide(); + this.mainDock.dash.showAppsButton._fromDesktop = false; + } else { + // TODO: I'm not sure how reliable this is, we might need to move the + // _onShowAppsButtonToggled logic into the extension. + if (!checked) { + this.mainDock.dash.showAppsButton._fromDesktop = false; + } + + // Instead of "syncing" the stock button, let's call its callback directly. + overviewControls._onShowAppsButtonToggled.call(overviewControls); + } + } + + // Because we "disconnected" from the search controller, we have to manage its state. + this.searchController._setSearchActive(false); + } + + destroy() { + this._signalsHandler.destroy(); + if (this._toggleLater) { + Meta.later_remove(this._toggleLater); + delete this._toggleLater; + } + this._restoreDash(); + this._deleteDocks(); + this._revertPanelCorners(); + if (this._oldSelectorMargin) + Main.overview._overview.controls._searchController.margin_bottom = this._oldSelectorMargin; + if (this._fm1Client) { + this._fm1Client.destroy(); + this._fm1Client = null; + } + this._remoteModel.destroy(); + this._settings.run_dispose(); + this._settings = null; + this._oldDash = null; + + Me.imports.extension.dockManager = null; + } + + /** + * Adjust Panel corners + */ + _adjustPanelCorners() { + let position = Utils.getPosition(); + let isHorizontal = ((position == St.Side.TOP) || (position == St.Side.BOTTOM)); + let extendHeight = this._settings.get_boolean('extend-height'); + let fixedIsEnabled = this._settings.get_boolean('dock-fixed'); + let dockOnPrimary = this._settings.get_boolean('multi-monitor') || + this._preferredMonitorIndex == Main.layoutManager.primaryIndex; + + if (!isHorizontal && dockOnPrimary && extendHeight && fixedIsEnabled) { + Main.panel._rightCorner.hide(); + Main.panel._leftCorner.hide(); + } + else + this._revertPanelCorners(); + } + + _revertPanelCorners() { + Main.panel._leftCorner.show(); + Main.panel._rightCorner.show(); + } +}; +Signals.addSignalMethods(DockManager.prototype); + +// This class drives long-running icon animations, to keep them running in sync +// with each other, and to save CPU by pausing them when the dock is hidden. +var IconAnimator = class DashToDock_IconAnimator { + constructor(actor) { + this._count = 0; + this._started = false; + this._animations = { + dance: [], + }; + this._timeline = new Clutter.Timeline({ + duration: 3000, + repeat_count: -1, + actor + }); + + this._timeline.connect('new-frame', () => { + const progress = this._timeline.get_progress(); + const danceRotation = progress < 1/6 ? 15*Math.sin(progress*24*Math.PI) : 0; + const dancers = this._animations.dance; + for (let i = 0, iMax = dancers.length; i < iMax; i++) { + dancers[i].target.rotation_angle_z = danceRotation; + } + }); + } + + destroy() { + this._timeline.stop(); + this._timeline = null; + for (const name in this._animations) { + const pairs = this._animations[name]; + for (let i = 0, iMax = pairs.length; i < iMax; i++) { + const pair = pairs[i]; + pair.target.disconnect(pair.targetDestroyId); + } + } + this._animations = null; + } + + pause() { + if (this._started && this._count > 0) { + this._timeline.stop(); + } + this._started = false; + } + + start() { + if (!this._started && this._count > 0) { + this._timeline.start(); + } + this._started = true; + } + + addAnimation(target, name) { + const targetDestroyId = target.connect('destroy', () => this.removeAnimation(target, name)); + this._animations[name].push({ target, targetDestroyId }); + if (this._started && this._count === 0) { + this._timeline.start(); + } + this._count++; + } + + removeAnimation(target, name) { + const pairs = this._animations[name]; + for (let i = 0, iMax = pairs.length; i < iMax; i++) { + const pair = pairs[i]; + if (pair.target === target) { + target.disconnect(pair.targetDestroyId); + pairs.splice(i, 1); + this._count--; + if (this._started && this._count === 0) { + this._timeline.stop(); + } + return; + } + } + } +}; diff --git a/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/extension.js b/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/extension.js new file mode 100644 index 0000000..0b7b5c6 --- /dev/null +++ b/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/extension.js @@ -0,0 +1,21 @@ +// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- + +const ExtensionUtils = imports.misc.extensionUtils; +const Me = ExtensionUtils.getCurrentExtension(); +const Docking = Me.imports.docking; + +// We declare this with var so it can be accessed by other extensions in +// GNOME Shell 3.26+ (mozjs52+). +var dockManager; + +function init() { + ExtensionUtils.initTranslations('dashtodock'); +} + +function enable() { + new Docking.DockManager(); +} + +function disable() { + dockManager.destroy(); +} diff --git a/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/fileManager1API.js b/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/fileManager1API.js new file mode 100644 index 0000000..64ec621 --- /dev/null +++ b/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/fileManager1API.js @@ -0,0 +1,226 @@ +// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- + +const Gio = imports.gi.Gio; +const Signals = imports.signals; + +const Me = imports.misc.extensionUtils.getCurrentExtension(); +const Utils = Me.imports.utils; + +const FileManager1Iface = '\ + \ + \ + '; + +const FileManager1Proxy = Gio.DBusProxy.makeProxyWrapper(FileManager1Iface); + +/** + * This class implements a client for the org.freedesktop.FileManager1 dbus + * interface, and specifically for the OpenWindowsWithLocations property + * which is published by Nautilus, but is not an official part of the interface. + * + * The property is a map from window identifiers to a list of locations open in + * the window. + * + * While OpeWindowsWithLocations is part of upstream Nautilus, for many years + * prior, Ubuntu patched Nautilus to publish XUbuntuOpenLocationsXids, which is + * similar but uses Xids as the window identifiers instead of gtk window paths. + * + * When an old or unpatched Nautilus is running, we will observe the properties + * to always be empty arrays, but there will not be any correctness issues. + */ +var FileManager1Client = class DashToDock_FileManager1Client { + + constructor() { + this._signalsHandler = new Utils.GlobalSignalsHandler(); + this._cancellable = new Gio.Cancellable(); + + this._locationMap = new Map(); + this._proxy = new FileManager1Proxy(Gio.DBus.session, + "org.freedesktop.FileManager1", + "/org/freedesktop/FileManager1", + (initable, error) => { + // Use async construction to avoid blocking on errors. + if (error) { + if (!error.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) + global.log(error); + } else { + this._updateLocationMap(); + } + }, this._cancellable); + + this._signalsHandler.add([ + this._proxy, + 'g-properties-changed', + this._onPropertyChanged.bind(this) + ], [ + // We must additionally listen for Screen events to know when to + // rebuild our location map when the set of available windows changes. + global.workspace_manager, + 'workspace-switched', + this._updateLocationMap.bind(this) + ], [ + global.display, + 'window-entered-monitor', + this._updateLocationMap.bind(this) + ], [ + global.display, + 'window-left-monitor', + this._updateLocationMap.bind(this) + ]); + } + + destroy() { + this._cancellable.cancel(); + this._signalsHandler.destroy(); + this._proxy.run_dispose(); + } + + /** + * Return an array of windows that are showing a location or + * sub-directories of that location. + */ + getWindows(location) { + let ret = new Set(); + let locationEsc = location; + + if (!location.endsWith('/')) { + locationEsc += '/'; + } + + for (let [k,v] of this._locationMap) { + if ((k + '/').startsWith(locationEsc)) { + for (let l of v) { + ret.add(l); + } + } + } + return Array.from(ret); + } + + _onPropertyChanged(proxy, changed, invalidated) { + let property = changed.unpack(); + if (property && + ('XUbuntuOpenLocationsXids' in property || + 'OpenWindowsWithLocations' in property)) { + this._updateLocationMap(); + } + } + + _updateLocationMap() { + let properties = this._proxy.get_cached_property_names(); + if (properties == null) { + // Nothing to check yet. + return; + } + + if (properties.includes('OpenWindowsWithLocations')) { + this._updateFromPaths(); + } else if (properties.includes('XUbuntuOpenLocationsXids')) { + this._updateFromXids(); + } + } + + _updateFromPaths() { + let pathToLocations = this._proxy.OpenWindowsWithLocations; + let pathToWindow = getPathToWindow(); + + let locationToWindow = new Map(); + for (let path in pathToLocations) { + let locations = pathToLocations[path]; + for (let i = 0; i < locations.length; i++) { + let l = locations[i]; + // Use a set to deduplicate when a window has a + // location open in multiple tabs. + if (!locationToWindow.has(l)) { + locationToWindow.set(l, new Set()); + } + let window = pathToWindow.get(path); + if (window != null) { + locationToWindow.get(l).add(window); + } + } + } + this._locationMap = locationToWindow; + this.emit('windows-changed'); + } + + _updateFromXids() { + let xidToLocations = this._proxy.XUbuntuOpenLocationsXids; + let xidToWindow = getXidToWindow(); + + let locationToWindow = new Map(); + for (let xid in xidToLocations) { + let locations = xidToLocations[xid]; + for (let i = 0; i < locations.length; i++) { + let l = locations[i]; + // Use a set to deduplicate when a window has a + // location open in multiple tabs. + if (!locationToWindow.has(l)) { + locationToWindow.set(l, new Set()); + } + let window = xidToWindow.get(parseInt(xid)); + if (window != null) { + locationToWindow.get(l).add(window); + } + } + } + this._locationMap = locationToWindow; + this.emit('windows-changed'); + } +} +Signals.addSignalMethods(FileManager1Client.prototype); + +/** + * Construct a map of gtk application window object paths to MetaWindows. + */ +function getPathToWindow() { + let pathToWindow = new Map(); + + for (let i = 0; i < global.workspace_manager.n_workspaces; i++) { + let ws = global.workspace_manager.get_workspace_by_index(i); + ws.list_windows().map(function(w) { + let path = w.get_gtk_window_object_path(); + if (path != null) { + pathToWindow.set(path, w); + } + }); + } + return pathToWindow; +} + +/** + * Construct a map of XIDs to MetaWindows. + * + * This is somewhat annoying as you cannot lookup a window by + * XID in any way, and must iterate through all of them looking + * for a match. + */ +function getXidToWindow() { + let xidToWindow = new Map(); + + for (let i = 0; i < global.workspace_manager.n_workspaces; i++) { + let ws = global.workspace_manager.get_workspace_by_index(i); + ws.list_windows().map(function(w) { + let xid = guessWindowXID(w); + if (xid != null) { + xidToWindow.set(parseInt(xid), w); + } + }); + } + return xidToWindow; +} + +/** + * Guesses the X ID of a window. + * + * This is the basic implementation that is sufficient for Nautilus + * windows. The pixel-saver extension has a much more complex + * implementation if we ever need it. + */ +function guessWindowXID(win) { + try { + return win.get_description().match(/0x[0-9a-f]+/)[0]; + } catch (err) { + return null; + } +} diff --git a/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/intellihide.js b/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/intellihide.js new file mode 100644 index 0000000..9c10938 --- /dev/null +++ b/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/intellihide.js @@ -0,0 +1,321 @@ +// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- + +const GLib = imports.gi.GLib; +const Meta = imports.gi.Meta; +const Shell = imports.gi.Shell; + +const Main = imports.ui.main; +const Signals = imports.signals; + +const Me = imports.misc.extensionUtils.getCurrentExtension(); +const Docking = Me.imports.docking; +const Utils = Me.imports.utils; + +// A good compromise between reactivity and efficiency; to be tuned. +const INTELLIHIDE_CHECK_INTERVAL = 100; + +const OverlapStatus = { + UNDEFINED: -1, + FALSE: 0, + TRUE: 1 +}; + +const IntellihideMode = { + ALL_WINDOWS: 0, + FOCUS_APPLICATION_WINDOWS: 1, + MAXIMIZED_WINDOWS : 2 +}; + +// List of windows type taken into account. Order is important (keep the original +// enum order). +const handledWindowTypes = [ + Meta.WindowType.NORMAL, + Meta.WindowType.DOCK, + Meta.WindowType.DIALOG, + Meta.WindowType.MODAL_DIALOG, + Meta.WindowType.TOOLBAR, + Meta.WindowType.MENU, + Meta.WindowType.UTILITY, + Meta.WindowType.SPLASHSCREEN +]; + +/** + * A rough and ugly implementation of the intellihide behaviour. + * Intallihide object: emit 'status-changed' signal when the overlap of windows + * with the provided targetBoxClutter.ActorBox changes; + */ +var Intellihide = class DashToDock_Intellihide { + + constructor(monitorIndex) { + // Load settings + this._monitorIndex = monitorIndex; + + this._signalsHandler = new Utils.GlobalSignalsHandler(); + this._tracker = Shell.WindowTracker.get_default(); + this._focusApp = null; // The application whose window is focused. + this._topApp = null; // The application whose window is on top on the monitor with the dock. + + this._isEnabled = false; + this.status = OverlapStatus.UNDEFINED; + this._targetBox = null; + + this._checkOverlapTimeoutContinue = false; + this._checkOverlapTimeoutId = 0; + + this._trackedWindows = new Map(); + + // Connect global signals + this._signalsHandler.add([ + // Add signals on windows created from now on + global.display, + 'window-created', + this._windowCreated.bind(this) + ], [ + // triggered for instance when the window list order changes, + // included when the workspace is switched + global.display, + 'restacked', + this._checkOverlap.bind(this) + ], [ + // when windows are alwasy on top, the focus window can change + // without the windows being restacked. Thus monitor window focus change. + this._tracker, + 'notify::focus-app', + this._checkOverlap.bind(this) + ], [ + // update wne monitor changes, for instance in multimonitor when monitor are attached + Meta.MonitorManager.get(), + 'monitors-changed', + this._checkOverlap.bind(this) + ]); + } + + destroy() { + // Disconnect global signals + this._signalsHandler.destroy(); + + // Remove residual windows signals + this.disable(); + } + + enable() { + this._isEnabled = true; + this._status = OverlapStatus.UNDEFINED; + global.get_window_actors().forEach(function(wa) { + this._addWindowSignals(wa); + }, this); + this._doCheckOverlap(); + } + + disable() { + this._isEnabled = false; + + for (let wa of this._trackedWindows.keys()) { + this._removeWindowSignals(wa); + } + this._trackedWindows.clear(); + + if (this._checkOverlapTimeoutId > 0) { + GLib.source_remove(this._checkOverlapTimeoutId); + this._checkOverlapTimeoutId = 0; + } + } + + _windowCreated(display, metaWindow) { + this._addWindowSignals(metaWindow.get_compositor_private()); + } + + _addWindowSignals(wa) { + if (!this._handledWindow(wa)) + return; + let signalId = wa.connect('notify::allocation', this._checkOverlap.bind(this)); + this._trackedWindows.set(wa, signalId); + wa.connect('destroy', this._removeWindowSignals.bind(this)); + } + + _removeWindowSignals(wa) { + if (this._trackedWindows.get(wa)) { + wa.disconnect(this._trackedWindows.get(wa)); + this._trackedWindows.delete(wa); + } + + } + + updateTargetBox(box) { + this._targetBox = box; + this._checkOverlap(); + } + + forceUpdate() { + this._status = OverlapStatus.UNDEFINED; + this._doCheckOverlap(); + } + + getOverlapStatus() { + return (this._status == OverlapStatus.TRUE); + } + + _checkOverlap() { + if (!this._isEnabled || (this._targetBox == null)) + return; + + /* Limit the number of calls to the doCheckOverlap function */ + if (this._checkOverlapTimeoutId) { + this._checkOverlapTimeoutContinue = true; + return + } + + this._doCheckOverlap(); + + this._checkOverlapTimeoutId = GLib.timeout_add( + GLib.PRIORITY_DEFAULT, INTELLIHIDE_CHECK_INTERVAL, () => { + this._doCheckOverlap(); + if (this._checkOverlapTimeoutContinue) { + this._checkOverlapTimeoutContinue = false; + return GLib.SOURCE_CONTINUE; + } else { + this._checkOverlapTimeoutId = 0; + return GLib.SOURCE_REMOVE; + } + }); + } + + _doCheckOverlap() { + + if (!this._isEnabled || (this._targetBox == null)) + return; + + let overlaps = OverlapStatus.FALSE; + let windows = global.get_window_actors(); + + if (windows.length > 0) { + /* + * Get the top window on the monitor where the dock is placed. + * The idea is that we dont want to overlap with the windows of the topmost application, + * event is it's not the focused app -- for instance because in multimonitor the user + * select a window in the secondary monitor. + */ + + let topWindow = null; + for (let i = windows.length - 1; i >= 0; i--) { + let meta_win = windows[i].get_meta_window(); + if (this._handledWindow(windows[i]) && (meta_win.get_monitor() == this._monitorIndex)) { + topWindow = meta_win; + break; + } + } + + if (topWindow !== null) { + this._topApp = this._tracker.get_window_app(topWindow); + // If there isn't a focused app, use that of the window on top + this._focusApp = this._tracker.focus_app || this._topApp + + windows = windows.filter(this._intellihideFilterInteresting, this); + + for (let i = 0; i < windows.length; i++) { + let win = windows[i].get_meta_window(); + + if (win) { + let rect = win.get_frame_rect(); + + let test = (rect.x < this._targetBox.x2) && + (rect.x + rect.width > this._targetBox.x1) && + (rect.y < this._targetBox.y2) && + (rect.y + rect.height > this._targetBox.y1); + + if (test) { + overlaps = OverlapStatus.TRUE; + break; + } + } + } + } + } + + if (this._status !== overlaps) { + this._status = overlaps; + this.emit('status-changed', this._status); + } + + } + + // Filter interesting windows to be considered for intellihide. + // Consider all windows visible on the current workspace. + // Optionally skip windows of other applications + _intellihideFilterInteresting(wa) { + let meta_win = wa.get_meta_window(); + if (!this._handledWindow(wa)) + return false; + + let currentWorkspace = global.workspace_manager.get_active_workspace_index(); + let wksp = meta_win.get_workspace(); + let wksp_index = wksp.index(); + + // Depending on the intellihide mode, exclude non-relevent windows + switch (Docking.DockManager.settings.get_enum('intellihide-mode')) { + case IntellihideMode.ALL_WINDOWS: + // Do nothing + break; + + case IntellihideMode.FOCUS_APPLICATION_WINDOWS: + // Skip windows of other apps + if (this._focusApp) { + // The DropDownTerminal extension is not an application per se + // so we match its window by wm class instead + if (meta_win.get_wm_class() == 'DropDownTerminalWindow') + return true; + + let currentApp = this._tracker.get_window_app(meta_win); + let focusWindow = global.display.get_focus_window() + + // Consider half maximized windows side by side + // and windows which are alwayson top + if((currentApp != this._focusApp) && (currentApp != this._topApp) + && !((focusWindow && focusWindow.maximized_vertically && !focusWindow.maximized_horizontally) + && (meta_win.maximized_vertically && !meta_win.maximized_horizontally) + && meta_win.get_monitor() == focusWindow.get_monitor()) + && !meta_win.is_above()) + return false; + } + break; + + case IntellihideMode.MAXIMIZED_WINDOWS: + // Skip unmaximized windows + if (!meta_win.maximized_vertically && !meta_win.maximized_horizontally) + return false; + break; + } + + if ( wksp_index == currentWorkspace && meta_win.showing_on_its_workspace() ) + return true; + else + return false; + + } + + // Filter windows by type + // inspired by Opacify@gnome-shell.localdomain.pl + _handledWindow(wa) { + let metaWindow = wa.get_meta_window(); + + if (!metaWindow) + return false; + + // The DropDownTerminal extension uses the POPUP_MENU window type hint + // so we match its window by wm class instead + if (metaWindow.get_wm_class() == 'DropDownTerminalWindow') + return true; + + let wtype = metaWindow.get_window_type(); + for (let i = 0; i < handledWindowTypes.length; i++) { + var hwtype = handledWindowTypes[i]; + if (hwtype == wtype) + return true; + else if (hwtype > wtype) + return false; + } + return false; + } +}; + +Signals.addSignalMethods(Intellihide.prototype); diff --git a/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/launcherAPI.js b/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/launcherAPI.js new file mode 100644 index 0000000..55f44f7 --- /dev/null +++ b/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/launcherAPI.js @@ -0,0 +1,281 @@ +// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- + +const Gio = imports.gi.Gio; + +const Me = imports.misc.extensionUtils.getCurrentExtension(); +const DbusmenuUtils = Me.imports.dbusmenuUtils; + +const Dbusmenu = DbusmenuUtils.haveDBusMenu(); + +var LauncherEntryRemoteModel = class DashToDock_LauncherEntryRemoteModel { + + constructor() { + this._entrySourceStacks = new Map(); + this._remoteMaps = new Map(); + + this._launcher_entry_dbus_signal_id = + Gio.DBus.session.signal_subscribe(null, // sender + 'com.canonical.Unity.LauncherEntry', // iface + 'Update', // member + null, // path + null, // arg0 + Gio.DBusSignalFlags.NONE, + (connection, sender_name, object_path, interface_name, signal_name, parameters) => + this._onUpdate(sender_name, ...parameters.deep_unpack())); + + this._dbus_name_owner_changed_signal_id = + Gio.DBus.session.signal_subscribe('org.freedesktop.DBus', // sender + 'org.freedesktop.DBus', // interface + 'NameOwnerChanged', // member + '/org/freedesktop/DBus', // path + null, // arg0 + Gio.DBusSignalFlags.NONE, + (connection, sender_name, object_path, interface_name, signal_name, parameters) => + this._onDBusNameChange(...parameters.deep_unpack().slice(1))); + + this._acquireUnityDBus(); + } + + destroy() { + if (this._launcher_entry_dbus_signal_id) { + Gio.DBus.session.signal_unsubscribe(this._launcher_entry_dbus_signal_id); + } + + if (this._dbus_name_owner_changed_signal_id) { + Gio.DBus.session.signal_unsubscribe(this._dbus_name_owner_changed_signal_id); + } + + this._releaseUnityDBus(); + } + + _lookupStackById(appId) { + let sourceStack = this._entrySourceStacks.get(appId); + if (!sourceStack) { + this._entrySourceStacks.set(appId, sourceStack = new PropertySourceStack(new LauncherEntry(), launcherEntryDefaults)); + } + return sourceStack; + } + + lookupById(appId) { + return this._lookupStackById(appId).target; + } + + _acquireUnityDBus() { + if (!this._unity_bus_id) { + this._unity_bus_id = Gio.DBus.session.own_name('com.canonical.Unity', + Gio.BusNameOwnerFlags.ALLOW_REPLACEMENT | Gio.BusNameOwnerFlags.REPLACE, + null, () => this._unity_bus_id = 0); + } + } + + _releaseUnityDBus() { + if (this._unity_bus_id) { + Gio.DBus.session.unown_name(this._unity_bus_id); + this._unity_bus_id = 0; + } + } + + _onDBusNameChange(before, after) { + if (!before || !this._remoteMaps.size) { + return; + } + const remoteMap = this._remoteMaps.get(before); + if (!remoteMap) { + return; + } + this._remoteMaps.delete(before); + if (after && !this._remoteMaps.has(after)) { + this._remoteMaps.set(after, remoteMap); + } else { + for (const [appId, remote] of remoteMap) { + const sourceStack = this._entrySourceStacks.get(appId); + const changed = sourceStack.remove(remote); + if (changed) { + sourceStack.target._emitChangedEvents(changed); + } + } + } + } + + _onUpdate(senderName, appUri, properties) { + if (!senderName) { + return; + } + + const appId = appUri.replace(/(^\w+:|^)\/\//, ''); + if (!appId) { + return; + } + + let remoteMap = this._remoteMaps.get(senderName); + if (!remoteMap) { + this._remoteMaps.set(senderName, remoteMap = new Map()); + } + let remote = remoteMap.get(appId); + if (!remote) { + remoteMap.set(appId, remote = Object.assign({}, launcherEntryDefaults)); + } + for (const name in properties) { + if (name === 'quicklist' && Dbusmenu) { + const quicklistPath = properties[name].unpack(); + if (quicklistPath && (!remote._quicklistMenuClient || remote._quicklistMenuClient.dbus_object !== quicklistPath)) { + remote.quicklist = null; + let menuClient = remote._quicklistMenuClient; + if (menuClient) { + menuClient.dbus_object = quicklistPath; + } else { + // This property should not be enumerable + Object.defineProperty(remote, '_quicklistMenuClient', { + writable: true, + value: menuClient = new Dbusmenu.Client({ dbus_name: senderName, dbus_object: quicklistPath }), + }); + } + const handler = () => { + const root = menuClient.get_root(); + if (remote.quicklist !== root) { + remote.quicklist = root; + if (sourceStack.isTop(remote)) { + sourceStack.target.quicklist = root; + sourceStack.target._emitChangedEvents(['quicklist']); + } + } + }; + menuClient.connect(Dbusmenu.CLIENT_SIGNAL_ROOT_CHANGED, handler); + } + } else { + remote[name] = properties[name].unpack(); + } + } + + const sourceStack = this._lookupStackById(appId); + sourceStack.target._emitChangedEvents(sourceStack.update(remote)); + } +}; + +const launcherEntryDefaults = { + count: 0, + progress: 0, + urgent: false, + quicklist: null, + 'count-visible': false, + 'progress-visible': false, +}; + +const LauncherEntry = class DashToDock_LauncherEntry { + constructor() { + this._connections = new Map(); + this._handlers = new Map(); + this._nextId = 0; + } + + connect(eventNames, callback) { + if (typeof eventNames === 'string') { + eventNames = [eventNames]; + } + callback(this, this); + const id = this._nextId++; + const handler = { id, callback }; + eventNames.forEach(name => { + let handlerList = this._handlers.get(name); + if (!handlerList) { + this._handlers.set(name, handlerList = []); + } + handlerList.push(handler); + }); + this._connections.set(id, eventNames); + return id; + } + + disconnect(id) { + const eventNames = this._connections.get(id); + if (!eventNames) { + return; + } + this._connections.delete(id); + eventNames.forEach(name => { + const handlerList = this._handlers.get(name); + if (handlerList) { + for (let i = 0, iMax = handlerList.length; i < iMax; i++) { + if (handlerList[i].id === id) { + handlerList.splice(i, 1); + break; + } + } + } + }); + } + + _emitChangedEvents(propertyNames) { + const handlers = new Set(); + propertyNames.forEach(name => { + const handlerList = this._handlers.get(name + '-changed'); + if (handlerList) { + for (let i = 0, iMax = handlerList.length; i < iMax; i++) { + handlers.add(handlerList[i]); + } + } + }); + Array.from(handlers).sort((x, y) => x.id - y.id).forEach(handler => handler.callback(this, this)); + } +} + +for (const name in launcherEntryDefaults) { + const jsName = name.replace(/-/g, '_'); + LauncherEntry.prototype[jsName] = launcherEntryDefaults[name]; + if (jsName !== name) { + Object.defineProperty(LauncherEntry.prototype, name, { + get() { + return this[jsName]; + }, + set(value) { + this[jsName] = value; + }, + }); + } +} + +const PropertySourceStack = class DashToDock_PropertySourceStack { + constructor(target, bottom) { + this.target = target; + this._bottom = bottom; + this._stack = []; + } + + isTop(source) { + return this._stack.length > 0 && this._stack[this._stack.length - 1] === source; + } + + update(source) { + if (!this.isTop(source)) { + this.remove(source); + this._stack.push(source); + } + return this._assignFrom(source); + } + + remove(source) { + const stack = this._stack; + const top = stack[stack.length - 1]; + if (top === source) { + stack.length--; + return this._assignFrom(stack.length > 0 ? stack[stack.length - 1] : this._bottom); + } + for (let i = 0, iMax = stack.length; i < iMax; i++) { + if (stack[i] === source) { + stack.splice(i, 1); + break; + } + } + } + + _assignFrom(source) { + const changedProperties = []; + for (const name in source) { + if (this.target[name] !== source[name]) { + this.target[name] = source[name]; + changedProperties.push(name); + } + } + return changedProperties; + } +} diff --git a/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/locale/ar/LC_MESSAGES/dashtodock.mo b/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/locale/ar/LC_MESSAGES/dashtodock.mo new file mode 100644 index 0000000..78deaa5 Binary files /dev/null and b/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/locale/ar/LC_MESSAGES/dashtodock.mo differ diff --git a/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/locale/cs/LC_MESSAGES/dashtodock.mo b/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/locale/cs/LC_MESSAGES/dashtodock.mo new file mode 100644 index 0000000..1cc799d Binary files /dev/null and b/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/locale/cs/LC_MESSAGES/dashtodock.mo differ diff --git a/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/locale/de/LC_MESSAGES/dashtodock.mo b/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/locale/de/LC_MESSAGES/dashtodock.mo new file mode 100644 index 0000000..f7b2ed5 Binary files /dev/null and b/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/locale/de/LC_MESSAGES/dashtodock.mo differ diff --git a/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/locale/el/LC_MESSAGES/dashtodock.mo b/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/locale/el/LC_MESSAGES/dashtodock.mo new file mode 100644 index 0000000..b116119 Binary files /dev/null and b/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/locale/el/LC_MESSAGES/dashtodock.mo differ diff --git a/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/locale/es/LC_MESSAGES/dashtodock.mo b/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/locale/es/LC_MESSAGES/dashtodock.mo new file mode 100644 index 0000000..99888b0 Binary files /dev/null and b/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/locale/es/LC_MESSAGES/dashtodock.mo differ diff --git a/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/locale/eu/LC_MESSAGES/dashtodock.mo b/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/locale/eu/LC_MESSAGES/dashtodock.mo new file mode 100644 index 0000000..03fec78 Binary files /dev/null and b/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/locale/eu/LC_MESSAGES/dashtodock.mo differ diff --git a/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/locale/fr/LC_MESSAGES/dashtodock.mo b/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/locale/fr/LC_MESSAGES/dashtodock.mo new file mode 100644 index 0000000..a9a712c Binary files /dev/null and b/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/locale/fr/LC_MESSAGES/dashtodock.mo differ diff --git a/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/locale/gl/LC_MESSAGES/dashtodock.mo b/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/locale/gl/LC_MESSAGES/dashtodock.mo new file mode 100644 index 0000000..b89c102 Binary files /dev/null and b/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/locale/gl/LC_MESSAGES/dashtodock.mo differ diff --git a/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/locale/hu/LC_MESSAGES/dashtodock.mo b/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/locale/hu/LC_MESSAGES/dashtodock.mo new file mode 100644 index 0000000..6e2d85e Binary files /dev/null and b/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/locale/hu/LC_MESSAGES/dashtodock.mo differ diff --git a/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/locale/id/LC_MESSAGES/dashtodock.mo b/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/locale/id/LC_MESSAGES/dashtodock.mo new file mode 100644 index 0000000..950b3ff Binary files /dev/null and b/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/locale/id/LC_MESSAGES/dashtodock.mo differ diff --git a/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/locale/it/LC_MESSAGES/dashtodock.mo b/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/locale/it/LC_MESSAGES/dashtodock.mo new file mode 100644 index 0000000..fc1f722 Binary files /dev/null and b/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/locale/it/LC_MESSAGES/dashtodock.mo differ diff --git a/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/locale/ja/LC_MESSAGES/dashtodock.mo b/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/locale/ja/LC_MESSAGES/dashtodock.mo new file mode 100644 index 0000000..9b9b943 Binary files /dev/null and b/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/locale/ja/LC_MESSAGES/dashtodock.mo differ diff --git a/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/locale/nb/LC_MESSAGES/dashtodock.mo b/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/locale/nb/LC_MESSAGES/dashtodock.mo new file mode 100644 index 0000000..005884f Binary files /dev/null and b/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/locale/nb/LC_MESSAGES/dashtodock.mo differ diff --git a/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/locale/nl/LC_MESSAGES/dashtodock.mo b/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/locale/nl/LC_MESSAGES/dashtodock.mo new file mode 100644 index 0000000..91063e8 Binary files /dev/null and b/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/locale/nl/LC_MESSAGES/dashtodock.mo differ diff --git a/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/locale/pl/LC_MESSAGES/dashtodock.mo b/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/locale/pl/LC_MESSAGES/dashtodock.mo new file mode 100644 index 0000000..c9d87d9 Binary files /dev/null and b/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/locale/pl/LC_MESSAGES/dashtodock.mo differ diff --git a/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/locale/pt/LC_MESSAGES/dashtodock.mo b/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/locale/pt/LC_MESSAGES/dashtodock.mo new file mode 100644 index 0000000..b298694 Binary files /dev/null and b/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/locale/pt/LC_MESSAGES/dashtodock.mo differ diff --git a/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/locale/pt_BR/LC_MESSAGES/dashtodock.mo b/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/locale/pt_BR/LC_MESSAGES/dashtodock.mo new file mode 100644 index 0000000..cbe7c66 Binary files /dev/null and b/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/locale/pt_BR/LC_MESSAGES/dashtodock.mo differ diff --git a/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/locale/ru/LC_MESSAGES/dashtodock.mo b/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/locale/ru/LC_MESSAGES/dashtodock.mo new file mode 100644 index 0000000..0c187f3 Binary files /dev/null and b/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/locale/ru/LC_MESSAGES/dashtodock.mo differ diff --git a/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/locale/sk/LC_MESSAGES/dashtodock.mo b/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/locale/sk/LC_MESSAGES/dashtodock.mo new file mode 100644 index 0000000..5b25bec Binary files /dev/null and b/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/locale/sk/LC_MESSAGES/dashtodock.mo differ diff --git a/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/locale/sr/LC_MESSAGES/dashtodock.mo b/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/locale/sr/LC_MESSAGES/dashtodock.mo new file mode 100644 index 0000000..da3e086 Binary files /dev/null and b/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/locale/sr/LC_MESSAGES/dashtodock.mo differ diff --git a/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/locale/sr@latin/LC_MESSAGES/dashtodock.mo b/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/locale/sr@latin/LC_MESSAGES/dashtodock.mo new file mode 100644 index 0000000..1fcfb47 Binary files /dev/null and b/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/locale/sr@latin/LC_MESSAGES/dashtodock.mo differ diff --git a/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/locale/sv/LC_MESSAGES/dashtodock.mo b/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/locale/sv/LC_MESSAGES/dashtodock.mo new file mode 100644 index 0000000..bab036e Binary files /dev/null and b/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/locale/sv/LC_MESSAGES/dashtodock.mo differ diff --git a/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/locale/tr/LC_MESSAGES/dashtodock.mo b/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/locale/tr/LC_MESSAGES/dashtodock.mo new file mode 100644 index 0000000..b2e36b6 Binary files /dev/null and b/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/locale/tr/LC_MESSAGES/dashtodock.mo differ diff --git a/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/locale/uk_UA/LC_MESSAGES/dashtodock.mo b/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/locale/uk_UA/LC_MESSAGES/dashtodock.mo new file mode 100644 index 0000000..a475c96 Binary files /dev/null and b/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/locale/uk_UA/LC_MESSAGES/dashtodock.mo differ diff --git a/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/locale/zh_CN/LC_MESSAGES/dashtodock.mo b/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/locale/zh_CN/LC_MESSAGES/dashtodock.mo new file mode 100644 index 0000000..8ebfc33 Binary files /dev/null and b/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/locale/zh_CN/LC_MESSAGES/dashtodock.mo differ diff --git a/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/locale/zh_TW/LC_MESSAGES/dashtodock.mo b/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/locale/zh_TW/LC_MESSAGES/dashtodock.mo new file mode 100644 index 0000000..3ca257a Binary files /dev/null and b/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/locale/zh_TW/LC_MESSAGES/dashtodock.mo differ diff --git a/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/locations.js b/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/locations.js new file mode 100644 index 0000000..9dae93f --- /dev/null +++ b/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/locations.js @@ -0,0 +1,293 @@ +// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- + +const Gio = imports.gi.Gio; +const GLib = imports.gi.GLib; +const Gtk = imports.gi.Gtk; +const Shell = imports.gi.Shell; +const Signals = imports.signals; + +// Use __ () and N__() for the extension gettext domain, and reuse +// the shell domain with the default _() and N_() +const Gettext = imports.gettext.domain('dashtodock'); +const __ = Gettext.gettext; +const N__ = function(e) { return e }; + +const Me = imports.misc.extensionUtils.getCurrentExtension(); +const Utils = Me.imports.utils; + +const UPDATE_TRASH_DELAY = 500; + +/** + * This class maintains a Shell.App representing the Trash and keeps it + * up-to-date as the trash fills and is emptied over time. + */ +var Trash = class DashToDock_Trash { + + constructor() { + this._file = Gio.file_new_for_uri('trash://'); + try { + this._monitor = this._file.monitor_directory(0, null); + this._signalId = this._monitor.connect( + 'changed', + this._onTrashChange.bind(this) + ); + } catch (e) { + log(`Impossible to monitor trash: ${e}`) + } + this._lastEmpty = true; + this._empty = true; + this._schedUpdateId = 0; + this._updateTrash(); + } + + destroy() { + if (this._monitor) { + this._monitor.disconnect(this._signalId); + this._monitor.run_dispose(); + } + this._file.run_dispose(); + } + + _onTrashChange() { + if (this._schedUpdateId) { + GLib.source_remove(this._schedUpdateId); + } + this._schedUpdateId = GLib.timeout_add( + GLib.PRIORITY_DEFAULT, UPDATE_TRASH_DELAY, () => { + this._schedUpdateId = 0; + this._updateTrash(); + return GLib.SOURCE_REMOVE; + }); + } + + _updateTrash() { + try { + let children = this._file.enumerate_children('*', 0, null); + this._empty = children.next_file(null) == null; + children.close(null); + } catch (e) { + log(`Impossible to enumerate trash children: ${e}`) + return; + } + + this._ensureApp(); + } + + _ensureApp() { + if (this._trashApp == null || + this._lastEmpty != this._empty) { + let trashKeys = new GLib.KeyFile(); + trashKeys.set_string('Desktop Entry', 'Name', __('Trash')); + trashKeys.set_string('Desktop Entry', 'Icon', + this._empty ? 'user-trash' : 'user-trash-full'); + trashKeys.set_string('Desktop Entry', 'Type', 'Application'); + trashKeys.set_string('Desktop Entry', 'Exec', 'gio open trash:///'); + trashKeys.set_string('Desktop Entry', 'StartupNotify', 'false'); + trashKeys.set_string('Desktop Entry', 'XdtdUri', 'trash:///'); + if (!this._empty) { + trashKeys.set_string('Desktop Entry', 'Actions', 'empty-trash;'); + trashKeys.set_string('Desktop Action empty-trash', 'Name', __('Empty Trash')); + trashKeys.set_string('Desktop Action empty-trash', 'Exec', + 'dbus-send --print-reply --dest=org.gnome.Nautilus /org/gnome/Nautilus org.gnome.Nautilus.FileOperations.EmptyTrash'); + } + + let trashAppInfo = Gio.DesktopAppInfo.new_from_keyfile(trashKeys); + this._trashApp = new Shell.App({appInfo: trashAppInfo}); + this._lastEmpty = this._empty; + + this.emit('changed'); + } + } + + getApp() { + this._ensureApp(); + return this._trashApp; + } +} +Signals.addSignalMethods(Trash.prototype); + +/** + * This class maintains Shell.App representations for removable devices + * plugged into the system, and keeps the list of Apps up-to-date as + * devices come and go and are mounted and unmounted. + */ +var Removables = class DashToDock_Removables { + + constructor() { + this._signalsHandler = new Utils.GlobalSignalsHandler(); + + this._monitor = Gio.VolumeMonitor.get(); + this._volumeApps = [] + this._mountApps = [] + + this._monitor.get_volumes().forEach( + (volume) => { + this._onVolumeAdded(this._monitor, volume); + } + ); + + this._monitor.get_mounts().forEach( + (mount) => { + this._onMountAdded(this._monitor, mount); + } + ); + + this._signalsHandler.add([ + this._monitor, + 'mount-added', + this._onMountAdded.bind(this) + ], [ + this._monitor, + 'mount-removed', + this._onMountRemoved.bind(this) + ], [ + this._monitor, + 'volume-added', + this._onVolumeAdded.bind(this) + ], [ + this._monitor, + 'volume-removed', + this._onVolumeRemoved.bind(this) + ]); + } + + destroy() { + this._signalsHandler.destroy(); + this._monitor.run_dispose(); + } + + _getWorkingIconName(icon) { + if (icon instanceof Gio.EmblemedIcon) { + icon = icon.get_icon(); + } + if (icon instanceof Gio.ThemedIcon) { + let iconTheme = Gtk.IconTheme.get_default(); + let names = icon.get_names(); + for (let i = 0; i < names.length; i++) { + let iconName = names[i]; + if (iconTheme.has_icon(iconName)) { + return iconName; + } + } + return ''; + } else { + return icon.to_string(); + } + } + + _onVolumeAdded(monitor, volume) { + if (!volume.can_mount()) { + return; + } + + if (volume.get_identifier('class') == 'network') { + return; + } + + let activationRoot = volume.get_activation_root(); + if (!activationRoot) { + // Can't offer to mount a device if we don't know + // where to mount it. + // These devices are usually ejectable so you + // don't normally unmount them anyway. + return; + } + + let escapedUri = activationRoot.get_uri() + let uri = GLib.uri_unescape_string(escapedUri, null); + + let volumeKeys = new GLib.KeyFile(); + volumeKeys.set_string('Desktop Entry', 'Name', volume.get_name()); + volumeKeys.set_string('Desktop Entry', 'Icon', this._getWorkingIconName(volume.get_icon())); + volumeKeys.set_string('Desktop Entry', 'Type', 'Application'); + volumeKeys.set_string('Desktop Entry', 'Exec', 'gio open "' + uri + '"'); + volumeKeys.set_string('Desktop Entry', 'StartupNotify', 'false'); + volumeKeys.set_string('Desktop Entry', 'XdtdUri', escapedUri); + volumeKeys.set_string('Desktop Entry', 'Actions', 'mount;'); + volumeKeys.set_string('Desktop Action mount', 'Name', __('Mount')); + volumeKeys.set_string('Desktop Action mount', 'Exec', 'gio mount "' + uri + '"'); + let volumeAppInfo = Gio.DesktopAppInfo.new_from_keyfile(volumeKeys); + let volumeApp = new Shell.App({appInfo: volumeAppInfo}); + this._volumeApps.push(volumeApp); + this.emit('changed'); + } + + _onVolumeRemoved(monitor, volume) { + for (let i = 0; i < this._volumeApps.length; i++) { + let app = this._volumeApps[i]; + if (app.get_name() == volume.get_name()) { + this._volumeApps.splice(i, 1); + } + } + this.emit('changed'); + } + + _onMountAdded(monitor, mount) { + // Filter out uninteresting mounts + if (!mount.can_eject() && !mount.can_unmount()) + return; + if (mount.is_shadowed()) + return; + + let volume = mount.get_volume(); + if (!volume || volume.get_identifier('class') == 'network') { + return; + } + + let escapedUri = mount.get_root().get_uri() + let uri = GLib.uri_unescape_string(escapedUri, null); + + let mountKeys = new GLib.KeyFile(); + mountKeys.set_string('Desktop Entry', 'Name', mount.get_name()); + mountKeys.set_string('Desktop Entry', 'Icon', + this._getWorkingIconName(volume.get_icon())); + mountKeys.set_string('Desktop Entry', 'Type', 'Application'); + mountKeys.set_string('Desktop Entry', 'Exec', 'gio open "' + uri + '"'); + mountKeys.set_string('Desktop Entry', 'StartupNotify', 'false'); + mountKeys.set_string('Desktop Entry', 'XdtdUri', escapedUri); + mountKeys.set_string('Desktop Entry', 'Actions', 'unmount;'); + if (mount.can_eject()) { + mountKeys.set_string('Desktop Action unmount', 'Name', __('Eject')); + mountKeys.set_string('Desktop Action unmount', 'Exec', + 'gio mount -e "' + uri + '"'); + } else { + mountKeys.set_string('Desktop Entry', 'Actions', 'unmount;'); + mountKeys.set_string('Desktop Action unmount', 'Name', __('Unmount')); + mountKeys.set_string('Desktop Action unmount', 'Exec', + 'gio mount -u "' + uri + '"'); + } + let mountAppInfo = Gio.DesktopAppInfo.new_from_keyfile(mountKeys); + let mountApp = new Shell.App({appInfo: mountAppInfo}); + this._mountApps.push(mountApp); + this.emit('changed'); + } + + _onMountRemoved(monitor, mount) { + for (let i = 0; i < this._mountApps.length; i++) { + let app = this._mountApps[i]; + if (app.get_name() == mount.get_name()) { + this._mountApps.splice(i, 1); + } + } + this.emit('changed'); + } + + getApps() { + // When we have both a volume app and a mount app, we prefer + // the mount app. + let apps = new Map(); + this._volumeApps.map(function(app) { + apps.set(app.get_name(), app); + }); + this._mountApps.map(function(app) { + apps.set(app.get_name(), app); + }); + + let ret = []; + for (let app of apps.values()) { + ret.push(app); + } + return ret; + } +} +Signals.addSignalMethods(Removables.prototype); diff --git a/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/media/glossy.svg b/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/media/glossy.svg new file mode 100644 index 0000000..55b71ba --- /dev/null +++ b/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/media/glossy.svg @@ -0,0 +1,139 @@ + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + diff --git a/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/media/highlight_stacked_bg.svg b/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/media/highlight_stacked_bg.svg new file mode 100644 index 0000000..19be5a9 --- /dev/null +++ b/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/media/highlight_stacked_bg.svg @@ -0,0 +1,82 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + diff --git a/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/media/highlight_stacked_bg_h.svg b/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/media/highlight_stacked_bg_h.svg new file mode 100644 index 0000000..eeaa869 --- /dev/null +++ b/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/media/highlight_stacked_bg_h.svg @@ -0,0 +1,82 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + diff --git a/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/media/logo.svg b/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/media/logo.svg new file mode 100644 index 0000000..eebd0b1 --- /dev/null +++ b/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/media/logo.svg @@ -0,0 +1,528 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Dash to Dock + Michele + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/metadata.json b/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/metadata.json new file mode 100644 index 0000000..fadda6a --- /dev/null +++ b/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/metadata.json @@ -0,0 +1,12 @@ +{ +"shell-version": [ + "40" +], +"uuid": "dash-to-dock@micxgx.gmail.com", +"name": "Dash to Dock", +"description": "A dock for the Gnome Shell. This extension moves the dash out of the overview transforming it in a dock for an easier launching of applications and a faster switching between windows and desktops. Side and bottom placement options are available.", +"original-author": "micxgx@gmail.com", +"url": "https://micheleg.github.io/dash-to-dock/", +"gettext-domain": "dashtodock", +"version": 69 +} diff --git a/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/prefs.js b/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/prefs.js new file mode 100644 index 0000000..88c6df4 --- /dev/null +++ b/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/prefs.js @@ -0,0 +1,838 @@ +// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- + +const Gio = imports.gi.Gio; +const GLib = imports.gi.GLib; +const GObject = imports.gi.GObject; +const Gtk = imports.gi.Gtk; +const Gdk = imports.gi.Gdk; + +// Use __ () and N__() for the extension gettext domain, and reuse +// the shell domain with the default _() and N_() +const Gettext = imports.gettext.domain('dashtodock'); +const __ = Gettext.gettext; +const N__ = function (e) { return e }; + +const ExtensionUtils = imports.misc.extensionUtils; +const Me = ExtensionUtils.getCurrentExtension(); + +const SCALE_UPDATE_TIMEOUT = 500; +const DEFAULT_ICONS_SIZES = [128, 96, 64, 48, 32, 24, 16]; + +const TransparencyMode = { + DEFAULT: 0, + FIXED: 1, + DYNAMIC: 3 +}; + +const RunningIndicatorStyle = { + DEFAULT: 0, + DOTS: 1, + SQUARES: 2, + DASHES: 3, + SEGMENTED: 4, + SOLID: 5, + CILIORA: 6, + METRO: 7 +}; + +// TODO: +// function setShortcut(settings) { +// let shortcut_text = settings.get_string('shortcut-text'); +// let [key, mods] = Gtk.accelerator_parse(shortcut_text); + +// if (Gtk.accelerator_valid(key, mods)) { +// let shortcut = Gtk.accelerator_name(key, mods); +// settings.set_strv('shortcut', [shortcut]); +// } +// else { +// settings.set_strv('shortcut', []); +// } +// } + +var Settings = GObject.registerClass({ + Implements: [Gtk.BuilderScope], +}, class DashToDock_Settings extends GObject.Object { + + _init() { + super._init(); + + this._settings = ExtensionUtils.getSettings('org.gnome.shell.extensions.dash-to-dock'); + + this._rtl = (Gtk.Widget.get_default_direction() == Gtk.TextDirection.RTL); + + this._builder = new Gtk.Builder(); + this._builder.set_scope(this); + this._builder.set_translation_domain(Me.metadata['gettext-domain']); + this._builder.add_from_file(Me.path + '/Settings.ui'); + + this.widget = new Gtk.ScrolledWindow({ hscrollbar_policy: Gtk.PolicyType.NEVER }); + this._notebook = this._builder.get_object('settings_notebook'); + this.widget.set_child(this._notebook); + + // Timeout to delay the update of the settings + this._dock_size_timeout = 0; + this._icon_size_timeout = 0; + this._opacity_timeout = 0; + + this._bindSettings(); + } + + vfunc_create_closure(builder, handlerName, flags, connectObject) { + if (flags & Gtk.BuilderClosureFlags.SWAPPED) + throw new Error('Unsupported template signal flag "swapped"'); + + if (typeof this[handlerName] === 'undefined') + throw new Error(`${handlerName} is undefined`); + + return this[handlerName].bind(connectObject || this); + } + + dock_display_combo_changed_cb(combo) { + this._settings.set_int('preferred-monitor', this._monitors[combo.get_active()]); + } + + position_top_button_toggled_cb(button) { + if (button.get_active()) + this._settings.set_enum('dock-position', 0); + } + + position_right_button_toggled_cb(button) { + if (button.get_active()) + this._settings.set_enum('dock-position', 1); + } + + position_bottom_button_toggled_cb(button) { + if (button.get_active()) + this._settings.set_enum('dock-position', 2); + } + + position_left_button_toggled_cb(button) { + if (button.get_active()) + this._settings.set_enum('dock-position', 3); + } + + icon_size_combo_changed_cb(combo) { + this._settings.set_int('dash-max-icon-size', this._allIconSizes[combo.get_active()]); + } + + dock_size_scale_value_changed_cb(scale) { + // Avoid settings the size continuously + if (this._dock_size_timeout > 0) + GLib.source_remove(this._dock_size_timeout); + const id = this._dock_size_timeout = GLib.timeout_add( + GLib.PRIORITY_DEFAULT, SCALE_UPDATE_TIMEOUT, () => { + if (id === this._dock_size_timeout) { + this._settings.set_double('height-fraction', scale.get_value()); + this._dock_size_timeout = 0; + return GLib.SOURCE_REMOVE; + } + }); + } + + icon_size_scale_value_changed_cb(scale) { + // Avoid settings the size consinuosly + if (this._icon_size_timeout > 0) + GLib.source_remove(this._icon_size_timeout); + this._icon_size_timeout = GLib.timeout_add( + GLib.PRIORITY_DEFAULT, SCALE_UPDATE_TIMEOUT, () => { + log(scale.get_value()); + this._settings.set_int('dash-max-icon-size', scale.get_value()); + this._icon_size_timeout = 0; + return GLib.SOURCE_REMOVE; + }); + } + custom_opacity_scale_value_changed_cb(scale) { + // Avoid settings the opacity consinuosly as it's change is animated + if (this._opacity_timeout > 0) + GLib.source_remove(this._opacity_timeout); + this._opacity_timeout = GLib.timeout_add( + GLib.PRIORITY_DEFAULT, SCALE_UPDATE_TIMEOUT, () => { + this._settings.set_double('background-opacity', scale.get_value()); + this._opacity_timeout = 0; + return GLib.SOURCE_REMOVE; + }); + } + min_opacity_scale_value_changed_cb(scale) { + // Avoid settings the opacity consinuosly as it's change is animated + if (this._opacity_timeout > 0) + GLib.source_remove(this._opacity_timeout); + this._opacity_timeout = GLib.timeout_add( + GLib.PRIORITY_DEFAULT, SCALE_UPDATE_TIMEOUT, () => { + this._settings.set_double('min-alpha', scale.get_value()); + this._opacity_timeout = 0; + return GLib.SOURCE_REMOVE; + }); + } + max_opacity_scale_value_changed_cb(scale) { + // Avoid settings the opacity consinuosly as it's change is animated + if (this._opacity_timeout > 0) + GLib.source_remove(this._opacity_timeout); + this._opacity_timeout = GLib.timeout_add( + GLib.PRIORITY_DEFAULT, SCALE_UPDATE_TIMEOUT, () => { + this._settings.set_double('max-alpha', scale.get_value()); + this._opacity_timeout = 0; + return GLib.SOURCE_REMOVE; + }); + } + + all_windows_radio_button_toggled_cb(button) { + if (button.get_active()) + this._settings.set_enum('intellihide-mode', 0); + } + focus_application_windows_radio_button_toggled_cb(button) { + if (button.get_active()) + this._settings.set_enum('intellihide-mode', 1); + } + maximized_windows_radio_button_toggled_cb(button) { + if (button.get_active()) + this._settings.set_enum('intellihide-mode', 2); + } + + + _bindSettings() { + // Position and size panel + + // Monitor options + + this._monitors = []; + // Build options based on the number of monitors and the current settings. + let monitors = Gdk.Display.get_default().get_monitors(); + let n_monitors = monitors.length; + let primary_monitor = 0; // Gdk.Screen.get_default().get_primary_monitor(); + + let monitor = this._settings.get_int('preferred-monitor'); + + // Add primary monitor with index 0, because in GNOME Shell the primary monitor is always 0 + this._builder.get_object('dock_monitor_combo').append_text(__('Primary monitor')); + this._monitors.push(0); + + // Add connected monitors + let ctr = 0; + for (let i = 0; i < n_monitors; i++) { + if (i !== primary_monitor) { + ctr++; + this._monitors.push(ctr); + this._builder.get_object('dock_monitor_combo').append_text(__('Secondary monitor ') + ctr); + } + } + + // If one of the external monitor is set as preferred, show it even if not attached + if ((monitor >= n_monitors) && (monitor !== primary_monitor)) { + this._monitors.push(monitor) + this._builder.get_object('dock_monitor_combo').append_text(__('Secondary monitor ') + ++ctr); + } + + this._builder.get_object('dock_monitor_combo').set_active(this._monitors.indexOf(monitor)); + + // Position option + let position = this._settings.get_enum('dock-position'); + + switch (position) { + case 0: + this._builder.get_object('position_top_button').set_active(true); + break; + case 1: + this._builder.get_object('position_right_button').set_active(true); + break; + case 2: + this._builder.get_object('position_bottom_button').set_active(true); + break; + case 3: + this._builder.get_object('position_left_button').set_active(true); + break; + } + + if (this._rtl) { + /* Left is Right in rtl as a setting */ + this._builder.get_object('position_left_button').set_label(__('Right')); + this._builder.get_object('position_right_button').set_label(__('Left')); + } + + // Intelligent autohide options + this._settings.bind('dock-fixed', + this._builder.get_object('intelligent_autohide_switch'), + 'active', + Gio.SettingsBindFlags.INVERT_BOOLEAN); + this._settings.bind('dock-fixed', + this._builder.get_object('intelligent_autohide_button'), + 'sensitive', + Gio.SettingsBindFlags.INVERT_BOOLEAN); + this._settings.bind('autohide', + this._builder.get_object('autohide_switch'), + 'active', + Gio.SettingsBindFlags.DEFAULT); + this._settings.bind('autohide-in-fullscreen', + this._builder.get_object('autohide_enable_in_fullscreen_checkbutton'), + 'active', + Gio.SettingsBindFlags.DEFAULT); + this._settings.bind('require-pressure-to-show', + this._builder.get_object('require_pressure_checkbutton'), + 'active', + Gio.SettingsBindFlags.DEFAULT); + this._settings.bind('intellihide', + this._builder.get_object('intellihide_switch'), + 'active', + Gio.SettingsBindFlags.DEFAULT); + this._settings.bind('animation-time', + this._builder.get_object('animation_duration_spinbutton'), + 'value', + Gio.SettingsBindFlags.DEFAULT); + this._settings.bind('hide-delay', + this._builder.get_object('hide_timeout_spinbutton'), + 'value', + Gio.SettingsBindFlags.DEFAULT); + this._settings.bind('show-delay', + this._builder.get_object('show_timeout_spinbutton'), + 'value', + Gio.SettingsBindFlags.DEFAULT); + this._settings.bind('pressure-threshold', + this._builder.get_object('pressure_threshold_spinbutton'), + 'value', + Gio.SettingsBindFlags.DEFAULT); + + //this._builder.get_object('animation_duration_spinbutton').set_value(this._settings.get_double('animation-time')); + + // Create dialog for intelligent autohide advanced settings + this._builder.get_object('intelligent_autohide_button').connect('clicked', () => { + + let dialog = new Gtk.Dialog({ + title: __('Intelligent autohide customization'), + transient_for: this.widget.get_root(), + use_header_bar: true, + modal: true + }); + + // GTK+ leaves positive values for application-defined response ids. + // Use +1 for the reset action + dialog.add_button(__('Reset to defaults'), 1); + + let box = this._builder.get_object('intelligent_autohide_advanced_settings_box'); + dialog.get_content_area().append(box); + + this._settings.bind('intellihide', + this._builder.get_object('intellihide_mode_box'), + 'sensitive', + Gio.SettingsBindFlags.GET); + + // intellihide mode + + let intellihideModeRadioButtons = [ + this._builder.get_object('all_windows_radio_button'), + this._builder.get_object('focus_application_windows_radio_button'), + this._builder.get_object('maximized_windows_radio_button') + ]; + + intellihideModeRadioButtons[this._settings.get_enum('intellihide-mode')].set_active(true); + + this._settings.bind('autohide', + this._builder.get_object('require_pressure_checkbutton'), + 'sensitive', + Gio.SettingsBindFlags.GET); + + this._settings.bind('autohide', + this._builder.get_object('autohide_enable_in_fullscreen_checkbutton'), + 'sensitive', + Gio.SettingsBindFlags.GET); + + this._settings.bind('require-pressure-to-show', + this._builder.get_object('show_timeout_spinbutton'), + 'sensitive', + Gio.SettingsBindFlags.INVERT_BOOLEAN); + this._settings.bind('require-pressure-to-show', + this._builder.get_object('show_timeout_label'), + 'sensitive', + Gio.SettingsBindFlags.INVERT_BOOLEAN); + this._settings.bind('require-pressure-to-show', + this._builder.get_object('pressure_threshold_spinbutton'), + 'sensitive', + Gio.SettingsBindFlags.DEFAULT); + this._settings.bind('require-pressure-to-show', + this._builder.get_object('pressure_threshold_label'), + 'sensitive', + Gio.SettingsBindFlags.DEFAULT); + + dialog.connect('response', (dialog, id) => { + if (id == 1) { + // restore default settings for the relevant keys + let keys = ['intellihide', 'autohide', 'intellihide-mode', 'autohide-in-fullscreen', 'require-pressure-to-show', + 'animation-time', 'show-delay', 'hide-delay', 'pressure-threshold']; + keys.forEach(function (val) { + this._settings.set_value(val, this._settings.get_default_value(val)); + }, this); + intellihideModeRadioButtons[this._settings.get_enum('intellihide-mode')].set_active(true); + } else { + // remove the settings box so it doesn't get destroyed; + dialog.get_content_area().remove(box); + dialog.destroy(); + } + return; + }); + + dialog.present(); + + }); + + // size options + const dock_size_scale = this._builder.get_object('dock_size_scale'); + dock_size_scale.set_value(this._settings.get_double('height-fraction')); + dock_size_scale.add_mark(0.9, Gtk.PositionType.TOP, null); + dock_size_scale.set_format_value_func((_, value) => { + return Math.round(value * 100) + ' %'; + }); + let icon_size_scale = this._builder.get_object('icon_size_scale'); + icon_size_scale.set_range(8, DEFAULT_ICONS_SIZES[0]); + icon_size_scale.set_value(this._settings.get_int('dash-max-icon-size')); + DEFAULT_ICONS_SIZES.forEach(function (val) { + icon_size_scale.add_mark(val, Gtk.PositionType.TOP, val.toString()); + }); + icon_size_scale.set_format_value_func((_, value) => { + return value + ' px'; + }); + + // Corrent for rtl languages + if (this._rtl) { + // Flip value position: this is not done automatically + dock_size_scale.set_value_pos(Gtk.PositionType.LEFT); + icon_size_scale.set_value_pos(Gtk.PositionType.LEFT); + // I suppose due to a bug, having a more than one mark and one above a value of 100 + // makes the rendering of the marks wrong in rtl. This doesn't happen setting the scale as not flippable + // and then manually inverting it + icon_size_scale.set_flippable(false); + icon_size_scale.set_inverted(true); + } + + this._settings.bind('icon-size-fixed', this._builder.get_object('icon_size_fixed_checkbutton'), 'active', Gio.SettingsBindFlags.DEFAULT); + this._settings.bind('extend-height', this._builder.get_object('dock_size_extend_checkbutton'), 'active', Gio.SettingsBindFlags.DEFAULT); + this._settings.bind('extend-height', this._builder.get_object('dock_size_scale'), 'sensitive', Gio.SettingsBindFlags.INVERT_BOOLEAN); + + + // Apps panel + + this._settings.bind('show-running', + this._builder.get_object('show_running_switch'), + 'active', + Gio.SettingsBindFlags.DEFAULT); + this._settings.bind('isolate-workspaces', + this._builder.get_object('application_button_isolation_button'), + 'active', + Gio.SettingsBindFlags.DEFAULT); + this._settings.bind('isolate-monitors', + this._builder.get_object('application_button_monitor_isolation_button'), + 'active', + Gio.SettingsBindFlags.DEFAULT); + this._settings.bind('show-windows-preview', + this._builder.get_object('windows_preview_button'), + 'active', + Gio.SettingsBindFlags.DEFAULT); + this._settings.bind('multi-monitor', + this._builder.get_object('multi_monitor_button'), + 'active', + Gio.SettingsBindFlags.DEFAULT); + this._settings.bind('show-favorites', + this._builder.get_object('show_favorite_switch'), + 'active', + Gio.SettingsBindFlags.DEFAULT); + this._settings.bind('show-trash', + this._builder.get_object('show_trash_switch'), + 'active', + Gio.SettingsBindFlags.DEFAULT); + this._settings.bind('show-mounts', + this._builder.get_object('show_mounts_switch'), + 'active', + Gio.SettingsBindFlags.DEFAULT); + this._settings.bind('show-show-apps-button', + this._builder.get_object('show_applications_button_switch'), + 'active', + Gio.SettingsBindFlags.DEFAULT); + this._settings.bind('show-apps-at-top', + this._builder.get_object('application_button_first_button'), + 'active', + Gio.SettingsBindFlags.DEFAULT); + this._settings.bind('show-show-apps-button', + this._builder.get_object('application_button_first_button'), + 'sensitive', + Gio.SettingsBindFlags.DEFAULT); + this._settings.bind('animate-show-apps', + this._builder.get_object('application_button_animation_button'), + 'active', + Gio.SettingsBindFlags.DEFAULT); + this._settings.bind('show-show-apps-button', + this._builder.get_object('application_button_animation_button'), + 'sensitive', + Gio.SettingsBindFlags.DEFAULT); + + + // Behavior panel + + this._settings.bind('hot-keys', + this._builder.get_object('hot_keys_switch'), + 'active', + Gio.SettingsBindFlags.DEFAULT); + this._settings.bind('hot-keys', + this._builder.get_object('overlay_button'), + 'sensitive', + Gio.SettingsBindFlags.DEFAULT); + + this._builder.get_object('click_action_combo').set_active(this._settings.get_enum('click-action')); + this._builder.get_object('click_action_combo').connect('changed', (widget) => { + this._settings.set_enum('click-action', widget.get_active()); + }); + + this._builder.get_object('scroll_action_combo').set_active(this._settings.get_enum('scroll-action')); + this._builder.get_object('scroll_action_combo').connect('changed', (widget) => { + this._settings.set_enum('scroll-action', widget.get_active()); + }); + + this._builder.get_object('shift_click_action_combo').connect('changed', (widget) => { + this._settings.set_enum('shift-click-action', widget.get_active()); + }); + + this._builder.get_object('middle_click_action_combo').connect('changed', (widget) => { + this._settings.set_enum('middle-click-action', widget.get_active()); + }); + this._builder.get_object('shift_middle_click_action_combo').connect('changed', (widget) => { + this._settings.set_enum('shift-middle-click-action', widget.get_active()); + }); + + // Create dialog for number overlay options + this._builder.get_object('overlay_button').connect('clicked', () => { + + let dialog = new Gtk.Dialog({ + title: __('Show dock and application numbers'), + transient_for: this.widget.get_root(), + use_header_bar: true, + modal: true + }); + + // GTK+ leaves positive values for application-defined response ids. + // Use +1 for the reset action + dialog.add_button(__('Reset to defaults'), 1); + + let box = this._builder.get_object('box_overlay_shortcut'); + dialog.get_content_area().append(box); + + this._builder.get_object('overlay_switch').set_active(this._settings.get_boolean('hotkeys-overlay')); + this._builder.get_object('show_dock_switch').set_active(this._settings.get_boolean('hotkeys-show-dock')); + + // We need to update the shortcut 'strv' when the text is modified + this._settings.connect('changed::shortcut-text', () => { setShortcut(this._settings); }); + this._settings.bind('shortcut-text', + this._builder.get_object('shortcut_entry'), + 'text', + Gio.SettingsBindFlags.DEFAULT); + + this._settings.bind('hotkeys-overlay', + this._builder.get_object('overlay_switch'), + 'active', + Gio.SettingsBindFlags.DEFAULT); + this._settings.bind('hotkeys-show-dock', + this._builder.get_object('show_dock_switch'), + 'active', + Gio.SettingsBindFlags.DEFAULT); + this._settings.bind('shortcut-timeout', + this._builder.get_object('timeout_spinbutton'), + 'value', + Gio.SettingsBindFlags.DEFAULT); + + dialog.connect('response', (dialog, id) => { + if (id == 1) { + // restore default settings for the relevant keys + let keys = ['shortcut-text', 'hotkeys-overlay', 'hotkeys-show-dock', 'shortcut-timeout']; + keys.forEach(function (val) { + this._settings.set_value(val, this._settings.get_default_value(val)); + }, this); + } else { + // remove the settings box so it doesn't get destroyed; + dialog.get_content_area().remove(box); + dialog.destroy(); + } + return; + }); + + dialog.present(); + }); + + // Create dialog for middle-click options + this._builder.get_object('middle_click_options_button').connect('clicked', () => { + + let dialog = new Gtk.Dialog({ + title: __('Customize middle-click behavior'), + transient_for: this.widget.get_root(), + use_header_bar: true, + modal: true + }); + + // GTK+ leaves positive values for application-defined response ids. + // Use +1 for the reset action + dialog.add_button(__('Reset to defaults'), 1); + + let box = this._builder.get_object('box_middle_click_options'); + dialog.get_content_area().append(box); + + this._builder.get_object('shift_click_action_combo').set_active(this._settings.get_enum('shift-click-action')); + + this._builder.get_object('middle_click_action_combo').set_active(this._settings.get_enum('middle-click-action')); + + this._builder.get_object('shift_middle_click_action_combo').set_active(this._settings.get_enum('shift-middle-click-action')); + + this._settings.bind('shift-click-action', + this._builder.get_object('shift_click_action_combo'), + 'active-id', + Gio.SettingsBindFlags.DEFAULT); + this._settings.bind('middle-click-action', + this._builder.get_object('middle_click_action_combo'), + 'active-id', + Gio.SettingsBindFlags.DEFAULT); + this._settings.bind('shift-middle-click-action', + this._builder.get_object('shift_middle_click_action_combo'), + 'active-id', + Gio.SettingsBindFlags.DEFAULT); + + dialog.connect('response', (dialog, id) => { + if (id == 1) { + // restore default settings for the relevant keys + let keys = ['shift-click-action', 'middle-click-action', 'shift-middle-click-action']; + keys.forEach(function (val) { + this._settings.set_value(val, this._settings.get_default_value(val)); + }, this); + this._builder.get_object('shift_click_action_combo').set_active(this._settings.get_enum('shift-click-action')); + this._builder.get_object('middle_click_action_combo').set_active(this._settings.get_enum('middle-click-action')); + this._builder.get_object('shift_middle_click_action_combo').set_active(this._settings.get_enum('shift-middle-click-action')); + } else { + // remove the settings box so it doesn't get destroyed; + dialog.get_content_area().remove(box); + dialog.destroy(); + } + return; + }); + + dialog.present(); + + }); + + // Appearance Panel + + this._settings.bind('apply-custom-theme', this._builder.get_object('customize_theme'), 'sensitive', Gio.SettingsBindFlags.INVERT_BOOLEAN | Gio.SettingsBindFlags.GET); + this._settings.bind('apply-custom-theme', this._builder.get_object('builtin_theme_switch'), 'active', Gio.SettingsBindFlags.DEFAULT); + this._settings.bind('custom-theme-shrink', this._builder.get_object('shrink_dash_switch'), 'active', Gio.SettingsBindFlags.DEFAULT); + + // Running indicators + this._builder.get_object('running_indicators_combo').set_active( + this._settings.get_enum('running-indicator-style') + ); + this._builder.get_object('running_indicators_combo').connect( + 'changed', + (widget) => { + this._settings.set_enum('running-indicator-style', widget.get_active()); + } + ); + + if (this._settings.get_enum('running-indicator-style') == RunningIndicatorStyle.DEFAULT) + this._builder.get_object('running_indicators_advance_settings_button').set_sensitive(false); + + this._settings.connect('changed::running-indicator-style', () => { + if (this._settings.get_enum('running-indicator-style') == RunningIndicatorStyle.DEFAULT) + this._builder.get_object('running_indicators_advance_settings_button').set_sensitive(false); + else + this._builder.get_object('running_indicators_advance_settings_button').set_sensitive(true); + }); + + // Create dialog for running indicators advanced settings + this._builder.get_object('running_indicators_advance_settings_button').connect('clicked', () => { + + let dialog = new Gtk.Dialog({ + title: __('Customize running indicators'), + transient_for: this.widget.get_root(), + use_header_bar: true, + modal: true + }); + + let box = this._builder.get_object('running_dots_advance_settings_box'); + dialog.get_content_area().append(box); + + this._settings.bind('running-indicator-dominant-color', + this._builder.get_object('dominant_color_switch'), + 'active', + Gio.SettingsBindFlags.DEFAULT); + + this._settings.bind('custom-theme-customize-running-dots', + this._builder.get_object('dot_style_switch'), + 'active', + Gio.SettingsBindFlags.DEFAULT); + this._settings.bind('custom-theme-customize-running-dots', + this._builder.get_object('dot_style_settings_box'), + 'sensitive', Gio.SettingsBindFlags.DEFAULT); + + let rgba = new Gdk.RGBA(); + rgba.parse(this._settings.get_string('custom-theme-running-dots-color')); + this._builder.get_object('dot_color_colorbutton').set_rgba(rgba); + + this._builder.get_object('dot_color_colorbutton').connect('notify::rgba', (button) => { + let css = button.rgba.to_string(); + + this._settings.set_string('custom-theme-running-dots-color', css); + }); + + rgba.parse(this._settings.get_string('custom-theme-running-dots-border-color')); + this._builder.get_object('dot_border_color_colorbutton').set_rgba(rgba); + + this._builder.get_object('dot_border_color_colorbutton').connect('notify::rgba', (button) => { + let css = button.rgba.to_string(); + + this._settings.set_string('custom-theme-running-dots-border-color', css); + }); + + this._settings.bind('custom-theme-running-dots-border-width', + this._builder.get_object('dot_border_width_spin_button'), + 'value', + Gio.SettingsBindFlags.DEFAULT); + + + dialog.connect('response', (dialog, id) => { + // remove the settings box so it doesn't get destroyed; + dialog.get_content_area().remove(box); + dialog.destroy(); + return; + }); + + dialog.present(); + + }); + + this._settings.bind('custom-background-color', this._builder.get_object('custom_background_color_switch'), 'active', Gio.SettingsBindFlags.DEFAULT); + this._settings.bind('custom-background-color', this._builder.get_object('custom_background_color'), 'sensitive', Gio.SettingsBindFlags.DEFAULT); + + let rgba = new Gdk.RGBA(); + rgba.parse(this._settings.get_string('background-color')); + this._builder.get_object('custom_background_color').set_rgba(rgba); + + this._builder.get_object('custom_background_color').connect('notify::rgba', (button) => { + let css = button.rgba.to_string(); + + this._settings.set_string('background-color', css); + }); + + // Opacity + this._builder.get_object('customize_opacity_combo').set_active_id( + this._settings.get_enum('transparency-mode').toString() + ); + this._builder.get_object('customize_opacity_combo').connect( + 'changed', + (widget) => { + this._settings.set_enum('transparency-mode', parseInt(widget.get_active_id())); + } + ); + + const custom_opacity_scale = this._builder.get_object('custom_opacity_scale'); + custom_opacity_scale.set_value(this._settings.get_double('background-opacity')); + custom_opacity_scale.set_format_value_func((_, value) => { + return Math.round(value * 100) + '%'; + }); + + if (this._settings.get_enum('transparency-mode') !== TransparencyMode.FIXED) + this._builder.get_object('custom_opacity_scale').set_sensitive(false); + + this._settings.connect('changed::transparency-mode', () => { + if (this._settings.get_enum('transparency-mode') !== TransparencyMode.FIXED) + this._builder.get_object('custom_opacity_scale').set_sensitive(false); + else + this._builder.get_object('custom_opacity_scale').set_sensitive(true); + }); + + if (this._settings.get_enum('transparency-mode') !== TransparencyMode.DYNAMIC) { + this._builder.get_object('dynamic_opacity_button').set_sensitive(false); + } + + this._settings.connect('changed::transparency-mode', () => { + if (this._settings.get_enum('transparency-mode') !== TransparencyMode.DYNAMIC) { + this._builder.get_object('dynamic_opacity_button').set_sensitive(false); + } + else { + this._builder.get_object('dynamic_opacity_button').set_sensitive(true); + } + }); + + // Create dialog for transparency advanced settings + this._builder.get_object('dynamic_opacity_button').connect('clicked', () => { + + let dialog = new Gtk.Dialog({ + title: __('Customize opacity'), + transient_for: this.widget.get_root(), + use_header_bar: true, + modal: true + }); + + let box = this._builder.get_object('advanced_transparency_dialog'); + dialog.get_content_area().append(box); + + this._settings.bind( + 'customize-alphas', + this._builder.get_object('customize_alphas_switch'), + 'active', + Gio.SettingsBindFlags.DEFAULT + ); + this._settings.bind( + 'customize-alphas', + this._builder.get_object('min_alpha_scale'), + 'sensitive', + Gio.SettingsBindFlags.DEFAULT + ); + this._settings.bind( + 'customize-alphas', + this._builder.get_object('max_alpha_scale'), + 'sensitive', + Gio.SettingsBindFlags.DEFAULT + ); + + const min_alpha_scale = this._builder.get_object('min_alpha_scale'); + const max_alpha_scale = this._builder.get_object('max_alpha_scale'); + min_alpha_scale.set_value( + this._settings.get_double('min-alpha') + ); + min_alpha_scale.set_format_value_func((_, value) => { + return Math.round(value * 100) + ' %'; + }); + max_alpha_scale.set_format_value_func((_, value) => { + return Math.round(value * 100) + ' %'; + }); + + max_alpha_scale.set_value( + this._settings.get_double('max-alpha') + ); + + dialog.connect('response', (dialog, id) => { + // remove the settings box so it doesn't get destroyed; + dialog.get_content_area().remove(box); + dialog.destroy(); + return; + }); + + dialog.present(); + }); + + + this._settings.bind('unity-backlit-items', + this._builder.get_object('unity_backlit_items_switch'), + 'active', Gio.SettingsBindFlags.DEFAULT + ); + + this._settings.bind('force-straight-corner', + this._builder.get_object('force_straight_corner_switch'), + 'active', Gio.SettingsBindFlags.DEFAULT); + + // About Panel + + this._builder.get_object('extension_version').set_label(Me.metadata.version.toString()); + } +}); + +function init() { + ExtensionUtils.initTranslations(); +} + +function buildPrefsWidget() { + let settings = new Settings(); + let widget = settings.widget; + if (widget.show_all) widget.show_all(); + return widget; +} diff --git a/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/schemas/gschemas.compiled b/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/schemas/gschemas.compiled new file mode 100644 index 0000000..e39b067 Binary files /dev/null and b/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/schemas/gschemas.compiled differ diff --git a/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/schemas/org.gnome.shell.extensions.dash-to-dock.gschema.xml b/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/schemas/org.gnome.shell.extensions.dash-to-dock.gschema.xml new file mode 100644 index 0000000..54dd9f2 --- /dev/null +++ b/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/schemas/org.gnome.shell.extensions.dash-to-dock.gschema.xml @@ -0,0 +1,551 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 'BOTTOM' + Dock position + Dock is shown on the Left, Right, Top or Bottom side of the screen. + + + 0.2 + Animation time + Sets the time duration of the autohide effect. + + + 0.25 + Show delay + Sets the delay after the mouse reaches the screen border before showing the dock. + + + 0.20 + Show delay + Sets the delay after the mouse left the dock before hiding it. + + + false + Set a custom dash background background color + Sets the color for the dash background. + + + "#ffffff" + Dash background color. + Customize the background color of the dash. + + + 'DEFAULT' + Transparency mode for the dock + FIXED: constant transparency. DYNAMIC: dock takes the opaque style only when windows are close to it. + + + 'DEFAULT' + ... + DEFAULT: .... DOTS: .... + + + false + Use application icon dominant color for the indicator color + + + + false + Manually set the min and max opacity + For the dynamic mode, the min/max opacity values will be given by 'min-alpha' and 'max-alpha'. + + + 0.2 + Opacity of the dash background when free-floating + Sets the opacity of the dash background when no windows are close. + + + 0.8 + Opacity of the dash background when windows are close. + Sets the opacity of the dash background when windows are close. + + + 0.8 + Opacity of the dash background + Sets the opacity of the dash background when in autohide mode. + + + true + Dock dodges windows + Enable or disable intellihide mode + + + 'FOCUS_APPLICATION_WINDOWS' + Define which windows are considered for intellihide. + + + + true + Dock shown on mouse over + Enable or disable autohide mode + + + true + Require pressure to show dash + Enable or disable requiring pressure to show the dash + + + 100 + Pressure threshold + Sets how much pressure is needed to show the dash. + + + false + Enable autohide in fullscreen mode. + Enable autohide in fullscreen mode. + + + false + Dock always visible + Dock is always visible + + + true + Switch workspace by scrolling over the dock + Add the possibility to switch workspace by mouse scrolling over the dock. + + + 48 + Maximum dash icon size + Set the allowed maximum dash icon size. Allowed range: 16..64. + + + false + Fixed icon size + Keep the icon size fived by scrolling the dock. + + + false + Apply custom theme + Apply customization to the dash appearance + + + false + TODO + TODO + + + false + Customize the style of the running application indicators. + Customize the style of the running application indicators. + + + "#ffffff" + Running application indicators color + Customize the color of the running application indicators. + + + "#ffffff" + Running application indicators border color. + Customize the border color of the running application indicators. + + + 0 + Running application indicators border width. + Customize the border width of the running application indicators. + + + true + Show running apps + Show or hide running applications icons in the dash + + + false + Provide workspace isolation + Dash shows only windows from the currentworkspace + + + false + Provide monitor isolation + Dash shows only windows from the monitor + + + true + Show preview of the open windows + Replace open windows list with windows previews + + + true + Show favorites apps + Show or hide favorite applications icons in the dash + + + true + Show trash can + Show or hide the trash can icon in the dash + + + true + Show mounted volumes and devices + Show or hide mounted volume and device icons in the dash + + + true + Show applications button + Show applications button in the dash + + + false + Show application button on the left + Show application button on the left of the dash + + + true + Animate Show Applications from the desktop + Animate Show Applications from the desktop + + + true + Basic compatibility with bolt extensions + Make the extension work properly when bolt extensions is enabled + + + 0.90 + Dock max height (fraction of available space) + + + false + Extend the dock container to all the available height + + + -1 + Monitor on which putting the dock + Set on which monitor to put the dock, use -1 for the primary one + + + false + Enable multi-monitor docks + Show a dock on every monitor + + + true + Minimize on shift+click + + + true + Activate only one window + + + 'cycle-windows' + Action when clicking on a running app + Set the action that is executed when clicking on the icon of a running application + + + 'do-nothing' + Action when scrolling app + Set the action that is executed when scrolling on the application icon + + + 'minimize' + Action when shift+clicking on a running app + Set the action that is executed when shift+clicking on the icon of a running application + + + 'launch' + Action when clicking on a running app + Set the action that is executed when middle-clicking on the icon of a running application + + + 'launch' + Action when clicking on a running app + Set the action that is executed when shift+middle-clicking on the icon of a running application + + + true + Super Hot-Keys + Launch and switch between dash items using Super+(0-9) + + + true + Show the dock when using the hotkeys + The dock will be quickly shown so that the number-overlay is visible and app activation is easier + + + "<Super>q" + Keybinding to show the dock and the number overlay. + Behavior depends on hotkeys-show-dock and hotkeys-overlay. + + + q']]]> + Keybinding to show the dock and the number overlay. + Behavior depends on hotkeys-show-dock and hotkeys-overlay. + + + 2 + Timeout to hide the dock + Sets the time duration before the dock is hidden again. + + + true + Show the dock when using the hotkeys + The dock will be quickly shown so that the number-overlay is visible and app activation is easier + + + 1']]]> + Keybinding to launch 1st dash app + + Keybinding to launch 1st app. + + + + 2']]]> + Keybinding to launch 2nd dash app + + Keybinding to launch 2nd app. + + + + 3']]]> + Keybinding to launch 3rd dash app + + Keybinding to launch 3rd app. + + + + 4']]]> + Keybinding to launch 4th dash app + + Keybinding to launch 4th app. + + + + 5']]]> + Keybinding to launch 5th dash app + + Keybinding to launch 5th app. + + + + 6']]]> + Keybinding to launch 6th dash app + + Keybinding to launch 6th app. + + + + 7']]]> + Keybinding to launch 7th dash app + + Keybinding to launch 7th app. + + + + 8']]]> + Keybinding to launch 8th dash app + + Keybinding to launch 8th app. + + + + 9']]]> + Keybinding to launch 9th dash app + + Keybinding to launch 9th app. + + + + 0']]]> + Keybinding to launch 10th dash app + + Keybinding to launch 10th app. + + + + 1']]]> + Keybinding to trigger 1st dash app with shift behavior + + Keybinding to trigger 1st app with shift behavior. + + + + 2']]]> + Keybinding to trigger 2nd dash app with shift behavior + + Keybinding to trigger 2nd app with shift behavior. + + + + 3']]]> + Keybinding to trigger 3rd dash app with shift behavior + + Keybinding to trigger 3rd app with shift behavior. + + + + 4']]]> + Keybinding to trigger 4th dash app with shift behavior + + Keybinding to trigger 4th app with shift behavior. + + + + 5']]]> + Keybinding to trigger 5th dash app with shift behavior + + Keybinding to trigger 5th app with shift behavior. + + + + 6']]]> + Keybinding to trigger 6th dash app with shift behavior + + Keybinding to trigger 6th app with shift behavior. + + + + 7']]]> + Keybinding to trigger 7th dash app with shift behavior + + Keybinding to trigger 7th app with shift behavior. + + + + 8']]]> + Keybinding to trigger 8th dash app with shift behavior + + Keybinding to trigger 8th app with shift behavior. + + + + 9']]]> + Keybinding to trigger 9th dash app with shift behavior + + Keybinding to trigger 9th app with shift behavior. + + + + 0']]]> + Keybinding to trigger 10th dash app with shift behavior + + Keybinding to trigger 10th app with shift behavior. + + + + 1']]]> + Keybinding to trigger 1st dash app + + Keybinding to either show or launch the 1st application in the dash. + + + + 2']]]> + Keybinding to trigger 2nd dash app + + Keybinding to either show or launch the 2nd application in the dash. + + + + 3']]]> + Keybinding to trigger 3rd dash app + + Keybinding to either show or launch the 3rd application in the dash. + + + + 4']]]> + Keybinding to trigger 4th dash app + + Keybinding to either show or launch the 4th application in the dash. + + + + 5']]]> + Keybinding to trigger 5th dash app + + Keybinding to either show or launch the 5th application in the dash. + + + + 6']]]> + Keybinding to trigger 6th dash app + + Keybinding to either show or launch the 6th application in the dash. + + + + 7']]]> + Keybinding to trigger 7th dash app + + Keybinding to either show or launch the 7th application in the dash. + + + + 8']]]> + Keybinding to trigger 8th dash app + + Keybinding to either show or launch the 8th application in the dash. + + + + 9']]]> + Keybinding to trigger 9th dash app + + Keybinding to either show or launch the 9th application in the dash. + + + + 0']]]> + Keybinding to trigger 10th dash app + + Keybinding to either show or launch the 10th application in the dash. + + + + false + Force straight corners in dash + Make the borders in the dash non rounded + + + false + Enable unity7 like glossy backlit items + Emulate the unity7 backlit glossy items behaviour + + + diff --git a/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/stylesheet.css b/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/stylesheet.css new file mode 100644 index 0000000..9a427f2 --- /dev/null +++ b/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/stylesheet.css @@ -0,0 +1,231 @@ +/* Shrink the dash by reducing padding */ +#dashtodockContainer.bottom.shrink #dash .dash-background { + border: 1px; + padding: 2.5px; + border-bottom: 0px; } + +#dashtodockContainer.bottom.shrink #dash .dash-item-container .app-well-app, +#dashtodockContainer.bottom.shrink #dash .dash-item-container .show-apps { + padding: 2.5px; } + +#dashtodockContainer.bottom.extended #dash { + margin-top: 0; + margin-bottom: 0; } + +#dashtodockContainer.top.shrink #dash .dash-background { + border: 1px; + padding: 2.5px; + border-top: 0px; } + +#dashtodockContainer.top.shrink #dash .dash-item-container .app-well-app, +#dashtodockContainer.top.shrink #dash .dash-item-container .show-apps { + padding: 2.5px; } + +#dashtodockContainer.top.extended #dash { + margin-top: 0; + margin-bottom: 0; } + +#dashtodockContainer.left.shrink #dash .dash-background { + border: 1px; + padding: 2.5px; + border-left: 0px; } + +#dashtodockContainer.left.shrink #dash .dash-item-container .app-well-app, +#dashtodockContainer.left.shrink #dash .dash-item-container .show-apps { + padding: 2.5px; } + +#dashtodockContainer.left.extended #dash { + margin-top: 0; + margin-bottom: 0; } + +#dashtodockContainer.right.shrink #dash .dash-background { + border: 1px; + padding: 2.5px; + border-right: 0px; } + +#dashtodockContainer.right.shrink #dash .dash-item-container .app-well-app, +#dashtodockContainer.right.shrink #dash .dash-item-container .show-apps { + padding: 2.5px; } + +#dashtodockContainer.right.extended #dash { + margin-top: 0; + margin-bottom: 0; } + +#dashtodockContainer.bottom.shrink #dash .dash-background { + margin-top: 0; + margin-bottom: 4px; } + +#dashtodockContainer.straight-corner #dash .dash-background, +#dashtodockContainer.shrink.straight-corner #dash .dash-background { + border-radius: 0px; } + +/* Scrollview style */ +.bottom #dashtodockDashScrollview, +.top #dashtodockDashScrollview { + -st-hfade-offset: 24px; } + +.left #dashtodockDashScrollview, +.right #dashtodockDashScrollview { + -st-vfade-offset: 24px; } + +#dashtodockContainer.running-dots .dash-item-container > StButton, +#dashtodockContainer.dashtodock .dash-item-container > StButton { + transition-duration: 250; + background-size: contain; } + +#dashtodockContainer #dash .dash-separator { + margin-bottom: 0; } + +#dashtodockContainer #dash .vertical-dash-separator { + height: 1px; + margin: 6.5px 0; + background-color: rgba(238, 238, 236, 0.3); } + +#dashtodockContainer.bottom #dash { + margin-top: 0; } + #dashtodockContainer.bottom #dash .dash-background { + margin-bottom: 8px; } + #dashtodockContainer.bottom #dash .dash-item-container .app-well-app, + #dashtodockContainer.bottom #dash .dash-item-container .show-apps { + padding: 10px 1.5px 18px; } + +#dashtodockContainer.bottom.overview #dash { + margin-top: 12px; } + +#dashtodockContainer.bottom.extended #dash .dash-background, #dashtodockContainer.bottom.shrink #dash .dash-background { + margin-bottom: 0; } + +#dashtodockContainer.bottom.extended #dash .dash-item-container .app-well-app, +#dashtodockContainer.bottom.extended #dash .dash-item-container .show-apps { + padding-bottom: 10px; } + +#dashtodockContainer.bottom.shrink #dash .dash-item-container .app-well-app, +#dashtodockContainer.bottom.shrink #dash .dash-item-container .show-apps { + padding-bottom: 2.5px; } + +#dashtodockContainer.left #dash, +#dashtodockContainer.right #dash { + padding-left: 4px; + padding-right: 4px; } + #dashtodockContainer.left #dash .dash-item-container .app-well-app, + #dashtodockContainer.left #dash .dash-item-container .show-apps, + #dashtodockContainer.right #dash .dash-item-container .app-well-app, + #dashtodockContainer.right #dash .dash-item-container .show-apps { + /* In vertical we don't want to add additional padding below the button. */ + padding-bottom: 6px; + padding-top: 6px; } + +#dashtodockContainer.left.shrink #dash, +#dashtodockContainer.right.shrink #dash { + padding-left: 0; + padding-right: 0; } + +#dashtodockContainer.top #dash { + margin-bottom: 0px; } + +/* Dash height extended to the whole available vertical space */ +#dashtodockContainer.extended.left #dash, #dashtodockContainer.extended.right #dash, #dashtodockContainer.extended.top #dash, #dashtodockContainer.extended.bottom #dash { + padding-top: 0; + padding-bottom: 0; + padding-left: 0; + padding-right: 0; } + #dashtodockContainer.extended.left #dash .dash-background, #dashtodockContainer.extended.right #dash .dash-background, #dashtodockContainer.extended.top #dash .dash-background, #dashtodockContainer.extended.bottom #dash .dash-background { + border-radius: 0; } + +#dashtodockContainer.extended.top #dash, #dashtodockContainer.extended.bottom #dash { + border-left: 0px; + border-right: 0px; } + #dashtodockContainer.extended.top #dash .dash-background, #dashtodockContainer.extended.bottom #dash .dash-background { + padding-left: 0; + padding-right: 0; } + +#dashtodockContainer.extended.left #dash, #dashtodockContainer.extended.right #dash { + border-top: 0px; + border-bottom: 0px; } + +/* Running and focused application style */ +#dashtodockContainer.running-dots .app-well-app.running > .overview-icon, +#dashtodockContainer.dashtodock .app-well-app.running > .overview-icon { + background-image: none; } + +#dashtodockContainer.running-dots .app-well-app.focused .overview-icon, +#dashtodockContainer.dashtodock .app-well-app.focused .overview-icon { + background-color: rgba(238, 238, 236, 0.2); } + +#dashtodockContainer.dashtodock #dash .dash-background { + background: #2e3436; } + +#dashtodockContainer.dashtodock .progress-bar { + /* Customization of the progress bar style, e.g.: + -progress-bar-background: rgba(0.8, 0.8, 0.8, 1); + -progress-bar-border: rgba(0.9, 0.9, 0.9, 1); + */ } + +#dashtodockContainer.top #dash .placeholder, +#dashtodockContainer.bottom #dash .placeholder { + width: 32px; + height: 1px; } + +/* + * This is applied to a dummy actor. Only the alpha value for the background and border color + * and the transition-duration are used + */ +#dashtodockContainer.dummy-opaque { + background-color: rgba(0, 0, 0, 0.8); + border-color: rgba(0, 0, 0, 0.4); + transition-duration: 300ms; } + +/* + * This is applied to a dummy actor. Only the alpha value for the background and border color + * and the transition-duration are used + */ +#dashtodockContainer.dummy-transparent { + background-color: rgba(0, 0, 0, 0.2); + border-color: rgba(0, 0, 0, 0.1); + transition-duration: 500ms; } + +#dashtodockContainer .number-overlay { + color: white; + background-color: rgba(0, 0, 0, 0.8); + text-align: center; } + +#dashtodockContainer .notification-badge { + color: white; + background-color: red; + padding: 0.2em 0.5em; + border-radius: 1em; + font-weight: bold; + text-align: center; + margin: 2px; } + +#dashtodockPreviewSeparator.popup-separator-menu-item-horizontal { + width: 1px; + height: auto; + border-right-width: 1px; + margin: 32px 0px; } + +.dashtodock-app-well-preview-menu-item { + padding: 1em 1em 0.5em 1em; } + +#dashtodockContainer .metro .overview-icon { + border-radius: 0px; } + +#dashtodockContainer.bottom .metro.running2.focused, +#dashtodockContainer.bottom .metro.running3.focused, +#dashtodockContainer.bottom .metro.running4.focused, +#dashtodockContainer.top .metro.running2.focused, +#dashtodockContainer.top .metro.running3.focused, +#dashtodockContainer.top .metro.running4.focused { + background-image: url("./media/highlight_stacked_bg.svg"); + background-position: 0px 0px; + background-size: contain; } + +#dashtodockContainer.left .metro.running2.focused, +#dashtodockContainer.left .metro.running3.focused, +#dashtodockContainer.left .metro.running4.focused, +#dashtodockContainer.right .metro.running2.focused, +#dashtodockContainer.right .metro.running3.focused, +#dashtodockContainer.right .metro.running4.focused { + background-image: url("./media/highlight_stacked_bg_h.svg"); + background-position: 0px 0px; + background-size: contain; } diff --git a/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/theming.js b/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/theming.js new file mode 100644 index 0000000..4b9e10a --- /dev/null +++ b/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/theming.js @@ -0,0 +1,553 @@ +// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- + +const Gio = imports.gi.Gio; +const GLib = imports.gi.GLib; +const Signals = imports.signals; +const Meta = imports.gi.Meta; +const Shell = imports.gi.Shell; +const St = imports.gi.St; +const Clutter = imports.gi.Clutter; + +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 Util = imports.misc.util; +const Workspace = imports.ui.workspace; + +const Me = imports.misc.extensionUtils.getCurrentExtension(); +const Docking = Me.imports.docking; +const Utils = Me.imports.utils; + +/* + * DEFAULT: transparency given by theme + * FIXED: constant transparency chosen by user + * DYNAMIC: apply 'transparent' style when no windows are close to the dock + * */ +const TransparencyMode = { + DEFAULT: 0, + FIXED: 1, + DYNAMIC: 3 +}; + +/** + * Manage theme customization and custom theme support + */ +var ThemeManager = class DashToDock_ThemeManager { + + constructor(dock) { + this._signalsHandler = new Utils.GlobalSignalsHandler(); + this._bindSettingsChanges(); + this._actor = dock; + this._dash = dock.dash; + + // initialize colors with generic values + this._customizedBackground = {red: 0, green: 0, blue: 0, alpha: 0}; + this._customizedBorder = {red: 0, green: 0, blue: 0, alpha: 0}; + this._transparency = new Transparency(dock); + + this._signalsHandler.add([ + // When theme changes re-obtain default background color + St.ThemeContext.get_for_stage (global.stage), + 'changed', + this.updateCustomTheme.bind(this) + ], [ + // update :overview pseudoclass + Main.overview, + 'showing', + this._onOverviewShowing.bind(this) + ], [ + Main.overview, + 'hiding', + this._onOverviewHiding.bind(this) + ]); + + this._updateCustomStyleClasses(); + + // destroy themeManager when the managed actor is destroyed (e.g. extension unload) + // in order to disconnect signals + this._actor.connect('destroy', this.destroy.bind(this)); + + } + + destroy() { + this._signalsHandler.destroy(); + this._transparency.destroy(); + } + + _onOverviewShowing() { + this._actor.add_style_pseudo_class('overview'); + } + + _onOverviewHiding() { + this._actor.remove_style_pseudo_class('overview'); + } + + _updateDashOpacity() { + let newAlpha = Docking.DockManager.settings.get_double('background-opacity'); + + let [backgroundColor, borderColor] = this._getDefaultColors(); + + if (backgroundColor==null) + return; + + // Get the background and border alphas. We check the background alpha + // for a minimum of .001 to prevent division by 0 errors + let backgroundAlpha = Math.max(Math.round(backgroundColor.alpha/2.55)/100, .001); + let borderAlpha = Math.round(borderColor.alpha/2.55)/100; + + // The border and background alphas should remain in sync + // We also limit the borderAlpha to a maximum of 1 (full opacity) + borderAlpha = Math.min((borderAlpha/backgroundAlpha)*newAlpha, 1); + + this._customizedBackground = 'rgba(' + + backgroundColor.red + ',' + + backgroundColor.green + ',' + + backgroundColor.blue + ',' + + newAlpha + ')'; + + this._customizedBorder = 'rgba(' + + borderColor.red + ',' + + borderColor.green + ',' + + borderColor.blue + ',' + + borderAlpha + ')'; + + } + + _getDefaultColors() { + // Prevent shell crash if the actor is not on the stage. + // It happens enabling/disabling repeatedly the extension + if (!this._dash._container.get_stage()) + return [null, null]; + + // Remove custom style + let oldStyle = this._dash._container.get_style(); + this._dash._container.set_style(null); + + let themeNode = this._dash._container.get_theme_node(); + this._dash._container.set_style(oldStyle); + + let backgroundColor = themeNode.get_background_color(); + + // Just in case the theme has different border colors .. + // We want to find the inside border-color of the dock because it is + // the side most visible to the user. We do this by finding the side + // opposite the position + let position = Utils.getPosition(); + let side = position + 2; + if (side > 3) + side = Math.abs(side - 4); + + let borderColor = themeNode.get_border_color(side); + + return [backgroundColor, borderColor]; + } + + _updateDashColor() { + // Retrieve the color. If needed we will adjust it before passing it to + // this._transparency. + let [backgroundColor, borderColor] = this._getDefaultColors(); + + if (backgroundColor==null) + return; + + let settings = Docking.DockManager.settings; + + if (settings.get_boolean('custom-background-color')) { + // When applying a custom color, we need to check the alpha value, + // if not the opacity will always be overridden by the color below. + // Note that if using 'dynamic' transparency modes, + // the opacity will be set by the opaque/transparent styles anyway. + let newAlpha = Math.round(backgroundColor.alpha/2.55)/100; + if (settings.get_enum('transparency-mode') == TransparencyMode.FIXED) + newAlpha = settings.get_double('background-opacity'); + + backgroundColor = settings.get_string('background-color'); + + this._customizedBackground = backgroundColor; + + this._customizedBorder = this._customizedBackground; + + // backgroundColor is a string like rgb(0,0,0) + const color = Clutter.Color.from_string(backgroundColor); + color.alpha = newAlpha; + + this._transparency.setColor(color); + } else { + // backgroundColor is a Clutter.Color object + this._transparency.setColor(backgroundColor); + } + } + + _updateCustomStyleClasses() { + let settings = Docking.DockManager.settings; + + if (settings.get_boolean('apply-custom-theme')) + this._actor.add_style_class_name('dashtodock'); + else + this._actor.remove_style_class_name('dashtodock'); + + if (settings.get_boolean('custom-theme-shrink')) + this._actor.add_style_class_name('shrink'); + else + this._actor.remove_style_class_name('shrink'); + + if (settings.get_enum('running-indicator-style') !== 0) + this._actor.add_style_class_name('running-dots'); + else + this._actor.remove_style_class_name('running-dots'); + + // If not the built-in theme option is not selected + if (!settings.get_boolean('apply-custom-theme')) { + if (settings.get_boolean('force-straight-corner')) + this._actor.add_style_class_name('straight-corner'); + else + this._actor.remove_style_class_name('straight-corner'); + } else { + this._actor.remove_style_class_name('straight-corner'); + } + } + + updateCustomTheme() { + this._updateCustomStyleClasses(); + this._updateDashOpacity(); + this._updateDashColor(); + this._adjustTheme(); + this._dash._redisplay(); + } + + /** + * Reimported back and adapted from atomdock + */ + _adjustTheme() { + // Prevent shell crash if the actor is not on the stage. + // It happens enabling/disabling repeatedly the extension + if (!this._dash._background.get_stage()) + return; + + let settings = Docking.DockManager.settings; + + // Remove prior style edits + this._dash._background.set_style(null); + this._transparency.disable(); + + // If built-in theme is enabled do nothing else + if (settings.get_boolean('apply-custom-theme')) + return; + + let newStyle = ''; + let position = Utils.getPosition(settings); + + // obtain theme border settings + let themeNode = this._dash._background.get_theme_node(); + let borderColor = themeNode.get_border_color(St.Side.TOP); + let borderWidth = themeNode.get_border_width(St.Side.TOP); + + // We're copying border and corner styles to left border and top-left + // corner, also removing bottom border and bottom-right corner styles + let borderInner = ''; + let borderMissingStyle = ''; + + if (this._rtl && (position != St.Side.RIGHT)) + borderMissingStyle = 'border-right: ' + borderWidth + 'px solid ' + + borderColor.to_string() + ';'; + else if (!this._rtl && (position != St.Side.LEFT)) + borderMissingStyle = 'border-left: ' + borderWidth + 'px solid ' + + borderColor.to_string() + ';'; + + newStyle = borderMissingStyle; + + // I do call set_style possibly twice so that only the background gets the transition. + // The transition-property css rules seems to be unsupported + this._dash._background.set_style(newStyle); + + // Customize background + let fixedTransparency = settings.get_enum('transparency-mode') == TransparencyMode.FIXED; + let defaultTransparency = settings.get_enum('transparency-mode') == TransparencyMode.DEFAULT; + if (!defaultTransparency && !fixedTransparency) { + this._transparency.enable(); + } + else if (!defaultTransparency || settings.get_boolean('custom-background-color')) { + newStyle = newStyle + 'background-color:'+ this._customizedBackground + '; ' + + 'border-color:'+ this._customizedBorder + '; ' + + 'transition-delay: 0s; transition-duration: 0.250s;'; + this._dash._background.set_style(newStyle); + } + } + + _bindSettingsChanges() { + let keys = ['transparency-mode', + 'customize-alphas', + 'min-alpha', + 'max-alpha', + 'background-opacity', + 'custom-background-color', + 'background-color', + 'apply-custom-theme', + 'custom-theme-shrink', + 'custom-theme-running-dots', + 'extend-height', + 'force-straight-corner']; + + keys.forEach(function(key) { + this._signalsHandler.add([ + Docking.DockManager.settings, + 'changed::' + key, + this.updateCustomTheme.bind(this) + ]); + }, this); + } +}; + +/** + * The following class is based on the following upstream commit: + * https://git.gnome.org/browse/gnome-shell/commit/?id=447bf55e45b00426ed908b1b1035f472c2466956 + * Transparency when free-floating + */ +var Transparency = class DashToDock_Transparency { + + constructor(dock) { + this._dash = dock.dash; + this._actor = this._dash._container; + this._backgroundActor = this._dash._background; + this._dockActor = dock; + this._dock = dock; + this._panel = Main.panel; + this._position = Utils.getPosition(); + + // All these properties are replaced with the ones in the .dummy-opaque and .dummy-transparent css classes + this._backgroundColor = '0,0,0'; + this._transparentAlpha = '0.2'; + this._opaqueAlpha = '1'; + this._transparentAlphaBorder = '0.1'; + this._opaqueAlphaBorder = '0.5'; + this._transparentTransition = '0ms'; + this._opaqueTransition = '0ms'; + this._base_actor_style = ""; + + this._signalsHandler = new Utils.GlobalSignalsHandler(); + this._injectionsHandler = new Utils.InjectionsHandler(); + this._trackedWindows = new Map(); + } + + enable() { + // ensure I never double-register/inject + // although it should never happen + this.disable(); + + this._base_actor_style = this._actor.get_style(); + if (this._base_actor_style == null) { + this._base_actor_style = ""; + } + + this._signalsHandler.addWithLabel('transparency', [ + global.window_group, + 'actor-added', + this._onWindowActorAdded.bind(this) + ], [ + global.window_group, + 'actor-removed', + this._onWindowActorRemoved.bind(this) + ], [ + global.window_manager, + 'switch-workspace', + this._updateSolidStyle.bind(this) + ], [ + Main.overview, + 'hiding', + this._updateSolidStyle.bind(this) + ], [ + Main.overview, + 'showing', + this._updateSolidStyle.bind(this) + ]); + + // Window signals + global.window_group.get_children().filter(function(child) { + // An irrelevant window actor ('Gnome-shell') produces an error when the signals are + // disconnected, therefore do not add signals to it. + return child instanceof Meta.WindowActor && + child.get_meta_window().get_wm_class() !== 'Gnome-shell'; + }).forEach(function(win) { + this._onWindowActorAdded(null, win); + }, this); + + if (this._actor.get_stage()) + this._updateSolidStyle(); + + this._updateStyles(); + this._updateSolidStyle(); + + this.emit('transparency-enabled'); + } + + disable() { + // ensure I never double-register/inject + // although it should never happen + this._signalsHandler.removeWithLabel('transparency'); + + for (let key of this._trackedWindows.keys()) + this._trackedWindows.get(key).forEach(id => { + key.disconnect(id); + }); + this._trackedWindows.clear(); + + this.emit('transparency-disabled'); + } + + destroy() { + this.disable(); + this._signalsHandler.destroy(); + this._injectionsHandler.destroy(); + } + + _onWindowActorAdded(container, metaWindowActor) { + let signalIds = []; + ['notify::allocation', 'notify::visible'].forEach(s => { + signalIds.push(metaWindowActor.connect(s, this._updateSolidStyle.bind(this))); + }); + this._trackedWindows.set(metaWindowActor, signalIds); + } + + _onWindowActorRemoved(container, metaWindowActor) { + if (!this._trackedWindows.get(metaWindowActor)) + return; + + this._trackedWindows.get(metaWindowActor).forEach(id => { + metaWindowActor.disconnect(id); + }); + this._trackedWindows.delete(metaWindowActor); + this._updateSolidStyle(); + } + + _updateSolidStyle() { + let isNear = this._dockIsNear(); + if (isNear) { + this._backgroundActor.set_style(this._opaque_style); + this._dockActor.remove_style_class_name('transparent'); + this._dockActor.add_style_class_name('opaque'); + } + else { + this._backgroundActor.set_style(this._transparent_style); + this._dockActor.remove_style_class_name('opaque'); + this._dockActor.add_style_class_name('transparent'); + } + + this.emit('solid-style-updated', isNear); + } + + _dockIsNear() { + if (this._dockActor.has_style_pseudo_class('overview')) + return false; + /* Get all the windows in the active workspace that are in the primary monitor and visible */ + let activeWorkspace = global.workspace_manager.get_active_workspace(); + let dash = this._dash; + let windows = activeWorkspace.list_windows().filter(function(metaWindow) { + return metaWindow.get_monitor() === dash._monitorIndex && + metaWindow.showing_on_its_workspace() && + metaWindow.get_window_type() != Meta.WindowType.DESKTOP; + }); + + /* Check if at least one window is near enough to the panel. + * If the dock is hidden, we need to account for the space it would take + * up when it slides out. This is avoid an ugly transition. + * */ + let factor = 0; + if (!Docking.DockManager.settings.get_boolean('dock-fixed') && + this._dock.getDockState() == Docking.State.HIDDEN) + factor = 1; + let [leftCoord, topCoord] = this._actor.get_transformed_position(); + let threshold; + if (this._position === St.Side.LEFT) + threshold = leftCoord + this._actor.get_width() * (factor + 1); + else if (this._position === St.Side.RIGHT) + threshold = leftCoord - this._actor.get_width() * factor; + else if (this._position === St.Side.TOP) + threshold = topCoord + this._actor.get_height() * (factor + 1); + else + threshold = topCoord - this._actor.get_height() * factor; + + let scale = St.ThemeContext.get_for_stage(global.stage).scale_factor; + let isNearEnough = windows.some((metaWindow) => { + let coord; + if (this._position === St.Side.LEFT) { + coord = metaWindow.get_frame_rect().x; + return coord < threshold + 5 * scale; + } + else if (this._position === St.Side.RIGHT) { + coord = metaWindow.get_frame_rect().x + metaWindow.get_frame_rect().width; + return coord > threshold - 5 * scale; + } + else if (this._position === St.Side.TOP) { + coord = metaWindow.get_frame_rect().y; + return coord < threshold + 5 * scale; + } + else { + coord = metaWindow.get_frame_rect().y + metaWindow.get_frame_rect().height; + return coord > threshold - 5 * scale; + } + }); + + return isNearEnough; + } + + _updateStyles() { + this._getAlphas(); + + this._transparent_style = this._base_actor_style + + 'background-color: rgba(' + + this._backgroundColor + ', ' + this._transparentAlpha + ');' + + 'border-color: rgba(' + + this._backgroundColor + ', ' + this._transparentAlphaBorder + ');' + + 'transition-duration: ' + this._transparentTransition + 'ms;'; + + this._opaque_style = this._base_actor_style + + 'background-color: rgba(' + + this._backgroundColor + ', ' + this._opaqueAlpha + ');' + + 'border-color: rgba(' + + this._backgroundColor + ',' + this._opaqueAlphaBorder + ');' + + 'transition-duration: ' + this._opaqueTransition + 'ms;'; + + this.emit('styles-updated'); + } + + setColor(color) { + this._backgroundColor = color.red + ',' + color.green + ',' + color.blue; + this._updateStyles(); + } + + _getAlphas() { + // Create dummy object and add to the uiGroup to get it to the stage + let dummyObject = new St.Bin({ + name: 'dashtodockContainer', + }); + Main.uiGroup.add_child(dummyObject); + + dummyObject.add_style_class_name('dummy-opaque'); + let themeNode = dummyObject.get_theme_node(); + this._opaqueAlpha = themeNode.get_background_color().alpha / 255; + this._opaqueAlphaBorder = themeNode.get_border_color(0).alpha / 255; + this._opaqueTransition = themeNode.get_transition_duration(); + + dummyObject.add_style_class_name('dummy-transparent'); + themeNode = dummyObject.get_theme_node(); + this._transparentAlpha = themeNode.get_background_color().alpha / 255; + this._transparentAlphaBorder = themeNode.get_border_color(0).alpha / 255; + this._transparentTransition = themeNode.get_transition_duration(); + + Main.uiGroup.remove_child(dummyObject); + + let settings = Docking.DockManager.settings; + + if (settings.get_boolean('customize-alphas')) { + this._opaqueAlpha = settings.get_double('max-alpha'); + this._opaqueAlphaBorder = this._opaqueAlpha / 2; + this._transparentAlpha = settings.get_double('min-alpha'); + this._transparentAlphaBorder = this._transparentAlpha / 2; + } + } +}; +Signals.addSignalMethods(Transparency.prototype); diff --git a/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/utils.js b/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/utils.js new file mode 100644 index 0000000..3dd8029 --- /dev/null +++ b/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/utils.js @@ -0,0 +1,308 @@ +const Clutter = imports.gi.Clutter; +const Meta = imports.gi.Meta; +const St = imports.gi.St; + +const Me = imports.misc.extensionUtils.getCurrentExtension(); +const Docking = Me.imports.docking; + +var SignalsHandlerFlags = { + NONE: 0, + CONNECT_AFTER: 1 +}; + +/** + * Simplify global signals and function injections handling + * abstract class + */ +const BasicHandler = class DashToDock_BasicHandler { + + constructor() { + this._storage = new Object(); + } + + add(/* unlimited 3-long array arguments */) { + // Convert arguments object to array, concatenate with generic + // Call addWithLabel with ags as if they were passed arguments + this.addWithLabel('generic', ...arguments); + } + + destroy() { + for( let label in this._storage ) + this.removeWithLabel(label); + } + + addWithLabel(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++) { + let item = this._storage[label]; + try { + item.push(this._create(arguments[i])); + } catch (e) { + logError(e); + } + } + } + + removeWithLabel(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(item) { + throw new GObject.NotImplementedError(`_create in ${this.constructor.name}`); + } + + /** + * Correctly delete single element + */ + _remove(item) { + throw new GObject.NotImplementedError(`_remove in ${this.constructor.name}`); + } +}; + +/** + * Manage global signals + */ +var GlobalSignalsHandler = class DashToDock_GlobalSignalHandler extends BasicHandler { + + _create(item) { + let object = item[0]; + let event = item[1]; + let callback = item[2] + let flags = item.length > 3 ? item[3] : SignalsHandlerFlags.NONE; + + if (!object) + throw new Error('Impossible to connect to an invalid object'); + + let after = flags == SignalsHandlerFlags.CONNECT_AFTER; + let connector = after ? object.connect_after : object.connect; + + if (!connector) { + throw new Error(`Requested to connect to signal '${event}', ` + + `but no implementation for 'connect${after ? '_after' : ''}' `+ + `found in ${object.constructor.name}`); + } + + let id = connector.call(object, event, callback); + + return [object, id]; + } + + _remove(item) { + item[0].disconnect(item[1]); + } +}; + +/** + * Color manipulation utilities + */ +var ColorUtils = class DashToDock_ColorUtils { + + // Darken or brigthen color by a fraction dlum + // Each rgb value is modified by the same fraction. + // Return "#rrggbb" string + static ColorLuminance(r, g, b, dlum) { + let rgbString = '#'; + + rgbString += ColorUtils._decimalToHex(Math.round(Math.min(Math.max(r*(1+dlum), 0), 255)), 2); + rgbString += ColorUtils._decimalToHex(Math.round(Math.min(Math.max(g*(1+dlum), 0), 255)), 2); + rgbString += ColorUtils._decimalToHex(Math.round(Math.min(Math.max(b*(1+dlum), 0), 255)), 2); + + return rgbString; + } + + // Convert decimal to an hexadecimal string adding the desired padding + static _decimalToHex(d, padding) { + let hex = d.toString(16); + while (hex.length < padding) + hex = '0'+ hex; + return hex; + } + + // Convert hsv ([0-1, 0-1, 0-1]) to rgb ([0-255, 0-255, 0-255]). + // Following algorithm in https://en.wikipedia.org/wiki/HSL_and_HSV + // here with h = [0,1] instead of [0, 360] + // Accept either (h,s,v) independently or {h:h, s:s, v:v} object. + // Return {r:r, g:g, b:b} object. + static HSVtoRGB(h, s, v) { + if (arguments.length === 1) { + s = h.s; + v = h.v; + h = h.h; + } + + let r,g,b; + let c = v*s; + let h1 = h*6; + let x = c*(1 - Math.abs(h1 % 2 - 1)); + let m = v - c; + + if (h1 <=1) + r = c + m, g = x + m, b = m; + else if (h1 <=2) + r = x + m, g = c + m, b = m; + else if (h1 <=3) + r = m, g = c + m, b = x + m; + else if (h1 <=4) + r = m, g = x + m, b = c + m; + else if (h1 <=5) + r = x + m, g = m, b = c + m; + else + r = c + m, g = m, b = x + m; + + return { + r: Math.round(r * 255), + g: Math.round(g * 255), + b: Math.round(b * 255) + }; + } + + // Convert rgb ([0-255, 0-255, 0-255]) to hsv ([0-1, 0-1, 0-1]). + // Following algorithm in https://en.wikipedia.org/wiki/HSL_and_HSV + // here with h = [0,1] instead of [0, 360] + // Accept either (r,g,b) independently or {r:r, g:g, b:b} object. + // Return {h:h, s:s, v:v} object. + static RGBtoHSV(r, g, b) { + if (arguments.length === 1) { + r = r.r; + g = r.g; + b = r.b; + } + + let h,s,v; + + let M = Math.max(r, g, b); + let m = Math.min(r, g, b); + let c = M - m; + + if (c == 0) + h = 0; + else if (M == r) + h = ((g-b)/c) % 6; + else if (M == g) + h = (b-r)/c + 2; + else + h = (r-g)/c + 4; + + h = h/6; + v = M/255; + if (M !== 0) + s = c/M; + else + s = 0; + + return { + h: h, + s: s, + v: v + }; + } +}; + +/** + * Manage function injection: both instances and prototype can be overridden + * and restored + */ +var InjectionsHandler = class DashToDock_InjectionsHandler extends BasicHandler { + + _create(item) { + let object = item[0]; + let name = item[1]; + let injectedFunction = item[2]; + let original = object[name]; + + object[name] = injectedFunction; + return [object, name, injectedFunction, original]; + } + + _remove(item) { + let object = item[0]; + let name = item[1]; + let original = item[3]; + object[name] = original; + } +}; + +/** + * Return the actual position reverseing left and right in rtl + */ +function getPosition() { + let position = Docking.DockManager.settings.get_enum('dock-position'); + if (Clutter.get_default_text_direction() == Clutter.TextDirection.RTL) { + if (position == St.Side.LEFT) + position = St.Side.RIGHT; + else if (position == St.Side.RIGHT) + position = St.Side.LEFT; + } + return position; +} + +function drawRoundedLine(cr, x, y, width, height, isRoundLeft, isRoundRight, stroke, fill) { + if (height > width) { + y += Math.floor((height - width) / 2.0); + height = width; + } + + height = 2.0 * Math.floor(height / 2.0); + + var leftRadius = isRoundLeft ? height / 2.0 : 0.0; + var rightRadius = isRoundRight ? height / 2.0 : 0.0; + + cr.moveTo(x + width - rightRadius, y); + cr.lineTo(x + leftRadius, y); + if (isRoundLeft) + cr.arcNegative(x + leftRadius, y + leftRadius, leftRadius, -Math.PI/2, Math.PI/2); + else + cr.lineTo(x, y + height); + cr.lineTo(x + width - rightRadius, y + height); + if (isRoundRight) + cr.arcNegative(x + width - rightRadius, y + rightRadius, rightRadius, Math.PI/2, -Math.PI/2); + else + cr.lineTo(x + width, y); + cr.closePath(); + + if (fill != null) { + cr.setSource(fill); + cr.fillPreserve(); + } + if (stroke != null) + cr.setSource(stroke); + cr.stroke(); +} + +/** + * Convert a signal handler with n value parameters (that is, excluding the + * signal source parameter) to an array of n handlers that are each responsible + * for receiving one of the n values and calling the original handler with the + * most up-to-date arguments. + */ +function splitHandler(handler) { + if (handler.length > 30) { + throw new Error("too many parameters"); + } + const count = handler.length - 1; + let missingValueBits = (1 << count) - 1; + const values = Array.from({ length: count }); + return values.map((_ignored, i) => { + const mask = ~(1 << i); + return (obj, value) => { + values[i] = value; + missingValueBits &= mask; + if (missingValueBits === 0) { + handler(obj, ...values); + } + }; + }); +} diff --git a/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/windowPreview.js b/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/windowPreview.js new file mode 100644 index 0000000..8cb14b8 --- /dev/null +++ b/src/other/dash-to-dock/dash-to-dock@micxgx.gmail.com/windowPreview.js @@ -0,0 +1,598 @@ +/* + * Credits: + * This file is based on code from the Dash to Panel extension by Jason DeRose + * and code from the Taskbar extension by Zorin OS + * Some code was also adapted from the upstream Gnome Shell source code. + */ +const Clutter = imports.gi.Clutter; +const GLib = imports.gi.GLib; +const GObject = imports.gi.GObject; +const St = imports.gi.St; +const Main = imports.ui.main; + +const Params = imports.misc.params; +const PopupMenu = imports.ui.popupMenu; +const Workspace = imports.ui.workspace; + +const Me = imports.misc.extensionUtils.getCurrentExtension(); +const Utils = Me.imports.utils; + +const PREVIEW_MAX_WIDTH = 250; +const PREVIEW_MAX_HEIGHT = 150; + +const PREVIEW_ANIMATION_DURATION = 250; + +var WindowPreviewMenu = class DashToDock_WindowPreviewMenu extends PopupMenu.PopupMenu { + + constructor(source) { + let side = Utils.getPosition(); + super(source, 0.5, side); + + // We want to keep the item hovered while the menu is up + this.blockSourceEvents = true; + + this._source = source; + this._app = this._source.app; + let monitorIndex = this._source.monitorIndex; + + this.actor.add_style_class_name('app-well-menu'); + this.actor.set_style('max-width: ' + (Main.layoutManager.monitors[monitorIndex].width - 22) + 'px; ' + + 'max-height: ' + (Main.layoutManager.monitors[monitorIndex].height - 22) + 'px;'); + this.actor.hide(); + + // Chain our visibility and lifecycle to that of the source + this._mappedId = this._source.connect('notify::mapped', () => { + if (!this._source.mapped) + this.close(); + }); + this._destroyId = this._source.connect('destroy', this.destroy.bind(this)); + + Main.uiGroup.add_actor(this.actor); + + // Change the initialized side where required. + this._arrowSide = side; + this._boxPointer._arrowSide = side; + this._boxPointer._userArrowSide = side; + + this.connect('destroy', this._onDestroy.bind(this)); + } + + _redisplay() { + if (this._previewBox) + this._previewBox.destroy(); + this._previewBox = new WindowPreviewList(this._source); + this.addMenuItem(this._previewBox); + this._previewBox._redisplay(); + } + + popup() { + let windows = this._source.getInterestingWindows(); + if (windows.length > 0) { + this._redisplay(); + this.open(); + this.actor.navigate_focus(null, St.DirectionType.TAB_FORWARD, false); + this._source.emit('sync-tooltip'); + } + } + + _onDestroy() { + if (this._mappedId) + this._source.disconnect(this._mappedId); + + if (this._destroyId) + this._source.disconnect(this._destroyId); + } +}; + +var WindowPreviewList = class DashToDock_WindowPreviewList extends PopupMenu.PopupMenuSection { + + constructor(source) { + super(); + this.actor = new St.ScrollView({ + name: 'dashtodockWindowScrollview', + hscrollbar_policy: St.PolicyType.NEVER, + vscrollbar_policy: St.PolicyType.NEVER, + enable_mouse_scrolling: true + }); + + this.actor.connect('scroll-event', this._onScrollEvent.bind(this)); + + let position = Utils.getPosition(); + this.isHorizontal = position == St.Side.BOTTOM || position == St.Side.TOP; + this.box.set_vertical(!this.isHorizontal); + this.box.set_name('dashtodockWindowList'); + this.actor.add_actor(this.box); + this.actor._delegate = this; + + this._shownInitially = false; + + this._source = source; + this.app = source.app; + + this._redisplayId = Main.initializeDeferredWork(this.actor, this._redisplay.bind(this)); + + this.actor.connect('destroy', this._onDestroy.bind(this)); + this._stateChangedId = this.app.connect('windows-changed', + this._queueRedisplay.bind(this)); + } + + _queueRedisplay () { + Main.queueDeferredWork(this._redisplayId); + } + + _onScrollEvent(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; + + // Skip to avoid double events mouse + if (event.is_pointer_emulated()) + return Clutter.EVENT_STOP; + + let adjustment, delta; + + if (this.isHorizontal) + adjustment = this.actor.get_hscroll_bar().get_adjustment(); + else + adjustment = this.actor.get_vscroll_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; + } + + _onDestroy() { + this.app.disconnect(this._stateChangedId); + this._stateChangedId = 0; + } + + _createPreviewItem(window) { + let preview = new WindowPreviewMenuItem(window); + return preview; + } + + _redisplay () { + let children = this._getMenuItems().filter(function(actor) { + return actor._window; + }); + + // Windows currently on the menu + let oldWin = children.map(function(actor) { + return actor._window; + }); + + // All app windows with a static order + let newWin = this._source.getInterestingWindows().sort(function(a, b) { + return a.get_stable_sequence() > b.get_stable_sequence(); + }); + + 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] && + 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]; + if (this._shownInitially) + item._animateOutAndDestroy(); + else + item.actor.destroy(); + } + + // 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(); + + if (newWin.length < 1) + this._getTopMenu().close(~0); + + // As for upstream: + // St.ScrollView always requests space horizontally for a possible vertical + // scrollbar if in AUTOMATIC mode. Doing better would require implementation + // of width-for-height in St.BoxLayout and St.ScrollView. 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. + let needsScrollbar = this._needsScrollbar(); + let scrollbar_policy = needsScrollbar ? + St.PolicyType.AUTOMATIC : St.PolicyType.NEVER; + if (this.isHorizontal) + this.actor.hscrollbar_policy = scrollbar_policy; + else + this.actor.vscrollbar_policy = scrollbar_policy; + + if (needsScrollbar) + this.actor.add_style_pseudo_class('scrolled'); + else + this.actor.remove_style_pseudo_class('scrolled'); + } + + _needsScrollbar() { + let topMenu = this._getTopMenu(); + let topThemeNode = topMenu.actor.get_theme_node(); + if (this.isHorizontal) { + let [topMinWidth, topNaturalWidth] = topMenu.actor.get_preferred_width(-1); + let topMaxWidth = topThemeNode.get_max_width(); + return topMaxWidth >= 0 && topNaturalWidth >= topMaxWidth; + } else { + let [topMinHeight, topNaturalHeight] = topMenu.actor.get_preferred_height(-1); + let topMaxHeight = topThemeNode.get_max_height(); + return topMaxHeight >= 0 && topNaturalHeight >= topMaxHeight; + } + + } + + isAnimatingOut() { + return this.actor.get_children().reduce(function(result, actor) { + return result || actor.animatingOut; + }, false); + } +}; + +var WindowPreviewMenuItem = GObject.registerClass( +class DashToDock_WindowPreviewMenuItem extends PopupMenu.PopupBaseMenuItem { + _init(window, params) { + super._init(params); + + this._window = window; + this._destroyId = 0; + this._windowAddedId = 0; + [this._width, this._height, this._scale] = this._getWindowPreviewSize(); // This gets the actual windows size for the preview + + // We don't want this: it adds spacing on the left of the item. + this.remove_child(this._ornamentLabel); + this.add_style_class_name('dashtodock-app-well-preview-menu-item'); + + // Now we don't have to set PREVIEW_MAX_WIDTH and PREVIEW_MAX_HEIGHT as preview size - that made all kinds of windows either stretched or squished (aspect ratio problem) + this._cloneBin = new St.Bin(); + this._cloneBin.set_size(this._width*this._scale, this._height*this._scale); + + // TODO: improve the way the closebutton is layout. Just use some padding + // for the moment. + this._cloneBin.set_style('padding-bottom: 0.5em'); + + this.closeButton = new St.Button({ style_class: 'window-close', + x_expand: true, + y_expand: true}); + this.closeButton.add_actor(new St.Icon({ icon_name: 'window-close-symbolic' })); + this.closeButton.set_x_align(Clutter.ActorAlign.END); + this.closeButton.set_y_align(Clutter.ActorAlign.START); + + + this.closeButton.opacity = 0; + this.closeButton.connect('clicked', this._closeWindow.bind(this)); + + let overlayGroup = new Clutter.Actor({layout_manager: new Clutter.BinLayout(), y_expand: true }); + + overlayGroup.add_actor(this._cloneBin); + overlayGroup.add_actor(this.closeButton); + + let label = new St.Label({ text: window.get_title()}); + label.set_style('max-width: '+PREVIEW_MAX_WIDTH +'px'); + let labelBin = new St.Bin({ child: label, + x_align: Clutter.ActorAlign.CENTER, + }); + + this._windowTitleId = this._window.connect('notify::title', () => { + label.set_text(this._window.get_title()); + }); + + let box = new St.BoxLayout({ vertical: true, + reactive:true, + x_expand:true }); + box.add(overlayGroup); + box.add(labelBin); + this.add_actor(box); + + this._cloneTexture(window); + + this.connect('destroy', this._onDestroy.bind(this)); + } + + _getWindowPreviewSize() { + let mutterWindow = this._window.get_compositor_private(); + let [width, height] = mutterWindow.get_size(); + let scale = Math.min(1.0, PREVIEW_MAX_WIDTH/width, PREVIEW_MAX_HEIGHT/height); + return [width, height, scale]; + } + + _cloneTexture(metaWin){ + + let mutterWindow = metaWin.get_compositor_private(); + + // Newly-created windows are added to a workspace before + // the compositor finds out about them... + // Moreover sometimes they return an empty texture, thus as a workarounf also check for it size + if (!mutterWindow || !mutterWindow.get_texture() || !mutterWindow.get_size()[0]) { + this._cloneTextureId = GLib.idle_add(GLib.PRIORITY_DEFAULT, () => { + // Check if there's still a point in getting the texture, + // otherwise this could go on indefinitely + if (metaWin.get_workspace()) + this._cloneTexture(metaWin); + this._cloneTextureId = 0; + return GLib.SOURCE_REMOVE; + }); + GLib.Source.set_name_by_id(this._cloneTextureId, '[dash-to-dock] this._cloneTexture'); + return; + } + + let clone = new Clutter.Clone ({ source: mutterWindow, + reactive: true, + width: this._width * this._scale, + height: this._height * this._scale }); + + // when the source actor is destroyed, i.e. the window closed, first destroy the clone + // and then destroy the menu item (do this animating out) + this._destroyId = mutterWindow.connect('destroy', () => { + clone.destroy(); + this._destroyId = 0; // avoid to try to disconnect this signal from mutterWindow in _onDestroy(), + // as the object was just destroyed + this._animateOutAndDestroy(); + }); + + this._clone = clone; + this._mutterWindow = mutterWindow; + this._cloneBin.set_child(this._clone); + + this._clone.connect('destroy', () => { + if (this._destroyId) { + mutterWindow.disconnect(this._destroyId); + this._destroyId = 0; + } + this._clone = null; + }) + } + + _windowCanClose() { + return this._window.can_close() && + !this._hasAttachedDialogs(); + } + + _closeWindow(actor) { + this._workspace = this._window.get_workspace(); + + // This mechanism is copied from the workspace.js upstream code + // It forces window activation if the windows don't get closed, + // for instance because asking user confirmation, by monitoring the opening of + // such additional confirmation window + this._windowAddedId = this._workspace.connect('window-added', + this._onWindowAdded.bind(this)); + + this.deleteAllWindows(); + } + + deleteAllWindows() { + // Delete all windows, starting from the bottom-most (most-modal) one + //let windows = this._window.get_compositor_private().get_children(); + let windows = this._clone.get_children(); + for (let i = windows.length - 1; i >= 1; i--) { + let realWindow = windows[i].source; + let metaWindow = realWindow.meta_window; + + metaWindow.delete(global.get_current_time()); + } + + this._window.delete(global.get_current_time()); + } + + _onWindowAdded(workspace, win) { + let metaWindow = this._window; + + if (win.get_transient_for() == metaWindow) { + workspace.disconnect(this._windowAddedId); + this._windowAddedId = 0; + + // use an idle handler to avoid mapping problems - + // see comment in Workspace._windowAdded + let activationEvent = Clutter.get_current_event(); + let id = GLib.idle_add(GLib.PRIORITY_DEFAULT, () => { + this.emit('activate', activationEvent); + return GLib.SOURCE_REMOVE; + }); + GLib.Source.set_name_by_id(id, '[dash-to-dock] this.emit'); + } + } + + _hasAttachedDialogs() { + // count trasient windows + let n=0; + this._window.foreach_transient(function(){n++;}); + return n>0; + } + + vfunc_key_focus_in() { + super.vfunc_key_focus_in(); + this._showCloseButton(); + } + + vfunc_key_focus_out() { + super.vfunc_key_focus_out(); + this._hideCloseButton(); + } + + vfunc_enter_event(crossingEvent) { + this._showCloseButton(); + return super.vfunc_enter_event(crossingEvent); + } + + vfunc_leave_event(crossingEvent) { + this._hideCloseButton(); + return super.vfunc_leave_event(crossingEvent); + } + + _idleToggleCloseButton() { + this._idleToggleCloseId = 0; + + this._hideCloseButton(); + + return GLib.SOURCE_REMOVE; + } + + _showCloseButton() { + + if (this._windowCanClose()) { + this.closeButton.show(); + this.closeButton.remove_all_transitions(); + this.closeButton.ease({ + opacity: 255, + duration: Workspace.WINDOW_OVERLAY_FADE_TIME, + mode: Clutter.AnimationMode.EASE_OUT_QUAD + }); + } + } + + _hideCloseButton() { + if (this.closeButton.has_pointer || + this.get_children().some(a => a.has_pointer)) + return; + + this.closeButton.remove_all_transitions(); + this.closeButton.ease({ + opacity: 0, + duration: Workspace.WINDOW_OVERLAY_FADE_TIME, + mode: Clutter.AnimationMode.EASE_IN_QUAD + }); + } + + show(animate) { + let fullWidth = this.get_width(); + + this.opacity = 0; + this.set_width(0); + + let time = animate ? PREVIEW_ANIMATION_DURATION : 0; + this.remove_all_transitions(); + this.ease({ + opacity: 255, + width: fullWidth, + duration: time, + mode: Clutter.AnimationMode.EASE_IN_OUT_QUAD, + }); + } + + _animateOutAndDestroy() { + this.remove_all_transitions(); + this.ease({ + opacity: 0, + duration: PREVIEW_ANIMATION_DURATION, + }); + + this.ease({ + width: 0, + height: 0, + duration: PREVIEW_ANIMATION_DURATION, + delay: PREVIEW_ANIMATION_DURATION, + onComplete: () => this.destroy() + }); + } + + activate() { + this._getTopMenu().close(); + Main.activateWindow(this._window); + } + + _onDestroy() { + if (this._cloneTextureId) { + GLib.source_remove(this._cloneTextureId); + this._cloneTextureId = 0; + } + + if (this._windowAddedId > 0) { + this._workspace.disconnect(this._windowAddedId); + this._windowAddedId = 0; + } + + if (this._destroyId > 0) { + this._mutterWindow.disconnect(this._destroyId); + this._destroyId = 0; + } + + if (this._windowTitleId > 0) { + this._window.disconnect(this._windowTitleId); + this._windowTitleId = 0; + } + } +}); \ No newline at end of file diff --git a/tweaks.sh b/tweaks.sh index 37f03a1..f273c09 100755 --- a/tweaks.sh +++ b/tweaks.sh @@ -15,6 +15,10 @@ readonly REPO_DIR="$(dirname "$(readlink -m "${0}")")" source "${REPO_DIR}/lib-install.sh" +# Customization, default values +colors=("${COLOR_VARIANTS[@]}") +opacities=("${OPACITY_VARIANTS[@]}") + usage() { # Please specify their default value manually, some of them are come from _variables.scss # You also have to check and update them regurally @@ -24,7 +28,7 @@ usage() { helpify "-F, --flatpak" "" "Connect '${THEME_NAME}' theme to Flatpak" "" helpify "-s, --snap" "" "Connect '${THEME_NAME}' theme the currently installed snap apps" "" helpify "-g, --gdm" "" "Install '${THEME_NAME}' theme for GDM" "Requires to run this shell as root" - helpify "-d, --dash-to-dock" "" "Install '${THEME_NAME}' theme for Dash to Dock and connect it to the current Dash to Dock installation(s)" "" + helpify "-d, --dash-to-dock" "" "Install '${THEME_NAME}' theme for Dash to Dock when Gnome < 40 or install fixed version on Gnome > 40" "" helpify "-N, --no-darken" "" "Don't darken '${THEME_NAME}' GDM theme background image" "" helpify "-n, --no-blur" "" "Don't blur '${THEME_NAME}' GDM theme background image" "" helpify "-b, --background" "[default|blank|IMAGE_PATH]" "Set '${THEME_NAME}' GDM theme background image" "Default is BigSur-like wallpaper" @@ -98,7 +102,7 @@ while [[ $# -gt 0 ]]; do has_any_error="true" fi; shift ;; -F|--flatpak) - flatpak="true"; + flatpak="true"; signal_exit if ! has_command flatpak; then prompt -e "'${1}' ERROR: There's no Flatpak installed in your system" @@ -122,8 +126,8 @@ while [[ $# -gt 0 ]]; do fi; shift ;; -d|--dash-to-dock) if [[ "${GNOME_VERSION}" == 'new' ]]; then - prompt -w "'${1}' WARNING: There's no need to install on GNOME >= 40.0" - dash_to_dock="false" + prompt -w "'${1}' It will install a fixed version on GNOME-SHELL >= 40.0" + dash_to_dock="new" elif [[ ! -d "${DASH_TO_DOCK_DIR_HOME}" && ! -d "${DASH_TO_DOCK_DIR_ROOT}" ]]; then prompt -e "'${1}' ERROR: There's no Dash to Dock installed in your system" has_any_error="true" @@ -163,31 +167,37 @@ if [[ "${uninstall}" == 'true' ]]; then prompt -w "REMOVAL: Non file-related parameters will be ignored." if [[ "${gdm}" == 'true' ]]; then - prompt -i "Removing '${name}' GDM theme..." + prompt -i "Removing '${name}' GDM theme... \n" revert_gdm_theme prompt -s "Done! '${name}' GDM theme has been removed."; echo fi if [[ "${dash_to_dock}" == 'true' ]]; then - prompt -i "Removing '${name}' Dash to Dock theme..." + prompt -i "Removing '${name}' Dash to Dock theme... \n" revert_dash_to_dock_theme prompt -s "Done! '${name}' Dash to Dock theme has been removed."; echo fi + if [[ "${dash_to_dock}" == 'new' ]]; then + prompt -i "Removing '${name}' Dash to Dock extension... \n" + revert_dash_to_dock + prompt -s "Done! '${name}' Dash to Dock extension has been removed."; echo + fi + if [[ "${firefox}" == 'true' ]]; then - prompt -i "Removing '${name}' Firefox theme..." + prompt -i "Removing '${name}' Firefox theme... \n" remove_firefox_theme prompt -s "Done! '${name}' Firefox theme has been removed."; echo fi if [[ "${snap}" == 'true' ]]; then - prompt -i "Disconnecting '${name}' theme from your installed snap apps..." + prompt -i "Disconnecting '${name}' theme from your installed snap apps... \n" disconnect_snap prompt -s "Done! '${name}' theme has been disconnected from your snap apps."; echo fi if [[ "${flatpak}" == 'true' ]]; then - prompt -i "Disconnecting '${name}' theme from your Flatpak..." + prompt -i "Disconnecting '${name}' theme from your Flatpak... \n" disconnect_flatpak prompt -s "Done! '${name}' theme has been disconnected from your Flatpak."; echo fi @@ -195,27 +205,34 @@ else show_needed_dialogs; customize_theme if [[ "${gdm}" == 'true' ]]; then - prompt -i "Installing '${name}' GDM theme..." + prompt -i "Installing '${name}' GDM theme... \n" install_gdm_theme prompt -s "Done! '${name}' GDM theme has been installed."; echo fi if [[ "${dash_to_dock}" == 'true' ]]; then - prompt -i "Installing '${name}' ${colors[0]} Dash to Dock theme..." + prompt -i "Installing '${name}' ${colors[0]} Dash to Dock theme... \n" install_dash_to_dock_theme prompt -s "Done! '${name}' Dash to Dock theme has been installed." prompt -w "DASH TO DOCK: You may need to logout to take effect."; echo fi + if [[ "${dash_to_dock}" == 'new' ]]; then + prompt -i "Installing fixed Dash to Dock... \n" + install_dash_to_dock + prompt -s "Done! '${name}' Dash to Dock extension has been installed." + prompt -w "DASH TO DOCK: You may need to logout to take effect."; echo + fi + if [[ "${firefox}" == 'true' || "${edit_firefox}" == 'true' ]]; then if [[ "${firefox}" == 'true' ]]; then - prompt -i "Installing '${name}' Firefox theme..." + prompt -i "Installing '${name}' Firefox theme... \n" install_firefox_theme prompt -s "Done! '${name}' Firefox theme has been installed."; echo fi if [[ "${edit_firefox}" == 'true' ]]; then - prompt -i "Editing '${name}' Firefox theme preferences..." + prompt -i "Editing '${name}' Firefox theme preferences... \n" edit_firefox_theme_prefs prompt -s "Done! '${name}' Firefox theme preferences has been edited."; echo fi @@ -226,13 +243,13 @@ else fi if [[ "${snap}" == 'true' ]]; then - prompt -i "Connecting '${name}' theme to your installed snap apps..." + prompt -i "Connecting '${name}' theme to your installed snap apps... \n" connect_snap prompt -s "Done! '${name}' theme has been connected to your snap apps."; echo fi if [[ "${flatpak}" == 'true' ]]; then - prompt -i "Connecting '${name}' theme to your Flatpak..." + prompt -i "Connecting '${name}' themes to your Flatpak... \n" connect_flatpak prompt -s "Done! '${name}' theme has been connected to your Flatpak."; echo fi