diff options
Diffstat (limited to 'subprojects')
117 files changed, 16104 insertions, 0 deletions
diff --git a/subprojects/extensions-app/COPYING b/subprojects/extensions-app/COPYING new file mode 100644 index 0000000..d159169 --- /dev/null +++ b/subprojects/extensions-app/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. + + <one line to give the program's name and a brief idea of what it does.> + Copyright (C) <year> <name of author> + + 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. + + <signature of Ty Coon>, 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/subprojects/extensions-app/README.md b/subprojects/extensions-app/README.md new file mode 100644 index 0000000..6c0feb6 --- /dev/null +++ b/subprojects/extensions-app/README.md @@ -0,0 +1,32 @@ +# ![logo] GNOME Extensions +GNOME Extensions is a small application for managing GNOME Shell +extensions. It is usually built as part of gnome-shell, but can be +used as a stand-alone project as well. + +Bugs should be reported to the GNOME [bug tracking system][bug-tracker]. + +## Installation +If Extensions is not already installed on your GNOME system, we +recommend getting it from [flathub]. + +<a href='https://flathub.org/apps/details/org.gnome.Extensions'> + <img width='240' alt='Download on Flathub' src='https://flathub.org/assets/badges/flathub-badge-en.png'/> +</a> + +## Building +Before the project can be built stand-alone, the po directory has +to be populated with translations (from gnome-shell). + +To do that, simply run the included script: +```sh +$ ./generate-translations.sh +``` + +## License +gnome-extensions-app is distributed under the terms of the GNU General Public +License, version 2 or later. See the [COPYING][license] file for details. + +[logo]: logo.png +[bug-tracker]: https://gitlab.gnome.org/GNOME/gnome-shell/issues +[flathub]: https://flathub.org +[license]: COPYING diff --git a/subprojects/extensions-app/build-aux/flatpak/org.gnome.Extensions.json b/subprojects/extensions-app/build-aux/flatpak/org.gnome.Extensions.json new file mode 100644 index 0000000..3ac00f6 --- /dev/null +++ b/subprojects/extensions-app/build-aux/flatpak/org.gnome.Extensions.json @@ -0,0 +1,39 @@ +{ + "app-id": "org.gnome.Extensions.Devel", + "runtime": "org.gnome.Platform", + "runtime-version": "master", + "sdk": "org.gnome.Sdk", + "command": "gnome-extensions-app", + "tags": ["nightly"], + "finish-args": [ + "--share=ipc", "--socket=fallback-x11", + "--socket=wayland", + "--device=dri", + "--talk-name=org.gnome.SessionManager", + "--talk-name=org.gnome.Shell.Extensions" + ], + "build-options": { + "cflags": "-O2 -g" + }, + "modules": [ + { + "name": "gnome-extensions-app", + "buildsystem": "meson", + "builddir": true, + "subdir": "subprojects/extensions-app", + "config-opts": ["-Dprofile=development"], + "sources": [ + { + "type": "git", + "url": "https://gitlab.gnome.org/GNOME/gnome-shell.git" + }, + { + "type": "shell", + "commands": [ + "subprojects/extensions-app/generate-translations.sh" + ] + } + ] + } + ] +} diff --git a/subprojects/extensions-app/build-aux/meson/check-version.py b/subprojects/extensions-app/build-aux/meson/check-version.py new file mode 120000 index 0000000..fbe6c74 --- /dev/null +++ b/subprojects/extensions-app/build-aux/meson/check-version.py @@ -0,0 +1 @@ +../../../../meson/check-version.py
\ No newline at end of file diff --git a/subprojects/extensions-app/data/css/style.css b/subprojects/extensions-app/data/css/style.css new file mode 100644 index 0000000..dac7633 --- /dev/null +++ b/subprojects/extensions-app/data/css/style.css @@ -0,0 +1,21 @@ +row.extension>box { + margin: 12px; +} + +row.extension>box, +row.extension box.header { + border-spacing: 12px; +} + +row.extension box.actions, +row.extension box.actions>box { + border-spacing: 6px; +} + +row.extension box.information, +row.extension box.status { + border-spacing: 3px; +} + +image.error { color: @error_color; } +image.warning { color: @warning_color; } diff --git a/subprojects/extensions-app/data/dbus-interfaces/org.gnome.Shell.Extensions.xml b/subprojects/extensions-app/data/dbus-interfaces/org.gnome.Shell.Extensions.xml new file mode 120000 index 0000000..defde79 --- /dev/null +++ b/subprojects/extensions-app/data/dbus-interfaces/org.gnome.Shell.Extensions.xml @@ -0,0 +1 @@ +../../../../data/dbus-interfaces/org.gnome.Shell.Extensions.xml
\ No newline at end of file diff --git a/subprojects/extensions-app/data/icons/hicolor/scalable/apps/org.gnome.Extensions.Devel.svg b/subprojects/extensions-app/data/icons/hicolor/scalable/apps/org.gnome.Extensions.Devel.svg new file mode 100644 index 0000000..60c1018 --- /dev/null +++ b/subprojects/extensions-app/data/icons/hicolor/scalable/apps/org.gnome.Extensions.Devel.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="128" height="128"><defs><clipPath id="a"><path d="M0 0h128v128H0z"/></clipPath><clipPath id="c"><path d="M0 0h128v128H0z"/></clipPath><clipPath id="i"><path d="M0 0h128v128H0z"/></clipPath><clipPath id="g"><path d="M0 0h128v128H0z"/></clipPath><g id="m" clip-path="url(#i)"><use xlink:href="#j" mask="url(#k)"/></g><g id="e" clip-path="url(#a)"><path d="M21.965 18.363v4l2.039 17.75c.418 3.637 1.055 7.282.844 10.938-.106 1.828-.465 3.668-1.309 5.293-.426.812-.969 1.57-1.644 2.191-.676.621-1.485 1.106-2.364 1.356-1.047.297-2.156.261-3.238.164-1.094-.098-2.227-.282-3.137-.903-.394-.273-.73-.617-1.082-.945-.351-.328-.726-.645-1.168-.832-.691-.297-1.492-.254-2.207-.023-.715.23-1.355.64-1.972 1.066-1.188.824-2.325 1.758-3.184 2.922-1.734 2.344-2.203 5.46-1.711 8.336.367 2.152 1.258 4.238 2.688 5.89.722.836 1.582 1.563 2.566 2.063.98.504 2.09.773 3.195.71 1.446-.085 2.79-.73 4.114-1.327 1.32-.594 2.718-1.16 4.168-1.067a5.113 5.113 0 012.332.758 5.958 5.958 0 011.773 1.707c.848 1.242 1.234 2.742 1.531 4.219 1.832 9.133.778 18.82-2.98 27.344v4c6.812 2.347 13.8 4.199 20.886 5.535 2.356.441 4.801.824 7.13.246 1.163-.29 2.28-.832 3.148-1.66a5.39 5.39 0 001.441-2.34c.266-.887.278-1.828.254-2.754L54 110c-.426.09-.875.059-1.285-.082a2.751 2.751 0 01-1.16-.8 3.768 3.768 0 01-.7-1.231c-.316-.895-.351-1.864-.355-2.813-.008-1.57.063-3.187.684-4.629.629-1.465 1.793-2.652 3.117-3.535 2.64-1.758 5.898-2.375 9.07-2.414 2.91-.035 5.918.414 8.387 1.953 1.238.77 2.316 1.809 3.043 3.07.722 1.262 1.078 2.739.953 4.188-.098 1.117-.477 2.2-1.02 3.18A9.862 9.862 0 0172 110c.012.996.02 1.992.02 2.988.003.555.003 1.114.07 1.664.07.551.21 1.098.476 1.586.383.7 1.012 1.239 1.692 1.657.68.414 1.422.722 2.176.984 4.207 1.46 8.793 1.562 13.203.922a40.756 40.756 0 0015.86-5.88l-.005-4.003a60.146 60.146 0 01-3.433-25.465c.125-1.512.312-3.039.894-4.441.29-.7.68-1.364 1.18-1.93.504-.566 1.12-1.039 1.82-1.332.82-.344 1.727-.434 2.613-.367.887.07 1.75.297 2.598.57 1.688.547 3.344 1.297 5.117 1.418 1.492.098 2.996-.266 4.332-.937a9.894 9.894 0 004.016-3.715 9.91 9.91 0 001.418-5.285l.176-2.258a16.406 16.406 0 00-3.075-6.477c-.785-.996-1.707-1.914-2.835-2.496a5.247 5.247 0 00-1.805-.558 4.18 4.18 0 00-1.875.214c-.988.364-1.774 1.125-2.703 1.614-1.032.543-2.203.738-3.364.824-1.148.086-2.324.07-3.425-.274-1.48-.464-2.743-1.52-3.602-2.812-.855-1.293-1.332-2.805-1.59-4.328-.574-3.399-.117-6.871.266-10.297a227.56 227.56 0 001.34-19.973 497.333 497.333 0 01-21.118-5.117c-1.355-.36-2.718-.726-4.113-.894-1.394-.165-2.836-.118-4.168.332a7.108 7.108 0 00-2.93 1.894v4c0 .414.016.828.11 1.235.094.402.258.789.445 1.156.371.738.82 1.433 1.313 2.097.98 1.329 2.136 2.563 2.765 4.094.688 1.676.668 3.606.07 5.317-.597 1.707-1.75 3.203-3.179 4.312-1.43 1.113-3.133 1.852-4.895 2.262-1.765.414-3.593.508-5.402.406-2.926-.164-5.906-.875-8.25-2.633-1.172-.883-2.164-2.015-2.793-3.34-.625-1.324-.879-2.828-.668-4.277.293-1.973 1.399-3.71 2.45-5.406.421-.684.831-1.38 1.078-2.14.242-.763.296-1.571.308-2.372L54.633 18a1.89 1.89 0 01-1.695.777c-.516-.047-.989-.3-1.41-.601-.419-.301-.801-.653-1.231-.938-.871-.578-1.914-.855-2.957-.933-1.043-.075-2.09.039-3.125.191-2.403.356-4.77.91-7.176 1.266-2.402.351-4.832.46-7.262.496zm0 0" fill="url(#b)"/><path d="M21.965 18.363l2.039 17.746c.426 3.715 1.16 7.414 1.094 11.149-.035 1.867-.34 3.77-1.239 5.41-.449.816-1.047 1.562-1.785 2.133-.738.574-1.617.972-2.543 1.09-1.504.195-2.996-.344-4.379-.961-1.379-.621-2.75-1.344-4.246-1.555-1.465-.21-2.984.094-4.293.777-1.308.688-2.406 1.746-3.207 2.989-1.601 2.484-1.972 5.605-1.574 8.535.3 2.219 1.05 4.433 2.512 6.133.73.847 1.633 1.558 2.656 2.011 1.023.457 2.168.657 3.281.52 1.54-.192 2.926-1 4.324-1.676.704-.336 1.422-.644 2.176-.828.754-.184 1.551-.238 2.309-.066.82.183 1.566.628 2.164 1.218.601.586 1.05 1.313 1.375 2.086.64 1.551.762 3.258.84 4.934a123.2 123.2 0 01-2.25 29.965 133.42 133.42 0 0020.886 5.53c2.352.427 4.793.782 7.11.196 1.16-.293 2.27-.828 3.152-1.633.883-.804 1.528-1.89 1.711-3.074.133-.879.012-1.789-.25-2.64-.262-.852-.668-1.657-1.101-2.438-.868-1.559-1.887-3.086-2.227-4.84-.3-1.558-.023-3.207.676-4.633.703-1.425 1.816-2.629 3.12-3.535 2.614-1.804 5.9-2.383 9.075-2.41 2.91-.023 5.918.414 8.39 1.953 1.235.77 2.313 1.809 3.04 3.07.73 1.258 1.097 2.743.953 4.188-.117 1.113-.524 2.18-1.024 3.18-.5 1.004-1.093 1.957-1.609 2.949-.512.996-.95 2.047-1.11 3.152-.156 1.11-.019 2.29.555 3.25.41.684 1.02 1.23 1.696 1.649.675.422 1.418.722 2.168.992a30.23 30.23 0 0015.183 1.332 30.128 30.128 0 0013.88-6.293l-2.587-19.371c-.453-3.406-1.14-6.813-1.047-10.246.043-1.719.34-3.461 1.141-4.98a6.796 6.796 0 011.578-2.02 5.338 5.338 0 012.262-1.18c.8-.183 1.637-.164 2.441-.02.809.145 1.59.41 2.352.708 1.527.597 3.023 1.34 4.648 1.558 1.5.203 3.055-.062 4.426-.699 1.375-.64 2.566-1.648 3.469-2.863 1.808-2.434 2.418-5.625 2.047-8.63-.305-2.448-1.258-4.862-2.953-6.655-.848-.895-1.875-1.63-3.016-2.098-1.14-.473-2.395-.684-3.621-.563-1.637.157-3.153.875-4.688 1.461-.765.297-1.55.559-2.355.707-.809.149-1.649.176-2.45-.004-1.214-.27-2.296-1.007-3.081-1.972-.786-.965-1.286-2.14-1.547-3.36-.524-2.43-.125-4.953.23-7.414a199.18 199.18 0 002-24.914 119.325 119.325 0 00-22.3-4.867c-2.329-.281-4.75-.48-6.97.274-1.113.378-2.152 1-2.933 1.875-.781.87-1.289 2.011-1.312 3.183-.02.824.199 1.645.543 2.395.347.75.812 1.441 1.312 2.097 1 1.317 2.148 2.559 2.773 4.09.688 1.676.676 3.606.082 5.32-.593 1.711-1.746 3.207-3.175 4.32-1.434 1.114-3.137 1.852-4.903 2.263-1.765.41-3.593.5-5.406.394-2.918-.172-5.894-.89-8.23-2.652-1.168-.88-2.157-2.016-2.79-3.332-.632-1.317-.906-2.82-.69-4.266.296-1.965 1.437-3.68 2.398-5.418.48-.871.925-1.77 1.199-2.723.277-.953.379-1.976.168-2.945-.219-1.016-.774-1.941-1.512-2.672-.738-.726-1.652-1.261-2.629-1.617-1.949-.71-4.078-.707-6.148-.586a58.229 58.229 0 00-22.254 5.867zm0 0" fill="#33d17a"/></g><g id="f" clip-path="url(#c)" filter="url(#d)"><use xlink:href="#e"/></g><g id="j" clip-path="url(#g)"><path d="M128 80.64V128H0V80.64zm0 0" fill="url(#h)"/><path d="M13.309 80.64L60.664 128H81.88l-47.36-47.36zm42.421 0L103.094 128h21.215L76.945 80.64zm42.43 0L128 110.48V89.27l-8.629-8.63zM0 88.548v21.215L18.238 128h21.215zm0 0"/></g><linearGradient id="l" gradientUnits="userSpaceOnUse" x1="10.23" y1="87.43" x2="133.236" y2="88.679" gradientTransform="translate(-8 -16)"><stop offset="0" stop-color="#208757"/><stop offset=".077" stop-color="#2ec27e"/><stop offset=".147" stop-color="#208a5a"/><stop offset=".198" stop-color="#26a269"/><stop offset=".364" stop-color="#1c774d"/><stop offset=".407" stop-color="#26a269"/><stop offset=".493" stop-color="#26a269"/><stop offset=".576" stop-color="#26a269"/><stop offset=".606" stop-color="#2ec27e"/><stop offset=".681" stop-color="#26a269"/><stop offset=".784" stop-color="#1d7a4e"/><stop offset=".945" stop-color="#28ab6f"/><stop offset="1" stop-color="#48d493"/></linearGradient><linearGradient id="h" gradientUnits="userSpaceOnUse" x1="300" y1="235" x2="428" y2="235" gradientTransform="matrix(0 .37 -.98462 0 295.385 -30.36)"><stop offset="0" stop-color="#f9f06b"/><stop offset="1" stop-color="#f5c211"/></linearGradient><linearGradient id="b" gradientUnits="userSpaceOnUse" x1="10.23" y1="87.43" x2="133.236" y2="88.679" gradientTransform="translate(-8 -16)"><stop offset="0" stop-color="#208757"/><stop offset=".077" stop-color="#2ec27e"/><stop offset=".147" stop-color="#208a5a"/><stop offset=".198" stop-color="#26a269"/><stop offset=".364" stop-color="#1c774d"/><stop offset=".407" stop-color="#26a269"/><stop offset=".493" stop-color="#26a269"/><stop offset=".576" stop-color="#26a269"/><stop offset=".606" stop-color="#2ec27e"/><stop offset=".681" stop-color="#26a269"/><stop offset=".784" stop-color="#1d7a4e"/><stop offset=".945" stop-color="#28ab6f"/><stop offset="1" stop-color="#48d493"/></linearGradient><mask id="k"><g filter="url(#d)"><path fill-opacity=".8" d="M0 0h128v128H0z"/></g></mask><mask id="n"><use xlink:href="#f"/></mask><filter id="d" filterUnits="objectBoundingBox" x="0%" y="0%" width="100%" height="100%"><feColorMatrix in="SourceGraphic" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 1 0"/></filter></defs><path d="M21.965 18.363v4l2.039 17.75c.418 3.637 1.055 7.282.844 10.938-.106 1.828-.465 3.668-1.309 5.293-.426.812-.969 1.57-1.644 2.191-.676.621-1.485 1.106-2.364 1.356-1.047.297-2.156.261-3.238.164-1.094-.098-2.227-.282-3.137-.903-.394-.273-.73-.617-1.082-.945-.351-.328-.726-.645-1.168-.832-.691-.297-1.492-.254-2.207-.023-.715.23-1.355.64-1.972 1.066-1.188.824-2.325 1.758-3.184 2.922-1.734 2.344-2.203 5.46-1.711 8.336.367 2.152 1.258 4.238 2.688 5.89.722.836 1.582 1.563 2.566 2.063.98.504 2.09.773 3.195.71 1.446-.085 2.79-.73 4.114-1.327 1.32-.594 2.718-1.16 4.168-1.067a5.113 5.113 0 012.332.758 5.958 5.958 0 011.773 1.707c.848 1.242 1.234 2.742 1.531 4.219 1.832 9.133.778 18.82-2.98 27.344v4c6.812 2.347 13.8 4.199 20.886 5.535 2.356.441 4.801.824 7.13.246 1.163-.29 2.28-.832 3.148-1.66a5.39 5.39 0 001.441-2.34c.266-.887.278-1.828.254-2.754L54 110c-.426.09-.875.059-1.285-.082a2.751 2.751 0 01-1.16-.8 3.768 3.768 0 01-.7-1.231c-.316-.895-.351-1.864-.355-2.813-.008-1.57.063-3.187.684-4.629.629-1.465 1.793-2.652 3.117-3.535 2.64-1.758 5.898-2.375 9.07-2.414 2.91-.035 5.918.414 8.387 1.953 1.238.77 2.316 1.809 3.043 3.07.722 1.262 1.078 2.739.953 4.188-.098 1.117-.477 2.2-1.02 3.18A9.862 9.862 0 0172 110c.012.996.02 1.992.02 2.988.003.555.003 1.114.07 1.664.07.551.21 1.098.476 1.586.383.7 1.012 1.239 1.692 1.657.68.414 1.422.722 2.176.984 4.207 1.46 8.793 1.562 13.203.922a40.756 40.756 0 0015.86-5.88l-.005-4.003a60.146 60.146 0 01-3.433-25.465c.125-1.512.312-3.039.894-4.441.29-.7.68-1.364 1.18-1.93.504-.566 1.12-1.039 1.82-1.332.82-.344 1.727-.434 2.613-.367.887.07 1.75.297 2.598.57 1.688.547 3.344 1.297 5.117 1.418 1.492.098 2.996-.266 4.332-.937a9.894 9.894 0 004.016-3.715 9.91 9.91 0 001.418-5.285l.176-2.258a16.406 16.406 0 00-3.075-6.477c-.785-.996-1.707-1.914-2.835-2.496a5.247 5.247 0 00-1.805-.558 4.18 4.18 0 00-1.875.214c-.988.364-1.774 1.125-2.703 1.614-1.032.543-2.203.738-3.364.824-1.148.086-2.324.07-3.425-.274-1.48-.464-2.743-1.52-3.602-2.812-.855-1.293-1.332-2.805-1.59-4.328-.574-3.399-.117-6.871.266-10.297a227.56 227.56 0 001.34-19.973 497.333 497.333 0 01-21.118-5.117c-1.355-.36-2.718-.726-4.113-.894-1.394-.165-2.836-.118-4.168.332a7.108 7.108 0 00-2.93 1.894v4c0 .414.016.828.11 1.235.094.402.258.789.445 1.156.371.738.82 1.433 1.313 2.097.98 1.329 2.136 2.563 2.765 4.094.688 1.676.668 3.606.07 5.317-.597 1.707-1.75 3.203-3.179 4.312-1.43 1.113-3.133 1.852-4.895 2.262-1.765.414-3.593.508-5.402.406-2.926-.164-5.906-.875-8.25-2.633-1.172-.883-2.164-2.015-2.793-3.34-.625-1.324-.879-2.828-.668-4.277.293-1.973 1.399-3.71 2.45-5.406.421-.684.831-1.38 1.078-2.14.242-.763.296-1.571.308-2.372L54.633 18a1.89 1.89 0 01-1.695.777c-.516-.047-.989-.3-1.41-.601-.419-.301-.801-.653-1.231-.938-.871-.578-1.914-.855-2.957-.933-1.043-.075-2.09.039-3.125.191-2.403.356-4.77.91-7.176 1.266-2.402.351-4.832.46-7.262.496zm0 0" fill="url(#l)"/><path d="M21.965 18.363l2.039 17.746c.426 3.715 1.16 7.414 1.094 11.149-.035 1.867-.34 3.77-1.239 5.41-.449.816-1.047 1.562-1.785 2.133-.738.574-1.617.972-2.543 1.09-1.504.195-2.996-.344-4.379-.961-1.379-.621-2.75-1.344-4.246-1.555-1.465-.21-2.984.094-4.293.777-1.308.688-2.406 1.746-3.207 2.989-1.601 2.484-1.972 5.605-1.574 8.535.3 2.219 1.05 4.433 2.512 6.133.73.847 1.633 1.558 2.656 2.011 1.023.457 2.168.657 3.281.52 1.54-.192 2.926-1 4.324-1.676.704-.336 1.422-.644 2.176-.828.754-.184 1.551-.238 2.309-.066.82.183 1.566.628 2.164 1.218.601.586 1.05 1.313 1.375 2.086.64 1.551.762 3.258.84 4.934a123.2 123.2 0 01-2.25 29.965 133.42 133.42 0 0020.886 5.53c2.352.427 4.793.782 7.11.196 1.16-.293 2.27-.828 3.152-1.633.883-.804 1.528-1.89 1.711-3.074.133-.879.012-1.789-.25-2.64-.262-.852-.668-1.657-1.101-2.438-.868-1.559-1.887-3.086-2.227-4.84-.3-1.558-.023-3.207.676-4.633.703-1.425 1.816-2.629 3.12-3.535 2.614-1.804 5.9-2.383 9.075-2.41 2.91-.023 5.918.414 8.39 1.953 1.235.77 2.313 1.809 3.04 3.07.73 1.258 1.097 2.743.953 4.188-.117 1.113-.524 2.18-1.024 3.18-.5 1.004-1.093 1.957-1.609 2.949-.512.996-.95 2.047-1.11 3.152-.156 1.11-.019 2.29.555 3.25.41.684 1.02 1.23 1.696 1.649.675.422 1.418.722 2.168.992a30.23 30.23 0 0015.183 1.332 30.128 30.128 0 0013.88-6.293l-2.587-19.371c-.453-3.406-1.14-6.813-1.047-10.246.043-1.719.34-3.461 1.141-4.98a6.796 6.796 0 011.578-2.02 5.338 5.338 0 012.262-1.18c.8-.183 1.637-.164 2.441-.02.809.145 1.59.41 2.352.708 1.527.597 3.023 1.34 4.648 1.558 1.5.203 3.055-.062 4.426-.699 1.375-.64 2.566-1.648 3.469-2.863 1.808-2.434 2.418-5.625 2.047-8.63-.305-2.448-1.258-4.862-2.953-6.655-.848-.895-1.875-1.63-3.016-2.098-1.14-.473-2.395-.684-3.621-.563-1.637.157-3.153.875-4.688 1.461-.765.297-1.55.559-2.355.707-.809.149-1.649.176-2.45-.004-1.214-.27-2.296-1.007-3.081-1.972-.786-.965-1.286-2.14-1.547-3.36-.524-2.43-.125-4.953.23-7.414a199.18 199.18 0 002-24.914 119.325 119.325 0 00-22.3-4.867c-2.329-.281-4.75-.48-6.97.274-1.113.378-2.152 1-2.933 1.875-.781.87-1.289 2.011-1.312 3.183-.02.824.199 1.645.543 2.395.347.75.812 1.441 1.312 2.097 1 1.317 2.148 2.559 2.773 4.09.688 1.676.676 3.606.082 5.32-.593 1.711-1.746 3.207-3.175 4.32-1.434 1.114-3.137 1.852-4.903 2.263-1.765.41-3.593.5-5.406.394-2.918-.172-5.894-.89-8.23-2.652-1.168-.88-2.157-2.016-2.79-3.332-.632-1.317-.906-2.82-.69-4.266.296-1.965 1.437-3.68 2.398-5.418.48-.871.925-1.77 1.199-2.723.277-.953.379-1.976.168-2.945-.219-1.016-.774-1.941-1.512-2.672-.738-.726-1.652-1.261-2.629-1.617-1.949-.71-4.078-.707-6.148-.586a58.229 58.229 0 00-22.254 5.867zm0 0" fill="#33d17a"/><use xlink:href="#m" mask="url(#n)"/></svg>
\ No newline at end of file diff --git a/subprojects/extensions-app/data/icons/hicolor/scalable/apps/org.gnome.Extensions.svg b/subprojects/extensions-app/data/icons/hicolor/scalable/apps/org.gnome.Extensions.svg new file mode 100644 index 0000000..496be6b --- /dev/null +++ b/subprojects/extensions-app/data/icons/hicolor/scalable/apps/org.gnome.Extensions.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128"><defs><linearGradient id="a" gradientUnits="userSpaceOnUse" x1="10.23" y1="87.43" x2="133.236" y2="88.679" gradientTransform="translate(-8 -16)"><stop offset="0" stop-color="#208757"/><stop offset=".077" stop-color="#2ec27e"/><stop offset=".147" stop-color="#208a5a"/><stop offset=".198" stop-color="#26a269"/><stop offset=".364" stop-color="#1c774d"/><stop offset=".407" stop-color="#26a269"/><stop offset=".493" stop-color="#26a269"/><stop offset=".576" stop-color="#26a269"/><stop offset=".606" stop-color="#2ec27e"/><stop offset=".681" stop-color="#26a269"/><stop offset=".784" stop-color="#1d7a4e"/><stop offset=".945" stop-color="#28ab6f"/><stop offset="1" stop-color="#48d493"/></linearGradient></defs><path d="M21.965 18.363v4l2.039 17.75c.418 3.637 1.055 7.282.844 10.938-.106 1.828-.465 3.668-1.309 5.293-.426.812-.969 1.57-1.644 2.191-.676.621-1.485 1.106-2.364 1.356-1.047.297-2.156.261-3.238.164-1.094-.098-2.227-.282-3.137-.903-.394-.273-.73-.617-1.082-.945-.351-.328-.726-.645-1.168-.832-.691-.297-1.492-.254-2.207-.023-.715.23-1.355.64-1.972 1.066-1.188.824-2.325 1.758-3.184 2.922-1.734 2.344-2.203 5.46-1.711 8.336.367 2.152 1.258 4.238 2.688 5.89.722.836 1.582 1.563 2.566 2.063.98.504 2.09.773 3.195.71 1.446-.085 2.79-.73 4.114-1.327 1.32-.594 2.718-1.16 4.168-1.067a5.113 5.113 0 012.332.758 5.958 5.958 0 011.773 1.707c.848 1.242 1.234 2.742 1.531 4.219 1.832 9.133.778 18.82-2.98 27.344v4c6.812 2.347 13.8 4.199 20.886 5.535 2.356.441 4.801.824 7.13.246 1.163-.29 2.28-.832 3.148-1.66a5.39 5.39 0 001.441-2.34c.266-.887.278-1.828.254-2.754L54 110c-.426.09-.875.059-1.285-.082a2.751 2.751 0 01-1.16-.8 3.768 3.768 0 01-.7-1.231c-.316-.895-.351-1.864-.355-2.813-.008-1.57.063-3.187.684-4.629.629-1.465 1.793-2.652 3.117-3.535 2.64-1.758 5.898-2.375 9.07-2.414 2.91-.035 5.918.414 8.387 1.953 1.238.77 2.316 1.809 3.043 3.07.722 1.262 1.078 2.739.953 4.188-.098 1.117-.477 2.2-1.02 3.18A9.862 9.862 0 0172 110c.012.996.02 1.992.02 2.988.003.555.003 1.114.07 1.664.07.551.21 1.098.476 1.586.383.7 1.012 1.239 1.692 1.657.68.414 1.422.722 2.176.984 4.207 1.46 8.793 1.562 13.203.922a40.756 40.756 0 0015.86-5.88l-.005-4.003a60.146 60.146 0 01-3.433-25.465c.125-1.512.312-3.039.894-4.441.29-.7.68-1.364 1.18-1.93.504-.566 1.12-1.039 1.82-1.332.82-.344 1.727-.434 2.613-.367.887.07 1.75.297 2.598.57 1.688.547 3.344 1.297 5.117 1.418 1.492.098 2.996-.266 4.332-.937a9.894 9.894 0 004.016-3.715 9.91 9.91 0 001.418-5.285l.176-2.258a16.406 16.406 0 00-3.075-6.477c-.785-.996-1.707-1.914-2.835-2.496a5.247 5.247 0 00-1.805-.558 4.18 4.18 0 00-1.875.214c-.988.364-1.774 1.125-2.703 1.614-1.032.543-2.203.738-3.364.824-1.148.086-2.324.07-3.425-.274-1.48-.464-2.743-1.52-3.602-2.812-.855-1.293-1.332-2.805-1.59-4.328-.574-3.399-.117-6.871.266-10.297a227.56 227.56 0 001.34-19.973 497.333 497.333 0 01-21.118-5.117c-1.355-.36-2.718-.726-4.113-.894-1.394-.165-2.836-.118-4.168.332a7.108 7.108 0 00-2.93 1.894v4c0 .414.016.828.11 1.235.094.402.258.789.445 1.156.371.738.82 1.433 1.313 2.097.98 1.329 2.136 2.563 2.765 4.094.688 1.676.668 3.606.07 5.317-.597 1.707-1.75 3.203-3.179 4.312-1.43 1.113-3.133 1.852-4.895 2.262-1.765.414-3.593.508-5.402.406-2.926-.164-5.906-.875-8.25-2.633-1.172-.883-2.164-2.015-2.793-3.34-.625-1.324-.879-2.828-.668-4.277.293-1.973 1.399-3.71 2.45-5.406.421-.684.831-1.38 1.078-2.14.242-.763.296-1.571.308-2.372L54.633 18a1.89 1.89 0 01-1.695.777c-.516-.047-.989-.3-1.41-.601-.419-.301-.801-.653-1.231-.938-.871-.578-1.914-.855-2.957-.933-1.043-.075-2.09.039-3.125.191-2.403.356-4.77.91-7.176 1.266-2.402.351-4.832.46-7.262.496zm0 0" fill="url(#a)"/><path d="M21.965 18.363l2.039 17.746c.426 3.715 1.16 7.414 1.094 11.149-.035 1.867-.34 3.77-1.239 5.41-.449.816-1.047 1.562-1.785 2.133-.738.574-1.617.972-2.543 1.09-1.504.195-2.996-.344-4.379-.961-1.379-.621-2.75-1.344-4.246-1.555-1.465-.21-2.984.094-4.293.777-1.308.688-2.406 1.746-3.207 2.989-1.601 2.484-1.972 5.605-1.574 8.535.3 2.219 1.05 4.433 2.512 6.133.73.847 1.633 1.558 2.656 2.011 1.023.457 2.168.657 3.281.52 1.54-.192 2.926-1 4.324-1.676.704-.336 1.422-.644 2.176-.828.754-.184 1.551-.238 2.309-.066.82.183 1.566.628 2.164 1.218.601.586 1.05 1.313 1.375 2.086.64 1.551.762 3.258.84 4.934a123.2 123.2 0 01-2.25 29.965 133.42 133.42 0 0020.886 5.53c2.352.427 4.793.782 7.11.196 1.16-.293 2.27-.828 3.152-1.633.883-.804 1.528-1.89 1.711-3.074.133-.879.012-1.789-.25-2.64-.262-.852-.668-1.657-1.101-2.438-.868-1.559-1.887-3.086-2.227-4.84-.3-1.558-.023-3.207.676-4.633.703-1.425 1.816-2.629 3.12-3.535 2.614-1.804 5.9-2.383 9.075-2.41 2.91-.023 5.918.414 8.39 1.953 1.235.77 2.313 1.809 3.04 3.07.73 1.258 1.097 2.743.953 4.188-.117 1.113-.524 2.18-1.024 3.18-.5 1.004-1.093 1.957-1.609 2.949-.512.996-.95 2.047-1.11 3.152-.156 1.11-.019 2.29.555 3.25.41.684 1.02 1.23 1.696 1.649.675.422 1.418.722 2.168.992a30.23 30.23 0 0015.183 1.332 30.128 30.128 0 0013.88-6.293l-2.587-19.371c-.453-3.406-1.14-6.813-1.047-10.246.043-1.719.34-3.461 1.141-4.98a6.796 6.796 0 011.578-2.02 5.338 5.338 0 012.262-1.18c.8-.183 1.637-.164 2.441-.02.809.145 1.59.41 2.352.708 1.527.597 3.023 1.34 4.648 1.558 1.5.203 3.055-.062 4.426-.699 1.375-.64 2.566-1.648 3.469-2.863 1.808-2.434 2.418-5.625 2.047-8.63-.305-2.448-1.258-4.862-2.953-6.655-.848-.895-1.875-1.63-3.016-2.098-1.14-.473-2.395-.684-3.621-.563-1.637.157-3.153.875-4.688 1.461-.765.297-1.55.559-2.355.707-.809.149-1.649.176-2.45-.004-1.214-.27-2.296-1.007-3.081-1.972-.786-.965-1.286-2.14-1.547-3.36-.524-2.43-.125-4.953.23-7.414a199.18 199.18 0 002-24.914 119.325 119.325 0 00-22.3-4.867c-2.329-.281-4.75-.48-6.97.274-1.113.378-2.152 1-2.933 1.875-.781.87-1.289 2.011-1.312 3.183-.02.824.199 1.645.543 2.395.347.75.812 1.441 1.312 2.097 1 1.317 2.148 2.559 2.773 4.09.688 1.676.676 3.606.082 5.32-.593 1.711-1.746 3.207-3.175 4.32-1.434 1.114-3.137 1.852-4.903 2.263-1.765.41-3.593.5-5.406.394-2.918-.172-5.894-.89-8.23-2.652-1.168-.88-2.157-2.016-2.79-3.332-.632-1.317-.906-2.82-.69-4.266.296-1.965 1.437-3.68 2.398-5.418.48-.871.925-1.77 1.199-2.723.277-.953.379-1.976.168-2.945-.219-1.016-.774-1.941-1.512-2.672-.738-.726-1.652-1.261-2.629-1.617-1.949-.71-4.078-.707-6.148-.586a58.229 58.229 0 00-22.254 5.867zm0 0" fill="#33d17a"/></svg>
\ No newline at end of file diff --git a/subprojects/extensions-app/data/icons/hicolor/symbolic/apps/org.gnome.Extensions-symbolic.svg b/subprojects/extensions-app/data/icons/hicolor/symbolic/apps/org.gnome.Extensions-symbolic.svg new file mode 100644 index 0000000..4208a4d --- /dev/null +++ b/subprojects/extensions-app/data/icons/hicolor/symbolic/apps/org.gnome.Extensions-symbolic.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path d="M6.5 1.031c-.371 0-.742-.035-1.11.016-.367.05-.73.203-.972.476-.125.141-.215.309-.266.485-.047.18-.054.367-.02.55.032.184.102.356.192.516.09.164.203.309.317.457L5 4H2a1.8 1.8 0 00-.41.035.791.791 0 00-.36.195.791.791 0 00-.195.36C1 4.723 1 4.863 1 5v2.75l.77-.344c.265-.117.542-.23.832-.242.289-.016.586.074.812.254.227.18.383.441.465.723.082.277.101.57.121.859.02.316.04.637-.016.95-.058.312-.199.616-.43.831a1.264 1.264 0 01-.874.32c-.317-.007-.618-.128-.91-.257L1 10.5V14c0 .137.004.277.035.41a.791.791 0 00.195.36c.098.097.227.16.36.195.133.035.273.035.41.035h3l-.328-.68c-.14-.293-.274-.597-.29-.922-.015-.32.095-.652.31-.894.214-.242.523-.39.84-.453.316-.067.644-.059.968-.059.324 0 .652-.008.969.059.316.062.625.21.84.453.214.242.324.574.308.894-.015.325-.148.63-.289.922L8 15h3a1.8 1.8 0 00.41-.035.791.791 0 00.36-.195.791.791 0 00.195-.36C12 14.277 12 14.137 12 14v-3.563l.703.297c.29.125.59.239.902.246.313.004.63-.101.864-.308.238-.203.386-.496.46-.8C15 9.565 15 9.25 15 8.937c0-.313 0-.63-.07-.934-.075-.305-.223-.598-.461-.8a1.288 1.288 0 00-.864-.31c-.312.008-.613.122-.902.247L12 7.437V5a1.8 1.8 0 00-.035-.41.791.791 0 00-.195-.36.791.791 0 00-.36-.195C11.277 4 11.137 4 11 4H8l.36-.469c.113-.148.226-.293.316-.457.09-.16.16-.332.191-.515a1.248 1.248 0 00-.02-.551 1.256 1.256 0 00-.265-.485c-.242-.273-.605-.425-.973-.476-.367-.05-.738-.016-1.109-.016zm0 0" fill="#474747"/></svg>
\ No newline at end of file diff --git a/subprojects/extensions-app/data/icons/meson.build b/subprojects/extensions-app/data/icons/meson.build new file mode 100644 index 0000000..eff6e4b --- /dev/null +++ b/subprojects/extensions-app/data/icons/meson.build @@ -0,0 +1 @@ +install_subdir('hicolor', install_dir: icondir) diff --git a/subprojects/extensions-app/data/meson.build b/subprojects/extensions-app/data/meson.build new file mode 100644 index 0000000..4f24267 --- /dev/null +++ b/subprojects/extensions-app/data/meson.build @@ -0,0 +1,50 @@ +gnome.compile_resources( + app_id + '.data', + configure_file( + input: base_id + '.data.gresource.xml.in', + output: app_id + '.data.gresource.xml', + configuration: {'profile': '/'.join(profile.split('.')) }, + ), + gresource_bundle: true, + install: true, + install_dir: pkgdatadir +) + +desktop_file = app_id + '.desktop' +desktopconf = configuration_data() +# We substitute in bindir so it works as an autostart +# file when built in a non-system prefix +desktopconf.set('bindir', bindir) +desktopconf.set('app_id', app_id) +desktopconf.set('prgname', prgname) + +i18n.merge_file( + input: configure_file( + input: base_id + '.desktop.in.in', + output: desktop_file + '.in', + configuration: desktopconf + ), + output: desktop_file, + po_dir: po_dir, + install: true, + install_dir: desktopdir, + type: 'desktop' +) + +if (desktop_file_validate.found()) + test('Validating ' + desktop_file, + desktop_file_validate, + args: [desktop_file], + workdir: meson.current_build_dir() + ) +endif + +configure_file( + input: base_id + '.service.in', + output: app_id + '.service', + configuration: desktopconf, + install_dir: servicedir, +) + +subdir('icons') +subdir('metainfo') diff --git a/subprojects/extensions-app/data/metainfo/extensions-main.png b/subprojects/extensions-app/data/metainfo/extensions-main.png Binary files differnew file mode 100644 index 0000000..7d3de2b --- /dev/null +++ b/subprojects/extensions-app/data/metainfo/extensions-main.png diff --git a/subprojects/extensions-app/data/metainfo/extensions-remove.png b/subprojects/extensions-app/data/metainfo/extensions-remove.png Binary files differnew file mode 100644 index 0000000..d54e201 --- /dev/null +++ b/subprojects/extensions-app/data/metainfo/extensions-remove.png diff --git a/subprojects/extensions-app/data/metainfo/extensions-update.png b/subprojects/extensions-app/data/metainfo/extensions-update.png Binary files differnew file mode 100644 index 0000000..4b91cd8 --- /dev/null +++ b/subprojects/extensions-app/data/metainfo/extensions-update.png diff --git a/subprojects/extensions-app/data/metainfo/meson.build b/subprojects/extensions-app/data/metainfo/meson.build new file mode 100644 index 0000000..a19bfa8 --- /dev/null +++ b/subprojects/extensions-app/data/metainfo/meson.build @@ -0,0 +1,16 @@ +metainfo = app_id + '.metainfo.xml' +i18n.merge_file( + input: base_id + '.metainfo.xml.in', + output: metainfo, + po_dir: po_dir, + install: true, + install_dir: metainfodir +) + +if (appstream_util.found()) + test('Validating ' + metainfo, + appstream_util, + args: ['validate', '--nonet', metainfo], + workdir: meson.current_build_dir() + ) +endif diff --git a/subprojects/extensions-app/data/metainfo/org.gnome.Extensions.metainfo.xml.in b/subprojects/extensions-app/data/metainfo/org.gnome.Extensions.metainfo.xml.in new file mode 100644 index 0000000..8ce6332 --- /dev/null +++ b/subprojects/extensions-app/data/metainfo/org.gnome.Extensions.metainfo.xml.in @@ -0,0 +1,67 @@ +<?xml version="1.0" encoding="UTF-8"?> +<component type="desktop-application"> + <id>org.gnome.Extensions</id> + + <name>Extensions</name> + <summary>Manage your GNOME Extensions</summary> + + <metadata_license>CC0-1.0</metadata_license> + <project_license>GPL-2.0-or-later</project_license> + + <url type="homepage">https://gitlab.gnome.org/GNOME/gnome-shell/-/tree/HEAD/subprojects/extensions-app</url> + <url type="bugtracker">https://gitlab.gnome.org/GNOME/gnome-shell/issues/new</url> + <url type="donation">http://www.gnome.org/friends/</url> + <url type="translate">https://wiki.gnome.org/TranslationProject</url> + + <project_group>GNOME</project_group> + <developer_name>The GNOME Project</developer_name> + + <launchable type="desktop-id">org.gnome.Extensions.desktop</launchable> + + <kudos> + <kudo>HiDpiIcon</kudo> + <kudo>HighContrast</kudo> + <kudo>ModernToolkit</kudo> + </kudos> + + <recommends> + <control>pointing</control> + <control>keyboard</control> + <control>touch</control> + </recommends> + + <content_rating type="oars-1.0"/> + + <description> + <p> + GNOME Extensions handles updating extensions, configuring extension preferences and removing or disabling unwanted extensions. + </p> + </description> + + <releases> + <release version="43.9" date="2023-09-16"/> + <release version="43.8" date="2023-08-22"/> + <release version="43.7" date="2023-07-04"/> + <release version="43.6" date="2023-06-03"/> + <release version="43.5" date="2023-04-24"/> + <release version="43.4" date="2023-03-19"/> + <release version="43.3" date="2023-02-13"/> + <release version="43.2" date="2022-12-06"/> + <release version="43.1" date="2022-10-22"/> + <release version="43.0" date="2022-09-17"> + <p>Modernize About window</p> + </release> + </releases> + + <screenshots> + <screenshot type="default"> + <image>https://gitlab.gnome.org/GNOME/gnome-shell/raw/HEAD/subprojects/extensions-app/data/metainfo/extensions-main.png</image> + </screenshot> + <screenshot> + <image>https://gitlab.gnome.org/GNOME/gnome-shell/raw/HEAD/subprojects/extensions-app/data/metainfo/extensions-update.png</image> + </screenshot> + <screenshot> + <image>https://gitlab.gnome.org/GNOME/gnome-shell/raw/HEAD/subprojects/extensions-app/data/metainfo/extensions-remove.png</image> + </screenshot> + </screenshots> +</component> diff --git a/subprojects/extensions-app/data/org.gnome.Extensions.data.gresource.xml.in b/subprojects/extensions-app/data/org.gnome.Extensions.data.gresource.xml.in new file mode 100644 index 0000000..ca04c08 --- /dev/null +++ b/subprojects/extensions-app/data/org.gnome.Extensions.data.gresource.xml.in @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="UTF-8"?> +<gresources> + <gresource prefix="/org/gnome/Extensions@profile@"> + <file alias="style.css">css/style.css</file> + </gresource> + + <gresource prefix="/org/gnome/Extensions"> + <file>dbus-interfaces/org.gnome.Shell.Extensions.xml</file> + + <file>ui/extension-row.ui</file> + <file>ui/extensions-window.ui</file> + </gresource> +</gresources> diff --git a/subprojects/extensions-app/data/org.gnome.Extensions.desktop.in.in b/subprojects/extensions-app/data/org.gnome.Extensions.desktop.in.in new file mode 100644 index 0000000..b68f5ff --- /dev/null +++ b/subprojects/extensions-app/data/org.gnome.Extensions.desktop.in.in @@ -0,0 +1,10 @@ +[Desktop Entry] +Type=Application +Name=Extensions +# Translators: Do NOT translate or transliterate this text (this is an icon file name)! +Icon=@app_id@ +Comment=Configure GNOME Shell Extensions +Exec=@bindir@/@prgname@ --gapplication-service +DBusActivatable=true +Categories=GNOME;GTK;Utility; +OnlyShowIn=GNOME; diff --git a/subprojects/extensions-app/data/org.gnome.Extensions.service.in b/subprojects/extensions-app/data/org.gnome.Extensions.service.in new file mode 100644 index 0000000..2150999 --- /dev/null +++ b/subprojects/extensions-app/data/org.gnome.Extensions.service.in @@ -0,0 +1,3 @@ +[D-BUS Service] +Name=@app_id@ +Exec=@bindir@/@prgname@ diff --git a/subprojects/extensions-app/data/ui/extension-row.ui b/subprojects/extensions-app/data/ui/extension-row.ui new file mode 100644 index 0000000..37acb68 --- /dev/null +++ b/subprojects/extensions-app/data/ui/extension-row.ui @@ -0,0 +1,143 @@ +<?xml version="1.0" encoding="UTF-8"?> +<interface> + <template class="ExtensionRow" parent="GtkListBoxRow"> + <style> + <class name="extension"/> + </style> + <child> + <object class="GtkBox"> + <property name="orientation">vertical</property> + <child> + <object class="GtkBox"> + <child> + <object class="GtkBox"> + <property name="orientation">vertical</property> + <property name="hexpand">true</property> + <style> + <class name="information"/> + </style> + <child> + <object class="GtkBox"> + <style> + <class name="header"/> + </style> + <child> + <object class="GtkLabel" id="nameLabel"> + <property name="xalign">0</property> + <style> + <class name="title"/> + </style> + </object> + </child> + <child> + <object class="GtkLabel" id="versionLabel"> + <property name="visible">false</property> + <property name="xalign">0</property> + <property name="yalign">1</property> + <style> + <class name="caption"/> + <class name="dim-label"/> + </style> + </object> + </child> + <child> + <object class="GtkBox"> + <style> + <class name="status"/> + </style> + <child> + <object class="GtkImage" id="errorIcon"> + <property name="visible">false</property> + <property name="icon-name">dialog-error-symbolic</property> + <property name="tooltip-text" translatable="yes">The extension had an error</property> + <style> + <class name="error"/> + </style> + </object> + </child> + <child> + <object class="GtkImage" id="updatesIcon"> + <property name="visible">false</property> + <property name="icon-name">software-update-available-symbolic</property> + <property name="tooltip-text" translatable="yes">The extension can be updated</property> + <style> + <class name="warning"/> + </style> + </object> + </child> + </object> + </child> + </object> + </child> + <child> + <object class="GtkLabel" id="descriptionLabel"> + <property name="xalign">0</property> + <property name="ellipsize">end</property> + <style> + <class name="subtitle"/> + </style> + </object> + </child> + <child> + <object class="GtkLabel" id="errorLabel"> + <property name="visible">false</property> + <property name="selectable">true</property> + <property name="wrap">True</property> + <property name="xalign">0</property> + <style> + <class name="caption"/> + <class name="error"/> + </style> + </object> + </child> + </object> + </child> + <child> + <object class="GtkSwitch" id="switch"> + <property name="valign">center</property> + <property name="action-name">row.enabled</property> + </object> + </child> + </object> + </child> + <child> + <object class="GtkCenterBox" id="actionsBox"> + <style> + <class name="actions"/> + </style> + <child type="start"> + <object class="GtkBox"> + <child> + <object class="GtkButton" id="websiteButton"> + <property name="label" translatable="yes">Website</property> + <property name="action-name">row.show-url</property> + </object> + </child> + <child> + <object class="GtkButton" id="prefsButton"> + <property name="visible" + bind-source="prefsButton" + bind-property="sensitive" + bind-flags="sync-create"/> + <property name="label" translatable="yes">Settings</property> + <property name="action-name">row.show-prefs</property> + </object> + </child> + </object> + </child> + <child type="end"> + <object class="GtkButton" id="removeButton"> + <property name="visible" + bind-source="removeButton" + bind-property="sensitive" + bind-flags="sync-create"/> + <property name="label" translatable="yes">Remove…</property> + <property name="action-name">row.uninstall</property> + </object> + </child> + </object> + </child> + </object> + </child> + </template> +</interface> diff --git a/subprojects/extensions-app/data/ui/extensions-window.ui b/subprojects/extensions-app/data/ui/extensions-window.ui new file mode 100644 index 0000000..88e0f11 --- /dev/null +++ b/subprojects/extensions-app/data/ui/extensions-window.ui @@ -0,0 +1,204 @@ +<?xml version="1.0" encoding="UTF-8"?> +<interface> + <menu id="primary-menu"> + <section> + <item> + <attribute name="label" translatable="yes">Help</attribute> + <attribute name="action">win.show-help</attribute> + </item> + <item> + <attribute name="label" translatable="yes">About Extensions</attribute> + <attribute name="action">win.show-about</attribute> + </item> + </section> + </menu> + <template class="ExtensionsWindow" parent="GtkApplicationWindow"> + <property name="default-width">800</property> + <property name="default-height">500</property> + <property name="title" translatable="yes">Extensions</property> + <child type="titlebar"> + <object class="GtkHeaderBar"> + <child type="end"> + <object class="GtkMenuButton" id="menuButton"> + <property name="receives-default">True</property> + <property name="menu-model">primary-menu</property> + <property name="icon-name">open-menu-symbolic</property> + </object> + </child> + <child type="end"> + <object class="GtkToggleButton" id="searchButton"> + <property name="receives-default">True</property> + <property name="icon-name">edit-find-symbolic</property> + <child> + <object class="GtkShortcutController"> + <property name='scope'>global</property> + <child> + <object class='GtkShortcut'> + <property name='trigger'><Control>f</property> + <property name='action'>activate</property> + </object> + </child> + <child> + <object class='GtkShortcut'> + <property name='trigger'><Control>s</property> + <property name='action'>activate</property> + </object> + </child> + </object> + </child> + </object> + </child> + </object> + </child> + <child> + <object class="GtkBox"> + <property name="orientation">vertical</property> + <child> + <object class="GtkSearchBar" id="searchBar"> + <property name="key-capture-widget">ExtensionsWindow</property> + <property name="search-mode-enabled" + bind-source="searchButton" + bind-property="active" + bind-flags="bidirectional"/> + <child> + <object class="GtkSearchEntry" id="searchEntry"> + <property name="max-width-chars">35</property> + </object> + </child> + </object> + </child> + <child> + <object class="GtkStack" id="mainStack"> + <property name="transition-type">crossfade</property> + <property name="vexpand">True</property> + <child> + <object class="GtkStackPage"> + <property name="name">main</property> + <property name="child"> + <object class="AdwPreferencesPage"> + <child> + <object class="AdwPreferencesGroup"> + <child> + <object class="AdwActionRow"> + <property name="title" translatable="yes">Extensions</property> + <property name="subtitle" translatable="yes">Extensions can cause performance and stability issues. Disable extensions if you encounter problems with your system.</property> + <property name="activatable-widget">enabledSwitch</property> + <child> + <object class="GtkSwitch" id="enabledSwitch"> + <property name="action-name">win.user-extensions-enabled</property> + <property name="valign">center</property> + </object> + </child> + </object> + </child> + </object> + </child> + <child> + <object class="AdwPreferencesGroup" id="userGroup"> + <property name="title" translatable="yes">Manually Installed</property> + <property name="description" translatable="yes">To find and add extensions, visit <a href="https://extensions.gnome.org">extensions.gnome.org</a>.</property> + <child> + <object class="GtkListBox" id="userList"> + <property name="selection-mode">none</property> + <style> + <class name="boxed-list"/> + </style> + </object> + </child> + </object> + </child> + <child> + <object class="AdwPreferencesGroup" id="systemGroup"> + <property name="title" translatable="yes">Built-In</property> + <child> + <object class="GtkListBox" id="systemList"> + <property name="selection-mode">none</property> + <style> + <class name="boxed-list"/> + </style> + </object> + </child> + </object> + </child> + </object> + </property> + </object> + </child> + <child> + <object class="GtkStackPage"> + <property name="name">placeholder</property> + <property name="child"> + <object class="AdwStatusPage"> + <property name="icon-name">org.gnome.Extensions-symbolic</property> + <property name="title" translatable="yes">No Installed Extensions</property> + <property name="description" translatable="yes">To find and add extensions, visit <a href="https://extensions.gnome.org">extensions.gnome.org</a>.</property> + </object> + </property> + </object> + </child> + <child> + <object class="GtkStackPage"> + <property name="name">noshell</property> + <property name="child"> + <object class="AdwStatusPage"> + <property name="title" translatable="yes">Something’s gone wrong</property> + <property name="description" translatable="yes">We’re very sorry, but it was not possible to get the list of installed extensions. Make sure you are logged into GNOME and try again.</property> + </object> + </property> + </object> + </child> + </object> + </child> + <child> + <object class="GtkActionBar" id="updatesBar"> + <property name="revealed">False</property> + <child> + <object class="GtkImage"> + <property name="pixel-size">24</property> + <property name="margin-start">6</property> + <property name="margin-end">6</property> + <property name="margin-top">6</property> + <property name="margin-bottom">6</property> + <property name="icon-name">software-update-available-symbolic</property> + <style> + <class name="warning"/> + </style> + </object> + </child> + <child> + <object class="GtkBox"> + <property name="orientation">vertical</property> + <property name="valign">center</property> + <child> + <object class="GtkLabel"> + <property name="halign">start</property> + <property name="label" translatable="yes">Extension Updates Ready</property> + <style> + <class name="heading"/> + </style> + </object> + </child> + <child> + <object class="GtkLabel" id="updatesLabel"> + <property name="halign">start</property> + </object> + </child> + </object> + </child> + <child type="end"> + <object class="GtkButton"> + <property name="label" translatable="yes">Log Out…</property> + <property name="valign">center</property> + <property name="action-name">win.logout</property> + <property name="receives-default">True</property> + <style> + <class name="suggested-action"/> + </style> + </object> + </child> + </object> + </child> + </object> + </child> + </template> +</interface> diff --git a/subprojects/extensions-app/generate-translations.sh b/subprojects/extensions-app/generate-translations.sh new file mode 100755 index 0000000..591eb76 --- /dev/null +++ b/subprojects/extensions-app/generate-translations.sh @@ -0,0 +1,19 @@ +#!/usr/bin/bash + +cd $(dirname $0) + +sed -e '/subprojects\/extensions-app/!d' \ + -e 's:subprojects/extensions-app/::' ../../po/POTFILES.in > po/POTFILES.in + +for l in $(<po/LINGUAS) +do + cp ../../po/$l.po po/$l.po +done + +builddir=$(mktemp -d -p.) + +meson setup $builddir +meson compile -C $builddir gnome-extensions-app-pot +meson compile -C $builddir gnome-extensions-app-update-po + +rm -rf $builddir diff --git a/subprojects/extensions-app/js/gnome-extensions-app.in b/subprojects/extensions-app/js/gnome-extensions-app.in new file mode 100644 index 0000000..dc6d627 --- /dev/null +++ b/subprojects/extensions-app/js/gnome-extensions-app.in @@ -0,0 +1,2 @@ +#!/bin/sh +@gjs@ @pkgdatadir@/@app_id@ "$@" diff --git a/subprojects/extensions-app/js/main.js b/subprojects/extensions-app/js/main.js new file mode 100644 index 0000000..792cc9e --- /dev/null +++ b/subprojects/extensions-app/js/main.js @@ -0,0 +1,548 @@ +/* exported main */ +imports.gi.versions.Adw = '1'; +imports.gi.versions.Gtk = '4.0'; + +const Gettext = imports.gettext; +const Package = imports.package; +const { Adw, GLib, Gio, GObject, Gtk, Shew } = imports.gi; + +Package.initFormat(); + +const ExtensionUtils = imports.misc.extensionUtils; + +const { ExtensionState, ExtensionType } = ExtensionUtils; + +const GnomeShellIface = loadInterfaceXML('org.gnome.Shell.Extensions'); +const GnomeShellProxy = Gio.DBusProxy.makeProxyWrapper(GnomeShellIface); + +Gio._promisify(Gio.DBusConnection.prototype, 'call'); +Gio._promisify(Shew.WindowExporter.prototype, 'export'); + +function loadInterfaceXML(iface) { + const uri = `resource:///org/gnome/Extensions/dbus-interfaces/${iface}.xml`; + const f = Gio.File.new_for_uri(uri); + + try { + let [ok_, bytes] = f.load_contents(null); + return new TextDecoder().decode(bytes); + } catch (e) { + log(`Failed to load D-Bus interface ${iface}`); + } + + return null; +} + +function toggleState(action) { + let state = action.get_state(); + action.change_state(new GLib.Variant('b', !state.get_boolean())); +} + +var Application = GObject.registerClass( +class Application extends Adw.Application { + _init() { + GLib.set_prgname('gnome-extensions-app'); + super._init({ application_id: Package.name }); + + this.connect('window-removed', (a, window) => window.run_dispose()); + } + + get shellProxy() { + return this._shellProxy; + } + + vfunc_activate() { + this._shellProxy.CheckForUpdatesAsync().catch(logError); + this._window.present(); + } + + vfunc_startup() { + super.vfunc_startup(); + + this.add_action_entries( + [{ + name: 'quit', + activate: () => this._window.close(), + }]); + + this.set_accels_for_action('app.quit', ['<Primary>q']); + + this._shellProxy = new GnomeShellProxy(Gio.DBus.session, + 'org.gnome.Shell.Extensions', '/org/gnome/Shell/Extensions'); + + this._window = new ExtensionsWindow({ application: this }); + } +}); + +var ExtensionsWindow = GObject.registerClass({ + GTypeName: 'ExtensionsWindow', + Template: 'resource:///org/gnome/Extensions/ui/extensions-window.ui', + InternalChildren: [ + 'userGroup', + 'userList', + 'systemGroup', + 'systemList', + 'mainStack', + 'searchBar', + 'searchButton', + 'searchEntry', + 'updatesBar', + 'updatesLabel', + ], +}, class ExtensionsWindow extends Gtk.ApplicationWindow { + _init(params) { + super._init(params); + + this._updatesCheckId = 0; + + this._exporter = new Shew.WindowExporter({ window: this }); + this._exportedHandle = ''; + + this.add_action_entries( + [{ + name: 'show-about', + activate: () => this._showAbout(), + }, { + name: 'logout', + activate: () => this._logout(), + }, { + name: 'user-extensions-enabled', + state: 'false', + change_state: (a, state) => { + this._shellProxy.UserExtensionsEnabled = state.get_boolean(); + }, + }]); + + this._searchTerms = []; + this._searchEntry.connect('search-changed', () => { + const { text } = this._searchEntry; + if (text === '') + this._searchTerms = []; + else + [this._searchTerms] = GLib.str_tokenize_and_fold(text, null); + + this._userList.invalidate_filter(); + this._systemList.invalidate_filter(); + }); + + this._userList.set_sort_func(this._sortList.bind(this)); + this._userList.set_filter_func(this._filterList.bind(this)); + this._userList.set_placeholder(new Gtk.Label({ + label: _('No Matches'), + margin_start: 12, + margin_end: 12, + margin_top: 12, + margin_bottom: 12, + })); + this._userList.connect('row-activated', (_list, row) => row.activate()); + + this._systemList.set_sort_func(this._sortList.bind(this)); + this._systemList.set_filter_func(this._filterList.bind(this)); + this._systemList.set_placeholder(new Gtk.Label({ + label: _('No Matches'), + margin_start: 12, + margin_end: 12, + margin_top: 12, + margin_bottom: 12, + })); + this._systemList.connect('row-activated', (_list, row) => row.activate()); + + this._shellProxy.connectSignal('ExtensionStateChanged', + this._onExtensionStateChanged.bind(this)); + + this._shellProxy.connect('g-properties-changed', + this._onUserExtensionsEnabledChanged.bind(this)); + this._onUserExtensionsEnabledChanged(); + + this._scanExtensions(); + } + + get _shellProxy() { + return this.application.shellProxy; + } + + uninstall(uuid) { + let row = this._findExtensionRow(uuid); + + let dialog = new Gtk.MessageDialog({ + transient_for: this, + modal: true, + text: _('Remove “%s”?').format(row.name), + secondary_text: _('If you remove the extension, you need to return to download it if you want to enable it again'), + }); + + dialog.add_button(_('Cancel'), Gtk.ResponseType.CANCEL); + dialog.add_button(_('Remove'), Gtk.ResponseType.ACCEPT) + .get_style_context().add_class('destructive-action'); + + dialog.connect('response', (dlg, response) => { + if (response === Gtk.ResponseType.ACCEPT) + this._shellProxy.UninstallExtensionAsync(uuid).catch(logError); + dialog.destroy(); + }); + dialog.present(); + } + + async openPrefs(uuid) { + if (!this._exportedHandle) { + try { + this._exportedHandle = await this._exporter.export(); + } catch (e) { + log(`Failed to export window: ${e.message}`); + } + } + + this._shellProxy.OpenExtensionPrefsAsync(uuid, + this._exportedHandle, + {modal: new GLib.Variant('b', true)}).catch(logError); + } + + _showAbout() { + let aboutWindow = new Adw.AboutWindow({ + developers: [ + 'Florian Müllner <fmuellner@gnome.org>', + 'Jasper St. Pierre <jstpierre@mecheye.net>', + 'Didier Roche <didrocks@ubuntu.com>', + 'Romain Vigier <contact@romainvigier.fr>', + ], + designers: [ + 'Allan Day <allanpday@gmail.com>', + 'Tobias Bernard <tbernard@gnome.org>', + ], + translator_credits: _('translator-credits'), + application_name: _('Extensions'), + license_type: Gtk.License.GPL_2_0, + application_icon: Package.name, + version: Package.version, + developer_name: _('The GNOME Project'), + website: 'https://apps.gnome.org/app/org.gnome.Extensions/', + issue_url: 'https://gitlab.gnome.org/GNOME/gnome-shell/issues/new', + + transient_for: this, + }); + aboutWindow.present(); + } + + _logout() { + this.application.get_dbus_connection().call( + 'org.gnome.SessionManager', + '/org/gnome/SessionManager', + 'org.gnome.SessionManager', + 'Logout', + new GLib.Variant('(u)', [0]), + null, + Gio.DBusCallFlags.NONE, + -1, + null); + } + + _sortList(row1, row2) { + return row1.name.localeCompare(row2.name); + } + + _filterList(row) { + return this._searchTerms.every( + t => row.keywords.some(k => k.startsWith(t))); + } + + _findExtensionRow(uuid) { + return [ + ...this._userList, + ...this._systemList, + ].find(c => c.uuid === uuid); + } + + _onUserExtensionsEnabledChanged() { + let action = this.lookup_action('user-extensions-enabled'); + action.set_state( + new GLib.Variant('b', this._shellProxy.UserExtensionsEnabled)); + } + + _onExtensionStateChanged(proxy, senderName, [uuid, newState]) { + let extension = ExtensionUtils.deserializeExtension(newState); + let row = this._findExtensionRow(uuid); + + this._queueUpdatesCheck(); + + // the extension's type changed; remove the corresponding row + // and reset the variable to null so that we create a new row + // below and add it to the appropriate list + if (row && row.type !== extension.type) { + row.get_parent().remove(row); + row = null; + } + + if (row) { + if (extension.state === ExtensionState.UNINSTALLED) + row.get_parent().remove(row); + } else { + this._addExtensionRow(extension); + } + + this._syncListVisibility(); + } + + async _scanExtensions() { + try { + const [extensionsMap] = await this._shellProxy.ListExtensionsAsync(); + + for (let uuid in extensionsMap) { + let extension = ExtensionUtils.deserializeExtension(extensionsMap[uuid]); + this._addExtensionRow(extension); + } + this._extensionsLoaded(); + } catch (e) { + if (e instanceof Gio.DBusError) { + log(`Failed to connect to shell proxy: ${e}`); + this._mainStack.visible_child_name = 'noshell'; + } else { + throw e; + } + } + } + + _addExtensionRow(extension) { + let row = new ExtensionRow(extension); + + if (row.type === ExtensionType.PER_USER) + this._userList.append(row); + else + this._systemList.append(row); + } + + _queueUpdatesCheck() { + if (this._updatesCheckId) + return; + + this._updatesCheckId = GLib.timeout_add_seconds( + GLib.PRIORITY_DEFAULT, 1, () => { + this._checkUpdates(); + + this._updatesCheckId = 0; + return GLib.SOURCE_REMOVE; + }); + } + + _syncListVisibility() { + this._userGroup.visible = [...this._userList].length > 1; + this._systemGroup.visible = [...this._systemList].length > 1; + + if (this._userGroup.visible || this._systemGroup.visible) + this._mainStack.visible_child_name = 'main'; + else + this._mainStack.visible_child_name = 'placeholder'; + } + + _checkUpdates() { + let nUpdates = [...this._userList].filter(c => c.hasUpdate).length; + + this._updatesLabel.label = Gettext.ngettext( + '%d extension will be updated on next login.', + '%d extensions will be updated on next login.', + nUpdates).format(nUpdates); + this._updatesBar.revealed = nUpdates > 0; + } + + _extensionsLoaded() { + this._syncListVisibility(); + this._checkUpdates(); + } +}); + +var ExtensionRow = GObject.registerClass({ + GTypeName: 'ExtensionRow', + Template: 'resource:///org/gnome/Extensions/ui/extension-row.ui', + InternalChildren: [ + 'nameLabel', + 'descriptionLabel', + 'versionLabel', + 'errorLabel', + 'errorIcon', + 'updatesIcon', + 'switch', + 'actionsBox', + ], +}, class ExtensionRow extends Gtk.ListBoxRow { + _init(extension) { + super._init(); + + this._app = Gio.Application.get_default(); + this._extension = extension; + this._prefsModule = null; + + [this._keywords] = GLib.str_tokenize_and_fold(this.name, null); + + this._actionGroup = new Gio.SimpleActionGroup(); + this.insert_action_group('row', this._actionGroup); + + let action; + action = new Gio.SimpleAction({ + name: 'show-prefs', + enabled: this.hasPrefs, + }); + action.connect('activate', () => this.get_root().openPrefs(this.uuid)); + this._actionGroup.add_action(action); + + action = new Gio.SimpleAction({ + name: 'show-url', + enabled: this.url !== '', + }); + action.connect('activate', () => { + Gio.AppInfo.launch_default_for_uri( + this.url, this.get_display().get_app_launch_context()); + }); + this._actionGroup.add_action(action); + + action = new Gio.SimpleAction({ + name: 'uninstall', + enabled: this.type === ExtensionType.PER_USER, + }); + action.connect('activate', () => this.get_root().uninstall(this.uuid)); + this._actionGroup.add_action(action); + + action = new Gio.SimpleAction({ + name: 'enabled', + state: new GLib.Variant('b', false), + }); + action.connect('activate', toggleState); + action.connect('change-state', (a, state) => { + if (state.get_boolean()) + this._app.shellProxy.EnableExtensionAsync(this.uuid).catch(logError); + else + this._app.shellProxy.DisableExtensionAsync(this.uuid).catch(logError); + }); + this._actionGroup.add_action(action); + + this._nameLabel.label = this.name; + + const desc = this._extension.metadata.description.split('\n')[0]; + this._descriptionLabel.label = desc; + this._descriptionLabel.tooltip_text = desc; + + this.connect('destroy', this._onDestroy.bind(this)); + + this._extensionStateChangedId = this._app.shellProxy.connectSignal( + 'ExtensionStateChanged', (p, sender, [uuid, newState]) => { + if (this.uuid !== uuid) + return; + + this._extension = ExtensionUtils.deserializeExtension(newState); + this._updateState(); + }); + this._updateState(); + } + + vfunc_activate() { + this._switch.mnemonic_activate(false); + } + + get uuid() { + return this._extension.uuid; + } + + get name() { + return this._extension.metadata.name; + } + + get hasPrefs() { + return this._extension.hasPrefs; + } + + get hasUpdate() { + return this._extension.hasUpdate || false; + } + + get hasError() { + const { state } = this._extension; + return state === ExtensionState.OUT_OF_DATE || + state === ExtensionState.ERROR; + } + + get type() { + return this._extension.type; + } + + get creator() { + return this._extension.metadata.creator || ''; + } + + get url() { + return this._extension.metadata.url || ''; + } + + get version() { + return this._extension.metadata.version || ''; + } + + get error() { + if (!this.hasError) + return ''; + + if (this._extension.state === ExtensionState.OUT_OF_DATE) + return _('The extension is incompatible with the current GNOME version'); + + return this._extension.error + ? this._extension.error : _('The extension had an error'); + } + + get keywords() { + return this._keywords; + } + + _updateState() { + let state = this._extension.state === ExtensionState.ENABLED; + + let action = this._actionGroup.lookup('enabled'); + action.set_state(new GLib.Variant('b', state)); + action.enabled = this._canToggle(); + + if (!action.enabled) + this._switch.active = state; + + this._updatesIcon.visible = this.hasUpdate; + this._errorIcon.visible = this.hasError; + + this._descriptionLabel.visible = !this.hasError; + + this._errorLabel.label = this.error; + this._errorLabel.visible = this.error !== ''; + + this._versionLabel.label = this.version.toString(); + this._versionLabel.visible = this.version !== ''; + } + + _onDestroy() { + if (!this._app.shellProxy) + return; + + if (this._extensionStateChangedId) + this._app.shellProxy.disconnectSignal(this._extensionStateChangedId); + this._extensionStateChangedId = 0; + } + + _canToggle() { + return this._extension.canChange; + } +}); + +function initEnvironment() { + // Monkey-patch in a "global" object that fakes some Shell utilities + // that ExtensionUtils depends on. + globalThis.global = { + log(...args) { + print(args.join(', ')); + }, + + logError(s) { + log(`ERROR: ${s}`); + }, + + userdatadir: GLib.build_filenamev([GLib.get_user_data_dir(), 'gnome-shell']), + }; +} + +function main(argv) { + initEnvironment(); + Package.initGettext(); + + new Application().run(argv); +} diff --git a/subprojects/extensions-app/js/meson.build b/subprojects/extensions-app/js/meson.build new file mode 100644 index 0000000..ce2a776 --- /dev/null +++ b/subprojects/extensions-app/js/meson.build @@ -0,0 +1,40 @@ +launcherconf = configuration_data() +launcherconf.set('app_id', app_id) +launcherconf.set('PACKAGE_NAME', package_name) +if vcs_tag != '' + launcherconf.set('PACKAGE_VERSION', '@0@ (@1@)'.format(package_version, vcs_tag)) +else + launcherconf.set('PACKAGE_VERSION', package_version) +endif +launcherconf.set('prefix', prefix) +launcherconf.set('libdir', libdir) +launcherconf.set('pkgdatadir', pkgdatadir) +launcherconf.set('gjs', gjs.full_path()) + +configure_file( + input: prgname + '.in', + output: prgname, + configuration: launcherconf, + install_dir: bindir, + install_mode: 'rwxr-xr-x', +) + +configure_file( + input: base_id + '.in', + output: app_id, + configuration: launcherconf, + install_dir: pkgdatadir, +) + +gnome.compile_resources( + app_id + '.src', + configure_file( + input: base_id + '.src.gresource.xml.in', + output: app_id + '.src.gresource.xml', + configuration: {'profile': '/'.join(profile.split('.')) }, + ), + source_dir: ['.', '../../../js'], + gresource_bundle: true, + install: true, + install_dir: pkgdatadir +) diff --git a/subprojects/extensions-app/js/misc/config.js b/subprojects/extensions-app/js/misc/config.js new file mode 100644 index 0000000..d213b78 --- /dev/null +++ b/subprojects/extensions-app/js/misc/config.js @@ -0,0 +1 @@ +/* Fake module to satify import in ExtensionUtils */ diff --git a/subprojects/extensions-app/js/org.gnome.Extensions.in b/subprojects/extensions-app/js/org.gnome.Extensions.in new file mode 100644 index 0000000..da7ab25 --- /dev/null +++ b/subprojects/extensions-app/js/org.gnome.Extensions.in @@ -0,0 +1,6 @@ +imports.package.start({ + name: '@PACKAGE_NAME@', + version: '@PACKAGE_VERSION@', + prefix: '@prefix@', + libdir: '@libdir@', +}); diff --git a/subprojects/extensions-app/js/org.gnome.Extensions.src.gresource.xml.in b/subprojects/extensions-app/js/org.gnome.Extensions.src.gresource.xml.in new file mode 100644 index 0000000..330ede1 --- /dev/null +++ b/subprojects/extensions-app/js/org.gnome.Extensions.src.gresource.xml.in @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="UTF-8"?> +<gresources> + <gresource prefix="/org/gnome/Extensions@profile@/js"> + <file>main.js</file> + + <file>misc/config.js</file> + <file>misc/extensionUtils.js</file> + </gresource> +</gresources> diff --git a/subprojects/extensions-app/logo.png b/subprojects/extensions-app/logo.png Binary files differnew file mode 100644 index 0000000..8872925 --- /dev/null +++ b/subprojects/extensions-app/logo.png diff --git a/subprojects/extensions-app/meson.build b/subprojects/extensions-app/meson.build new file mode 100644 index 0000000..dfb28dc --- /dev/null +++ b/subprojects/extensions-app/meson.build @@ -0,0 +1,90 @@ +project('gnome-extensions-app', + version: '43.9', + meson_version: '>= 0.58.0', + license: 'GPLv2+' +) + +if get_option('profile') == 'development' + profile = '.Devel' + vcs_tag = run_command('git', 'rev-parse', '--short', '@', + check: false, + ).stdout().strip() +else + profile = '' + vcs_tag = '' +endif + +base_id = 'org.gnome.Extensions' +app_id = base_id + profile +prgname = 'gnome-extensions-app' + +gnome = import('gnome') +i18n = import('i18n') + +if meson.is_subproject() + package_name = get_option('package_name') + assert(package_name != '', + 'package_name must be specified for subproject builds') + + po_dir = join_paths(meson.current_source_dir(), '../../po') +else + package_name = meson.project_name() + po_dir = join_paths(meson.current_source_dir(), 'po') +endif + +package_version = meson.project_version() +prefix = get_option('prefix') + +bindir = join_paths(prefix, get_option('bindir')) +libdir = join_paths(prefix, get_option('libdir')) +datadir = join_paths(prefix, get_option('datadir')) +pkgdatadir = join_paths(datadir, package_name) + +desktopdir = join_paths(datadir, 'applications') +icondir = join_paths(datadir, 'icons') +localedir = join_paths(datadir, 'locale') +metainfodir = join_paths(datadir, 'metainfo') +servicedir = join_paths(datadir, 'dbus-1', 'services') + +gjs = find_program('gjs') +appstream_util = find_program('appstream-util', required: false) +desktop_file_validate = find_program('desktop-file-validate', required: false) + +subdir('data') +subdir('js') + +if not meson.is_subproject() + subproject('shew', + default_options: [ + 'package_name=@0@'.format(meson.project_name()), + ] + ) + + subdir('po') + + gnome.post_install( + gtk_update_icon_cache: true + ) + + if appstream_util.found() + meson.add_dist_script('build-aux/meson/check-version.py', + meson.project_version(), + '--type=metainfo', + 'data/metainfo/org.gnome.Extensions.metainfo.xml.in') + endif + + summary_dirs = { + 'prefix': get_option('prefix'), + 'bindir': get_option('bindir'), + 'libdir': get_option('bindir'), + 'datadir': get_option('datadir'), + } + + summary_build = { + 'buildtype': get_option('buildtype'), + 'debug': get_option('debug'), + } + + summary(summary_dirs, section: 'Directories') + summary(summary_build, section: 'Build Configuration') +endif diff --git a/subprojects/extensions-app/meson_options.txt b/subprojects/extensions-app/meson_options.txt new file mode 100644 index 0000000..ca2eb41 --- /dev/null +++ b/subprojects/extensions-app/meson_options.txt @@ -0,0 +1,12 @@ +option('package_name', + type: 'string', + description: 'The gettext domain name', +) +option('profile', + type: 'combo', + choices: [ + 'default', + 'development' + ], + value: 'default' +) diff --git a/subprojects/extensions-app/po/.gitignore b/subprojects/extensions-app/po/.gitignore new file mode 100644 index 0000000..3b2228d --- /dev/null +++ b/subprojects/extensions-app/po/.gitignore @@ -0,0 +1,3 @@ +*.po +*.pot +POTFILES.in diff --git a/subprojects/extensions-app/po/LINGUAS b/subprojects/extensions-app/po/LINGUAS new file mode 120000 index 0000000..4fb83a5 --- /dev/null +++ b/subprojects/extensions-app/po/LINGUAS @@ -0,0 +1 @@ +../../../po/LINGUAS
\ No newline at end of file diff --git a/subprojects/extensions-app/po/meson.build b/subprojects/extensions-app/po/meson.build new file mode 100644 index 0000000..5a1b0e2 --- /dev/null +++ b/subprojects/extensions-app/po/meson.build @@ -0,0 +1 @@ +i18n.gettext(package_name, preset: 'glib') diff --git a/subprojects/extensions-app/subprojects/shew b/subprojects/extensions-app/subprojects/shew new file mode 120000 index 0000000..50988a9 --- /dev/null +++ b/subprojects/extensions-app/subprojects/shew @@ -0,0 +1 @@ +../../shew/
\ No newline at end of file diff --git a/subprojects/extensions-tool/COPYING b/subprojects/extensions-tool/COPYING new file mode 100644 index 0000000..10926e8 --- /dev/null +++ b/subprojects/extensions-tool/COPYING @@ -0,0 +1,675 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/> + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. 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 +them 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 prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. 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. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey 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; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If 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 convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU 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 that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + 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. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +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. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + 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 +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + <one line to give the program's name and a brief idea of what it does.> + Copyright (C) <year> <name of author> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + <program> Copyright (C) <year> <name of author> + This program 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, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +<http://www.gnu.org/licenses/>. + + The GNU 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. But first, please read +<http://www.gnu.org/philosophy/why-not-lgpl.html>. + diff --git a/subprojects/extensions-tool/README.md b/subprojects/extensions-tool/README.md new file mode 100644 index 0000000..dc2e2d7 --- /dev/null +++ b/subprojects/extensions-tool/README.md @@ -0,0 +1,23 @@ +# gnome-extensions-tool +gnome-extensions-tool is a command line utility for managing +GNOME Shell extensions. It is usually built as part of gnome-shell, +but can be used as a stand-alone project as well (for example to +create an extension bundle as part of continuous integration). + +Bugs should be reported to the GNOME [bug tracking system][bug-tracker]. + +## Building +Before the project can be built stand-alone, the po directory has +to be populated with translations (from gnome-shell). + +To do that, simply run the included script: +```sh +$ ./generate-translations.sh +``` + +## License +gnome-extensions-tool is distributed under the terms of the GNU General Public +License, version 3 or later. See the [COPYING][license] file for details. + +[bug-tracker]: https://gitlab.gnome.org/GNOME/gnome-shell/issues +[license]: COPYING diff --git a/subprojects/extensions-tool/completion/bash/gnome-extensions b/subprojects/extensions-tool/completion/bash/gnome-extensions new file mode 100644 index 0000000..05cd039 --- /dev/null +++ b/subprojects/extensions-tool/completion/bash/gnome-extensions @@ -0,0 +1,91 @@ + +# Check for bash +[ -z "$BASH_VERSION" ] && return + +################################################################################ + +__gnome_extensions() { + local commands="version enable disable reset info install show list create pack prefs uninstall" + local COMMAND=${COMP_WORDS[1]} + + _init_completion -s || return + + case "${COMP_CWORD}" in + 1) + COMPREPLY=($(compgen -W "help $commands" -- "$2")) + return 0 + ;; + + 2) + case "$COMMAND" in + help) + COMPREPLY=($(compgen -W "$commands" -- "$2")) + return 0 + ;; + + disable) + local list_opt=--enabled + ;;& + enable) + local list_opt=--disabled + ;;& + prefs) + local list_opt=--prefs + ;;& + uninstall) + local list_opt=--user + ;;& + enable|disable|info|show|prefs|reset|uninstall) + COMPREPLY=($(compgen -W "`gnome-extensions list $list_opt`" -- "$2")) + return 0 + ;; + esac + ;; + esac + + case "$COMMAND" in + create) + case "$prev" in + --template) + COMPREPLY=($(compgen -W "`gnome-extensions create --list-templates`" -- "$2")) + return 0 + ;; + esac + ;; + pack) + case "$prev" in + --podir|--out-dir|-o) + _filedir -d + return 0 + ;; + --schema) + _filedir gschema.xml + return 0 + ;; + --extra-source) + _filedir + return 0 + ;; + esac + ;; + install) + if [[ $cur != -* ]] + then + _filedir zip + return 0 + fi + ;; + esac + + # Stop if we are currently waiting for an option value + $split && return + + # Otherwise, get the supported options for ${COMMAND} (if any) + COMPREPLY=($(compgen -W "$(_parse_help $1 "help $COMMAND")" -- "$2")) + [[ $COMPREPLY == *= ]] && compopt -o nospace + return 0 +} + +################################################################################ + +complete -F __gnome_extensions gnome-extensions diff --git a/subprojects/extensions-tool/generate-translations.sh b/subprojects/extensions-tool/generate-translations.sh new file mode 100755 index 0000000..3521a44 --- /dev/null +++ b/subprojects/extensions-tool/generate-translations.sh @@ -0,0 +1,19 @@ +#!/usr/bin/bash + +cd $(dirname $0) + +sed -e '/subprojects\/extensions-tool/!d' \ + -e 's:subprojects/extensions-tool/::' ../../po/POTFILES.in > po/POTFILES.in + +for l in $(<po/LINGUAS) +do + cp ../../po/$l.po po/$l.po +done + +builddir=$(mktemp -d -p.) + +meson setup -Dman=False $builddir +meson compile -C $builddir gnome-extensions-tool-pot +meson compile -C $builddir gnome-extensions-tool-update-po + +rm -rf $builddir diff --git a/subprojects/extensions-tool/man/gnome-extensions.1 b/subprojects/extensions-tool/man/gnome-extensions.1 new file mode 100644 index 0000000..d106682 --- /dev/null +++ b/subprojects/extensions-tool/man/gnome-extensions.1 @@ -0,0 +1,297 @@ +'\" t +.\" Title: gnome-extensions +.\" Author: [FIXME: author] [see http://www.docbook.org/tdg5/en/html/author] +.\" Generator: DocBook XSL Stylesheets vsnapshot <http://docbook.sf.net/> +.\" Date: August 2018 +.\" Manual: User Commands +.\" Source: GNOME-EXTENSIONS-TOOL +.\" Language: English +.\" +.TH "GNOME\-EXTENSIONS" "1" "August 2018" "GNOME\-EXTENSIONS\-TOOL" "User Commands" +.\" ----------------------------------------------------------------- +.\" * Define some portability stuff +.\" ----------------------------------------------------------------- +.\" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.\" http://bugs.debian.org/507673 +.\" http://lists.gnu.org/archive/html/groff/2009-02/msg00013.html +.\" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.ie \n(.g .ds Aq \(aq +.el .ds Aq ' +.\" ----------------------------------------------------------------- +.\" * set default formatting +.\" ----------------------------------------------------------------- +.\" disable hyphenation +.nh +.\" disable justification (adjust text to left margin only) +.ad l +.\" ----------------------------------------------------------------- +.\" * MAIN CONTENT STARTS HERE * +.\" ----------------------------------------------------------------- +.SH "NAME" +gnome-extensions \- Command line tool for managing GNOME extensions +.SH "SYNOPSIS" +.sp +\fBgnome\-extensions\fR help [\fICOMMAND\fR] +.sp +\fBgnome\-extensions\fR version +.sp +\fBgnome\-extensions\fR enable \fIUUID\fR +.sp +\fBgnome\-extensions\fR disable \fIUUID\fR +.sp +\fBgnome\-extensions\fR reset \fIUUID\fR +.sp +\fBgnome\-extensions\fR info \fIUUID\fR +.sp +\fBgnome\-extensions\fR show \fIUUID\fR +.sp +\fBgnome\-extensions\fR list [\fIOPTION\fR\&...] +.sp +\fBgnome\-extensions\fR prefs \fIUUID\fR +.sp +\fBgnome\-extensions\fR create [\fIOPTION\fR\&...] +.sp +\fBgnome\-extensions\fR pack [\fIOPTION\fR\&...] +.sp +\fBgnome\-extensions\fR install [\fIOPTION\fR\&...] \fIPACK\fR +.sp +\fBgnome\-extensions\fR uninstall \fIUUID\fR +.SH "DESCRIPTION" +.sp +\fBgnome\-extensions\fR is a utility that makes some common GNOME extensions operations available on the command line\&. +.SH "COMMON OPTIONS" +.sp +All commands except for \fBhelp\fR and \fBversion\fR handle the following options: +.PP +\fB\-\-quiet\fR, \fB\-q\fR +.RS 4 +Do not print error messages +.RE +.SH "COMMANDS" +.PP +\fBhelp\fR [\fICOMMAND\fR] +.RS 4 +Displays a short synopsis of the available commands or provides detailed help on a specific command\&. +.RE +.PP +\fBversion\fR +.RS 4 +Prints the program version\&. +.RE +.PP +\fBenable\fR \fIUUID\fR +.RS 4 +Enables the extension identified by +\fIUUID\fR\&. +.sp +The command will not detect any errors from the extension itself, use the +\fBinfo\fR +command to confirm that the extension state is +\fBENABLED\fR\&. +.sp +If the extension is already enabled, the command will do nothing\&. +.RE +.PP +\fBdisable\fR \fIUUID\fR +.RS 4 +Disables the extension identified by +\fIUUID\fR\&. +.sp +If the extension is not enabled, the command will do nothing\&. +.RE +.PP +\fBreset\fR \fIUUID\fR +.RS 4 +Reset the extension identified by +\fIUUID\fR\&. +.sp +The extension will be disabled in GNOME, but may be enabled by other sessions like GNOME Classic\&. +.RE +.PP +\fBinfo\fR \fIUUID\fR +.RS 4 +Show details of the extension identified by +\fIUUID\fR, including name, description and state\&. +.RE +.PP +\fBshow\fR \fIUUID\fR +.RS 4 +Synonym of info\&. +.RE +.PP +\fBlist\fR [\fIOPTION\fR\&...] +.RS 4 +Displays a list of installed extensions\&. +.PP +\fBOptions\fR +.RS 4 +.\".PP +\fB\-\-user\fR +.RS 4 +Include extensions installed in the user\(cqs +\fB$HOME\fR +.RE +.PP +\fB\-\-system\fR +.RS 4 +Include extensions installed in the system +.RE +.PP +\fB\-\-enabled\fR +.RS 4 +Include enabled extensions +.RE +.PP +\fB\-\-disabled\fR +.RS 4 +Include disabled extensions +.RE +.PP +\fB\-\-prefs\fR +.RS 4 +Only include extensions with preferences +.RE +.PP +\fB\-\-updates\fR +.RS 4 +Only include extensions with pending updates +.RE +.PP +\fB\-d\fR, \fB\-\-details\fR +.RS 4 +Show some extra information for each extension +.RE +.RE +.RE +.PP +\fBprefs\fR \fIUUID\fR +.RS 4 +Open the preference dialog of the extension identified by +\fIUUID\fR\&. +.RE +.PP +\fBcreate\fR [\fIOPTION\fR\&...] +.RS 4 +Creates a new extension from a template\&. +.PP +\fBOptions\fR +.RS 4 +.\".PP +\fB\-\-name\fR=\fINAME\fR +.RS 4 +Set the user\-visible name in the extension\(cqs metadata to +\fINAME\fR +.RE +.PP +\fB\-\-description\fR=\fIDESC\fR +.RS 4 +Set the description in the extension\(cqs metadata to +\fIDESC\fR +.RE +.PP +\fB\-\-uuid\fR=\fIUUID\fR +.RS 4 +Set the unique extension ID in the metadata to +\fIUUID\fR +.RE +.PP +\fB\-\-template\fR=\fITEMPLATE\fR +.RS 4 +Use +\fITEMPLATE\fR +as base for the new extension +.RE +.PP +\fB\-i\fR, \fB\-\-interactive\fR +.RS 4 +Prompt for any extension metadata that hasn\(cqt been provided on the command line +.RE +.RE +.RE +.PP +\fBpack\fR [\fIOPTION\fR\&...] [\fISOURCE\-DIRECTORY\fR] +.RS 4 +Creates an extension bundle that is suitable for publishing\&. +.sp +The bundle will always include the required files extension\&.js and metadata\&.json, as well as the optional stylesheet\&.css and prefs\&.js if found\&. Each additional source that should be included must be specified with +\fB\-\-extra\-source\fR\&. +.sp +If the extension includes one or more GSettings schemas, they can either be placed in a schemas/ folder to be picked up automatically, or be specified with +\fB\-\-schema\fR\&. +.sp +Similarily, translations are included automatically when they are located in a po/ folder, otherwise the +\fB\-\-podir\fR +option can be used to point to the correct directory\&. If no gettext domain is provided on the command line, the value of the +\fBgettext\-domain\fR +metadata field is used if it exists, and the extension UUID if not\&. +.sp +All files are searched in +\fISOURCE\-DIRECTORY\fR +if specified, or the current directory otherwise\&. +.PP +\fBOptions\fR +.RS 4 +.\".PP +\fB\-\-extra\-source\fR=\fIFILE\fR +.RS 4 +Additional source to include in the bundle +.RE +.PP +\fB\-\-schema\fR=\fISCHEMA\fR +.RS 4 +A GSettings schema that should be compiled and included +.RE +.PP +\fB\-\-podir\fR=\fIPODIR\fR +.RS 4 +A directory with translations that should be compiled and included +.RE +.PP +\fB\-\-gettext\-domain\fR=\fIDOMAIN\fR +.RS 4 +The gettext domain to use for translations +.RE +.PP +\fB\-f\fR, \fB\-\-force\fR +.RS 4 +Overwrite an existing pack +.RE +.PP +\fB\-o\fR, \fB\-\-out\-dir\fR=\fIDIRECTORY\fR +.RS 4 +The directory where the pack should be created +.RE +.RE +.RE +.PP +\fBinstall\fR [\fIOPTION\fR\&...] \fIPACK\fR +.RS 4 +Installs an extension from the bundle +\fIPACK\fR\&. +.sp +The command unpacks the extension files and moves them to the expected location in the user\(cqs +\fB$HOME\fR, so that it will be loaded in the next session\&. +.sp +It is mainly intended for testing, not as a replacement for the extension website\&. As extensions have privileged access to the user\(cqs session, it is advised to never load extensions from untrusted sources without carefully reviewing their content\&. +.PP +\fBOptions\fR +.RS 4 +.\".PP +\fB\-\-force\fR +.RS 4 +Override an existing extension +.RE +.RE +.RE +.PP +\fBuninstall\fR \fIUUID\fR +.RS 4 +Uninstalls the extension identified by +\fIUUID\fR\&. +.RE +.SH "EXIT STATUS" +.sp +On success 0 is returned, a non\-zero failure code otherwise\&. +.SH "BUGS" +.sp +The tool is part of the gnome\-shell project, and bugs should be reported in its issue tracker at \m[blue]\fBhttps://gitlab\&.gnome\&.org/GNOME/gnome\-shell/issues\fR\m[]\&. diff --git a/subprojects/extensions-tool/man/gnome-extensions.txt b/subprojects/extensions-tool/man/gnome-extensions.txt new file mode 100644 index 0000000..85d657b --- /dev/null +++ b/subprojects/extensions-tool/man/gnome-extensions.txt @@ -0,0 +1,211 @@ +GNOME-EXTENSIONS(1) +=================== +:man manual: User Commands +:man source: GNOME-EXTENSIONS-TOOL +:doctype: manpage +:date: August 2018 + +NAME +---- +gnome-extensions - Command line tool for managing GNOME extensions + +SYNOPSIS +-------- +*gnome-extensions* help ['COMMAND'] + +*gnome-extensions* version + +*gnome-extensions* enable 'UUID' + +*gnome-extensions* disable 'UUID' + +*gnome-extensions* reset 'UUID' + +*gnome-extensions* info 'UUID' + +*gnome-extensions* show 'UUID' + +*gnome-extensions* list ['OPTION'...] + +*gnome-extensions* prefs 'UUID' + +*gnome-extensions* create ['OPTION'...] + +*gnome-extensions* pack ['OPTION'...] + +*gnome-extensions* install ['OPTION'...] 'PACK' + +*gnome-extensions* uninstall 'UUID' + +DESCRIPTION +----------- +*gnome-extensions* is a utility that makes some common GNOME extensions +operations available on the command line. + +COMMON OPTIONS +-------------- +All commands except for *help* and *version* handle the following options: + +*--quiet*, *-q*:: +Do not print error messages + +COMMANDS +-------- +*help* ['COMMAND']:: +Displays a short synopsis of the available commands or provides +detailed help on a specific command. + +*version*:: +Prints the program version. + +*enable* 'UUID':: +Enables the extension identified by 'UUID'. ++ +The command will not detect any errors from the extension itself, use the +*info* command to confirm that the extension state is *ENABLED*. ++ +If the extension is already enabled, the command will do nothing. + +*disable* 'UUID':: +Disables the extension identified by 'UUID'. ++ +If the extension is not enabled, the command will do nothing. + +*reset* 'UUID':: +Reset the extension identified by 'UUID'. ++ +The extension will be disabled in GNOME, but may be enabled by other sessions +like GNOME Classic. + +*info* 'UUID':: +Show details of the extension identified by 'UUID', including name, +description and state. + +*show* 'UUID':: +Synonym of info. + +*list* ['OPTION'...]:: +Displays a list of installed extensions. ++ +.Options + *--user*;; + Include extensions installed in the user's *$HOME* + + *--system*;; + Include extensions installed in the system + + *--enabled*;; + Include enabled extensions + + *--disabled*;; + Include disabled extensions + + *--prefs*;; + Only include extensions with preferences + + *--updates*;; + Only include extensions with pending updates + + *-d*;; + *--details*;; + Show some extra information for each extension + +*prefs* 'UUID':: +Open the preference dialog of the extension identified by 'UUID'. + + +*create* ['OPTION'...]:: +Creates a new extension from a template. ++ +.Options + *--name*='NAME'::: + Set the user-visible name in the extension's metadata + to 'NAME' + + *--description*='DESC'::: + Set the description in the extension's metadata to 'DESC' + + *--uuid*='UUID'::: + Set the unique extension ID in the metadata to 'UUID' + + *--template*='TEMPLATE'::: + Use 'TEMPLATE' as base for the new extension + + *-i*::: + *--interactive*::: + Prompt for any extension metadata that hasn't been provided + on the command line + +*pack* ['OPTION'...] ['SOURCE-DIRECTORY']:: +Creates an extension bundle that is suitable for publishing. ++ +The bundle will always include the required files extension.js +and metadata.json, as well as the optional stylesheet.css and +prefs.js if found. Each additional source that should be included +must be specified with *--extra-source*. ++ +If the extension includes one or more GSettings schemas, they can +either be placed in a schemas/ folder to be picked up automatically, +or be specified with *--schema*. ++ +Similarily, translations are included automatically when they are +located in a po/ folder, otherwise the *--podir* option can be +used to point to the correct directory. If no gettext domain is +provided on the command line, the value of the *gettext-domain* +metadata field is used if it exists, and the extension UUID +if not. ++ +All files are searched in 'SOURCE-DIRECTORY' if specified, or +the current directory otherwise. ++ +.Options + *--extra-source*='FILE'::: + Additional source to include in the bundle + + *--schema*='SCHEMA'::: + A GSettings schema that should be compiled and + included + + *--podir*='PODIR'::: + A directory with translations that should be + compiled and included + + *--gettext-domain*='DOMAIN'::: + The gettext domain to use for translations + + *-f*::: + *--force*::: + Overwrite an existing pack + + *-o*::: + *--out-dir*='DIRECTORY'::: + The directory where the pack should be created + +*install* ['OPTION'...] 'PACK':: +Installs an extension from the bundle 'PACK'. ++ +The command unpacks the extension files and moves them to +the expected location in the user's *$HOME*, so that it +will be loaded in the next session. ++ +It is mainly intended for testing, not as a replacement for +the extension website. As extensions have privileged access +to the user's session, it is advised to never load extensions +from untrusted sources without carefully reviewing their content. ++ +.Options + *--force*::: + Override an existing extension + +*uninstall* 'UUID':: +Uninstalls the extension identified by 'UUID'. + + +EXIT STATUS +----------- +On success 0 is returned, a non-zero failure code otherwise. + +BUGS +---- +The tool is part of the gnome-shell project, and bugs should be reported +in its issue tracker at https://gitlab.gnome.org/GNOME/gnome-shell/issues. diff --git a/subprojects/extensions-tool/man/meson.build b/subprojects/extensions-tool/man/meson.build new file mode 100644 index 0000000..643509c --- /dev/null +++ b/subprojects/extensions-tool/man/meson.build @@ -0,0 +1,7 @@ +custom_target('gnome-extensions.1', + input: ['gnome-extensions.txt', 'stylesheet.xsl'], + output: 'gnome-extensions.1', + command: [a2x, '-D', '@OUTDIR@', '--xsl-file', '@INPUT1@', '-f', 'manpage', '@INPUT0@'], + install_dir: mandir + '/man1', + install: true +) diff --git a/subprojects/extensions-tool/man/stylesheet.xsl b/subprojects/extensions-tool/man/stylesheet.xsl new file mode 100644 index 0000000..047bd1b --- /dev/null +++ b/subprojects/extensions-tool/man/stylesheet.xsl @@ -0,0 +1,27 @@ +<?xml version='1.0'?> +<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" + version='1.0'> +<xsl:import href="http://docbook.sourceforge.net/release/xsl/current/manpages/docbook.xsl"/> + +<xsl:template match="variablelist/title"> + <xsl:text>.PP </xsl:text> + <xsl:call-template name="bold"> + <xsl:with-param name="node" select="."/> + <xsl:with-param name="context" select=".."/> + </xsl:call-template> + <xsl:text> </xsl:text> +</xsl:template> + +<xsl:template match="varlistentry[preceding-sibling::title]"> + <xsl:if test="not(preceding-sibling::varlistentry)"> + <xsl:text>.RS 4 </xsl:text> + <!-- comment out the leading .PP added by the original template --> + <xsl:text>.\"</xsl:text> + </xsl:if> + <xsl:apply-imports/> + <xsl:if test="position() = last()"> + <xsl:text>.RE </xsl:text> + </xsl:if> +</xsl:template> + +</xsl:stylesheet> diff --git a/subprojects/extensions-tool/meson.build b/subprojects/extensions-tool/meson.build new file mode 100644 index 0000000..11e48d9 --- /dev/null +++ b/subprojects/extensions-tool/meson.build @@ -0,0 +1,84 @@ +project('gnome-extensions-tool', 'c', + version: '43.9', + meson_version: '>= 0.58.0', + license: 'GPLv2+' +) + +gio_req = '>= 2.56.0' + +fs = import('fs') +gnome = import('gnome') +i18n = import('i18n') + +if meson.is_subproject() + package_name = get_option('package_name') + assert(package_name != '', + 'package_name must be specified for subproject builds') +else + package_name = meson.project_name() +endif + +package_version = meson.project_version() +prefix = get_option('prefix') + +bindir = join_paths(prefix, get_option('bindir')) +datadir = join_paths(prefix, get_option('datadir')) +mandir = join_paths(prefix, get_option('mandir')) + +localedir = join_paths(datadir, 'locale') + +gio_dep = dependency('gio-2.0', version: gio_req) +gio_unix_dep = dependency('gio-unix-2.0', version: gio_req) +autoar_dep = dependency('gnome-autoar-0') +json_dep = dependency('json-glib-1.0') + +cc = meson.get_compiler('c') + +bash_completion = dependency('bash-completion', required: get_option('bash_completion')) + +po_dir = meson.global_source_root() + '/po' + +subdir('src') + +if bash_completion.found() + install_data('completion/bash/gnome-extensions', + install_dir: bash_completion.get_variable('completionsdir', pkgconfig_define: ['datadir', datadir]) + ) +endif + +if get_option('man') + if fs.exists('man/gnome-extensions.1') + install_man('man/gnome-extensions.1') + else + a2x = find_program('a2x') + subdir('man') + endif +endif + +if not meson.is_subproject() + subdir('po') + + summary_dirs = { + 'prefix': get_option('prefix'), + 'bindir': get_option('bindir'), + 'datadir': get_option('datadir'), + } + + if get_option('man') + summary_dirs += { 'mandir': get_option('mandir') } + endif + + summary_build = { + 'buildtype': get_option('buildtype'), + 'debug': get_option('debug'), + } + + summary_options = { + 'man': get_option('man'), + 'bash_completion': bash_completion.found(), + } + + summary(summary_dirs, section: 'Directories') + summary(summary_build, section: 'Build Configuration') + summary(summary_options, section: 'Build Options') +endif diff --git a/subprojects/extensions-tool/meson_options.txt b/subprojects/extensions-tool/meson_options.txt new file mode 100644 index 0000000..fb6e370 --- /dev/null +++ b/subprojects/extensions-tool/meson_options.txt @@ -0,0 +1,17 @@ +option('man', + type: 'boolean', + value: true, + description: 'Generate man pages', + yield: true, +) + +option('bash_completion', + type: 'feature', + value: 'auto', + description: 'Install bash completion support', +) + +option('package_name', + type: 'string', + description: 'The gettext domain name when used as a subproject' +) diff --git a/subprojects/extensions-tool/po/.gitignore b/subprojects/extensions-tool/po/.gitignore new file mode 100644 index 0000000..3b2228d --- /dev/null +++ b/subprojects/extensions-tool/po/.gitignore @@ -0,0 +1,3 @@ +*.po +*.pot +POTFILES.in diff --git a/subprojects/extensions-tool/po/LINGUAS b/subprojects/extensions-tool/po/LINGUAS new file mode 120000 index 0000000..4fb83a5 --- /dev/null +++ b/subprojects/extensions-tool/po/LINGUAS @@ -0,0 +1 @@ +../../../po/LINGUAS
\ No newline at end of file diff --git a/subprojects/extensions-tool/po/meson.build b/subprojects/extensions-tool/po/meson.build new file mode 100644 index 0000000..5a1b0e2 --- /dev/null +++ b/subprojects/extensions-tool/po/meson.build @@ -0,0 +1 @@ +i18n.gettext(package_name, preset: 'glib') diff --git a/subprojects/extensions-tool/src/command-create.c b/subprojects/extensions-tool/src/command-create.c new file mode 100644 index 0000000..420fb27 --- /dev/null +++ b/subprojects/extensions-tool/src/command-create.c @@ -0,0 +1,506 @@ +/* command-create.c + * + * Copyright 2018 Florian Müllner <fmuellner@gnome.org> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#define _GNU_SOURCE /* for strcasestr */ +#include <string.h> + +#include <glib/gi18n.h> +#include <gio/gio.h> +#include <gio/gdesktopappinfo.h> +#include <gio/gunixinputstream.h> + +#include "commands.h" +#include "common.h" +#include "config.h" + +#define TEMPLATES_PATH "/org/gnome/extensions-tool/templates" +#define TEMPLATE_KEY "Path" +#define SORT_DATA "desktop-id" + +static char * +get_shell_version (GError **error) +{ + g_autoptr (GDBusProxy) proxy = NULL; + g_autoptr (GVariant) variant = NULL; + g_auto (GStrv) split_version = NULL; + + proxy = get_shell_proxy (error); + if (proxy == NULL) + return NULL; + + variant = g_dbus_proxy_get_cached_property (proxy, "ShellVersion"); + if (variant == NULL) + return NULL; + + split_version = g_strsplit (g_variant_get_string (variant, NULL), ".", 2); + return g_steal_pointer(&split_version[0]); +} + +static GDesktopAppInfo * +load_app_info_from_resource (const char *uri) +{ + g_autoptr (GFile) file = NULL; + g_autofree char *contents = NULL; + g_autoptr (GKeyFile) keyfile = NULL; + + file = g_file_new_for_uri (uri); + if (!g_file_load_contents (file, NULL, &contents, NULL, NULL, NULL)) + return NULL; + + keyfile = g_key_file_new (); + if (!g_key_file_load_from_data (keyfile, contents, -1, G_KEY_FILE_NONE, NULL)) + return NULL; + + return g_desktop_app_info_new_from_keyfile (keyfile); +} + +static int +sort_func (gconstpointer a, gconstpointer b) +{ + GObject *info1 = *((GObject **) a); + GObject *info2 = *((GObject **) b); + const char *desktop1 = g_object_get_data (info1, SORT_DATA); + const char *desktop2 = g_object_get_data (info2, SORT_DATA); + + return g_strcmp0 (desktop1, desktop2); +} + +static GPtrArray * +get_templates (void) +{ + g_auto (GStrv) children = NULL; + GPtrArray *templates = g_ptr_array_new_with_free_func (g_object_unref); + char **s; + + children = g_resources_enumerate_children (TEMPLATES_PATH, 0, NULL); + + for (s = children; *s; s++) + { + g_autofree char *uri = NULL; + GDesktopAppInfo *info; + + if (!g_str_has_suffix (*s, ".desktop")) + continue; + + uri = g_strdup_printf ("resource://" TEMPLATES_PATH "/%s", *s); + info = load_app_info_from_resource (uri); + if (!info) + continue; + + g_object_set_data_full (G_OBJECT (info), SORT_DATA, g_strdup (*s), g_free); + g_ptr_array_add (templates, info); + } + + g_ptr_array_sort (templates, sort_func); + + return templates; +} + +static char * +escape_json_string (const char *string) +{ + GString *escaped = g_string_new (string); + + for (gsize i = 0; i < escaped->len; ++i) + { + if (escaped->str[i] == '"' || escaped->str[i] == '\\') + { + g_string_insert_c (escaped, i, '\\'); + ++i; + } + } + + return g_string_free (escaped, FALSE); +} + +static gboolean +create_metadata (GFile *target_dir, + const char *uuid, + const char *name, + const char *description, + GError **error) +{ + g_autofree char *uuid_escaped = NULL; + g_autofree char *name_escaped = NULL; + g_autofree char *desc_escaped = NULL; + g_autoptr (GFile) target = NULL; + g_autoptr (GString) json = NULL; + g_autofree char *version = NULL; + + version = get_shell_version (error); + if (version == NULL) + return FALSE; + + uuid_escaped = escape_json_string (uuid); + name_escaped = escape_json_string (name); + desc_escaped = escape_json_string (description); + + json = g_string_new ("{\n"); + + g_string_append_printf (json, " \"name\": \"%s\",\n", name_escaped); + g_string_append_printf (json, " \"description\": \"%s\",\n", desc_escaped); + g_string_append_printf (json, " \"uuid\": \"%s\",\n", uuid_escaped); + g_string_append_printf (json, " \"shell-version\": [\n"); + g_string_append_printf (json, " \"%s\"\n", version); + g_string_append_printf (json, " ]\n}\n"); + + target = g_file_get_child (target_dir, "metadata.json"); + return g_file_replace_contents (target, + json->str, + json->len, + NULL, + FALSE, + 0, + NULL, + NULL, + error); +} + + +static gboolean +copy_extension_template (const char *template, GFile *target_dir, GError **error) +{ + g_auto (GStrv) templates = NULL; + g_autofree char *path = NULL; + char **s; + + path = g_strdup_printf (TEMPLATES_PATH "/%s", template); + templates = g_resources_enumerate_children (path, 0, NULL); + + if (templates == NULL) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND, + "No template %s", template); + return FALSE; + } + + for (s = templates; *s; s++) + { + g_autoptr (GFile) target = NULL; + g_autoptr (GFile) source = NULL; + g_autofree char *uri = NULL; + + uri = g_strdup_printf ("resource://%s/%s", path, *s); + source = g_file_new_for_uri (uri); + target = g_file_get_child (target_dir, *s); + + if (!g_file_copy (source, target, G_FILE_COPY_TARGET_DEFAULT_PERMS, NULL, NULL, NULL, error)) + return FALSE; + } + + return TRUE; +} + +static gboolean +launch_extension_source (GFile *dir, GError **error) +{ + g_autoptr (GFile) main_source = NULL; + g_autoptr (GAppInfo) handler = NULL; + GList l; + + main_source = g_file_get_child (dir, "extension.js"); + handler = g_file_query_default_handler (main_source, NULL, NULL); + + /* Translators: a file path to an extension directory */ + g_print (_("The new extension was successfully created in %s.\n"), + g_file_peek_path (dir)); + + if (handler == NULL) + return TRUE; + + l.data = main_source; + l.next = l.prev = NULL; + + return g_app_info_launch (handler, &l, NULL, error); +} + +static gboolean +create_extension (const char *uuid, const char *name, const char *description, const char *template) +{ + g_autoptr (GFile) dir = NULL; + g_autoptr (GError) error = NULL; + + if (template == NULL) + template = "plain"; + + dir = g_file_new_build_filename (g_get_user_data_dir (), + "gnome-shell", + "extensions", + uuid, + NULL); + + if (!g_file_make_directory_with_parents (dir, NULL, &error)) + { + g_printerr ("%s\n", error->message); + return FALSE; + } + + if (!create_metadata (dir, uuid, name, description, &error)) + { + g_printerr ("%s\n", error->message); + return FALSE; + } + + if (!copy_extension_template (template, dir, &error)) + { + g_printerr ("%s\n", error->message); + return FALSE; + } + + if (!launch_extension_source (dir, &error)) + { + g_printerr ("%s\n", error->message); + return FALSE; + } + + return TRUE; +} + +static void +prompt_metadata (char **uuid, char **name, char **description, char **template) +{ + g_autoptr (GInputStream) stdin = NULL; + g_autoptr (GDataInputStream) istream = NULL; + + if ((uuid == NULL || *uuid != NULL) && + (name == NULL || *name != NULL) && + (description == NULL || *description != NULL) && + (template == NULL || *template != NULL)) + return; + + stdin = g_unix_input_stream_new (0, FALSE); + istream = g_data_input_stream_new (stdin); + + if (name != NULL && *name == NULL) + { + char *line = NULL; + + g_print ( + _("Name should be a very short (ideally descriptive) string.\n" + "Examples are: %s"), + "“Click To Focus”, “Adblock”, “Shell Window Shrinker”\n"); + + while (line == NULL) + { + g_print ("%s: ", _("Name")); + + line = g_data_input_stream_read_line_utf8 (istream, NULL, NULL, NULL); + } + *name = g_strdelimit (line, "\n", '\0'); + + g_print ("\n"); + } + + if (description != NULL && *description == NULL) + { + char *line = NULL; + + g_print ( + _("Description is a single-sentence explanation of what your extension does.\n" + "Examples are: %s"), + "“Make windows visible on click”, “Block advertisement popups”, “Animate windows shrinking on minimize”\n"); + + while (line == NULL) + { + g_print ("%s: ", _("Description")); + + line = g_data_input_stream_read_line_utf8 (istream, NULL, NULL, NULL); + } + *description = g_strdelimit (line, "\n", '\0'); + + g_print ("\n"); + } + + if (uuid != NULL && *uuid == NULL) + { + char *line = NULL; + + g_print ( + _("UUID is a globally-unique identifier for your extension.\n" + "This should be in the format of an email address (clicktofocus@janedoe.example.com)\n")); + + while (line == NULL) + { + g_print ("UUID: "); + + line = g_data_input_stream_read_line_utf8 (istream, NULL, NULL, NULL); + } + *uuid = g_strdelimit (line, "\n", '\0'); + + g_print ("\n"); + } + + if (template != NULL && *template == NULL) + { + g_autoptr (GPtrArray) templates = get_templates (); + + if (templates->len == 1) + { + GDesktopAppInfo *info = g_ptr_array_index (templates, 0); + *template = g_desktop_app_info_get_string (info, TEMPLATE_KEY); + } + else + { + int i; + + g_print (_("Choose one of the available templates:\n")); + for (i = 0; i < templates->len; i++) + { + GAppInfo *info = g_ptr_array_index (templates, i); + g_print ("%d) %-10s – %s\n", + i + 1, + g_app_info_get_name (info), + g_app_info_get_description (info)); + } + + while (*template == NULL) + { + g_autofree char *line = NULL; + + g_print ("%s [1-%d]: ", _("Template"), templates->len); + + line = g_data_input_stream_read_line_utf8 (istream, NULL, NULL, NULL); + + if (line == NULL) + continue; + + if (g_ascii_isdigit (*line)) + { + long i = strtol (line, NULL, 10); + + if (i > 0 && i <= templates->len) + { + GDesktopAppInfo *info; + + info = g_ptr_array_index (templates, i - 1); + *template = + g_desktop_app_info_get_string (info, TEMPLATE_KEY); + } + } + else + { + for (i = 0; i < templates->len; i++) + { + GDesktopAppInfo *info = g_ptr_array_index (templates, i); + g_autofree char *cur_template = NULL; + + cur_template = + g_desktop_app_info_get_string (info, TEMPLATE_KEY); + + if (strcasestr (cur_template, line) != NULL) + *template = g_steal_pointer (&cur_template); + } + } + } + g_print ("\n"); + } + } +} + +int +handle_create (int argc, char *argv[], gboolean do_help) +{ + g_autoptr (GOptionContext) context = NULL; + g_autoptr (GError) error = NULL; + g_autofree char *name = NULL; + g_autofree char *description = NULL; + g_autofree char *uuid = NULL; + g_autofree char *template = NULL; + gboolean interactive = FALSE; + gboolean list_templates = FALSE; + GOptionEntry entries[] = { + { .long_name = "uuid", + .arg = G_OPTION_ARG_STRING, .arg_data = &uuid, + .arg_description = "UUID", + .description = _("The unique identifier of the new extension") }, + { .long_name = "name", + .arg = G_OPTION_ARG_STRING, .arg_data = &name, + .arg_description = _("NAME"), + .description = _("The user-visible name of the new extension") }, + { .long_name = "description", + .arg_description = _("DESCRIPTION"), + .arg = G_OPTION_ARG_STRING, .arg_data = &description, + .description = _("A short description of what the extension does") }, + { .long_name = "template", + .arg = G_OPTION_ARG_STRING, .arg_data = &template, + .arg_description = _("TEMPLATE"), + .description = _("The template to use for the new extension") }, + { .long_name = "list-templates", + .arg = G_OPTION_ARG_NONE, .arg_data = &list_templates, + .flags = G_OPTION_FLAG_HIDDEN }, + { .long_name = "interactive", .short_name = 'i', + .arg = G_OPTION_ARG_NONE, .arg_data = &interactive, + .description = _("Enter extension information interactively") }, + { NULL } + }; + + g_set_prgname ("gnome-extensions create"); + + context = g_option_context_new (NULL); + g_option_context_set_help_enabled (context, FALSE); + g_option_context_set_summary (context, _("Create a new extension")); + g_option_context_add_main_entries (context, entries, GETTEXT_PACKAGE); + g_option_context_add_group (context, get_option_group ()); + + if (do_help) + { + show_help (context, NULL); + return 0; + } + + if (!g_option_context_parse (context, &argc, &argv, &error)) + { + show_help (context, error->message); + return 1; + } + + if (argc > 1) + { + show_help (context, _("Unknown arguments")); + return 1; + } + + if (list_templates) + { + g_autoptr (GPtrArray) templates = get_templates (); + int i; + + for (i = 0; i < templates->len; i++) + { + GDesktopAppInfo *info = g_ptr_array_index (templates, i); + g_autofree char *template = NULL; + + template = g_desktop_app_info_get_string (info, TEMPLATE_KEY); + g_print ("%s\n", template); + } + return 0; + } + + if (interactive) + prompt_metadata (&uuid, &name, &description, &template); + + if (uuid == NULL || name == NULL || description == NULL) + { + show_help (context, _("UUID, name and description are required")); + return 1; + } + + return create_extension (uuid, name, description, template) ? 0 : 2; +} diff --git a/subprojects/extensions-tool/src/command-disable.c b/subprojects/extensions-tool/src/command-disable.c new file mode 100644 index 0000000..bae11b2 --- /dev/null +++ b/subprojects/extensions-tool/src/command-disable.c @@ -0,0 +1,126 @@ +/* command-disable.c + * + * Copyright 2018 Florian Müllner <fmuellner@gnome.org> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#include <glib/gi18n.h> +#include <gio/gio.h> + +#include "commands.h" +#include "common.h" +#include "config.h" + +static gboolean +disable_extension_gsettings (const char *uuid) +{ + g_autoptr(GSettings) settings = get_shell_settings (); + + if (settings == NULL) + return FALSE; + + return settings_list_remove (settings, "enabled-extensions", uuid) && + settings_list_add (settings, "disabled-extensions", uuid); +} + +static gboolean +disable_extension_dbus (GDBusProxy *proxy, + const char *uuid) +{ + g_autoptr (GVariant) response = NULL; + g_autoptr (GError) error = NULL; + gboolean success = FALSE; + + response = g_dbus_proxy_call_sync (proxy, + "DisableExtension", + g_variant_new ("(s)", uuid), + 0, + -1, + NULL, + &error); + + if (response == NULL) + return disable_extension_gsettings (uuid); + + g_variant_get (response, "(b)", &success); + + if (!success) + g_printerr (_("Extension “%s” does not exist\n"), uuid); + + return success; +} + +static gboolean +disable_extension (const char *uuid) +{ + g_autoptr (GDBusProxy) proxy = NULL; + g_autoptr (GError) error = NULL; + + proxy = get_shell_proxy (&error); + + if (proxy != NULL) + return disable_extension_dbus (proxy, uuid); + else + return disable_extension_gsettings (uuid); +} + +int +handle_disable (int argc, char *argv[], gboolean do_help) +{ + g_autoptr (GOptionContext) context = NULL; + g_autoptr (GError) error = NULL; + g_auto(GStrv) uuids = NULL; + GOptionEntry entries[] = { + { .long_name = G_OPTION_REMAINING, + .arg_description = "UUID", + .arg = G_OPTION_ARG_STRING_ARRAY, .arg_data = &uuids }, + { NULL } + }; + + g_set_prgname ("gnome-extensions disable"); + + context = g_option_context_new (NULL); + g_option_context_set_help_enabled (context, FALSE); + g_option_context_set_summary (context, _("Disable an extension")); + g_option_context_add_main_entries (context, entries, GETTEXT_PACKAGE); + g_option_context_add_group (context, get_option_group()); + + if (do_help) + { + show_help (context, NULL); + return 0; + } + + if (!g_option_context_parse (context, &argc, &argv, &error)) + { + show_help (context, error->message); + return 1; + } + + if (uuids == NULL) + { + show_help (context, _("No UUID given")); + return 1; + } + else if (g_strv_length (uuids) > 1) + { + show_help (context, _("More than one UUID given")); + return 1; + } + + return disable_extension (*uuids) ? 0 : 2; +} diff --git a/subprojects/extensions-tool/src/command-enable.c b/subprojects/extensions-tool/src/command-enable.c new file mode 100644 index 0000000..712de4a --- /dev/null +++ b/subprojects/extensions-tool/src/command-enable.c @@ -0,0 +1,126 @@ +/* command-enable.c + * + * Copyright 2018 Florian Müllner <fmuellner@gnome.org> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#include <glib/gi18n.h> +#include <gio/gio.h> + +#include "commands.h" +#include "common.h" +#include "config.h" + +static gboolean +enable_extension_gsettings (const char *uuid) +{ + g_autoptr(GSettings) settings = get_shell_settings (); + + if (settings == NULL) + return FALSE; + + return settings_list_add (settings, "enabled-extensions", uuid) && + settings_list_remove (settings, "disabled-extensions", uuid); +} + +static gboolean +enable_extension_dbus (GDBusProxy *proxy, + const char *uuid) +{ + g_autoptr (GVariant) response = NULL; + g_autoptr (GError) error = NULL; + gboolean success = FALSE; + + response = g_dbus_proxy_call_sync (proxy, + "EnableExtension", + g_variant_new ("(s)", uuid), + 0, + -1, + NULL, + &error); + + if (response == NULL) + return enable_extension_gsettings (uuid); + + g_variant_get (response, "(b)", &success); + + if (!success) + g_printerr (_("Extension “%s” does not exist\n"), uuid); + + return success; +} + +static gboolean +enable_extension (const char *uuid) +{ + g_autoptr (GDBusProxy) proxy = NULL; + g_autoptr (GError) error = NULL; + + proxy = get_shell_proxy (&error); + + if (proxy != NULL) + return enable_extension_dbus (proxy, uuid); + else + return enable_extension_gsettings (uuid); +} + +int +handle_enable (int argc, char *argv[], gboolean do_help) +{ + g_autoptr (GOptionContext) context = NULL; + g_autoptr (GError) error = NULL; + g_auto(GStrv) uuids = NULL; + GOptionEntry entries[] = { + { .long_name = G_OPTION_REMAINING, + .arg_description = "UUID", + .arg = G_OPTION_ARG_STRING_ARRAY, .arg_data = &uuids }, + { NULL } + }; + + g_set_prgname ("gnome-extensions enable"); + + context = g_option_context_new (NULL); + g_option_context_set_help_enabled (context, FALSE); + g_option_context_set_summary (context, _("Enable an extension")); + g_option_context_add_main_entries (context, entries, GETTEXT_PACKAGE); + g_option_context_add_group (context, get_option_group()); + + if (do_help) + { + show_help (context, NULL); + return 0; + } + + if (!g_option_context_parse (context, &argc, &argv, &error)) + { + show_help (context, error->message); + return 1; + } + + if (uuids == NULL) + { + show_help (context, _("No UUID given")); + return 1; + } + else if (g_strv_length (uuids) > 1) + { + show_help (context, _("More than one UUID given")); + return 1; + } + + return enable_extension (*uuids) ? 0 : 2; +} diff --git a/subprojects/extensions-tool/src/command-info.c b/subprojects/extensions-tool/src/command-info.c new file mode 100644 index 0000000..61492a5 --- /dev/null +++ b/subprojects/extensions-tool/src/command-info.c @@ -0,0 +1,113 @@ +/* commands-info.c + * + * Copyright 2018 Florian Müllner <fmuellner@gnome.org> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#include <glib/gi18n.h> +#include <gio/gio.h> + +#include "commands.h" +#include "common.h" +#include "config.h" + +static gboolean +show_extension_info (const char *uuid) +{ + g_autoptr (GDBusProxy) proxy = NULL; + g_autoptr (GVariant) response = NULL; + g_autoptr (GVariant) asv = NULL; + g_autoptr (GVariantDict) info = NULL; + g_autoptr (GError) error = NULL; + + proxy = get_shell_proxy (&error); + if (proxy == NULL) + return FALSE; + + response = g_dbus_proxy_call_sync (proxy, + "GetExtensionInfo", + g_variant_new ("(s)", uuid), + 0, + -1, + NULL, + &error); + if (response == NULL) + { + g_printerr (_("Failed to connect to GNOME Shell\n")); + return FALSE; + } + + asv = g_variant_get_child_value (response, 0); + info = g_variant_dict_new (asv); + + if (!g_variant_dict_contains (info, "uuid")) + { + g_printerr (_("Extension “%s” doesn't exist\n"), uuid); + return FALSE; + } + + print_extension_info (info, DISPLAY_DETAILED); + + return TRUE; +} + +int +handle_info (int argc, char *argv[], gboolean do_help) +{ + g_autoptr (GOptionContext) context = NULL; + g_autoptr (GError) error = NULL; + g_auto(GStrv) uuids = NULL; + GOptionEntry entries[] = { + { .long_name = G_OPTION_REMAINING, + .arg_description = "UUID", + .arg = G_OPTION_ARG_STRING_ARRAY, .arg_data = &uuids }, + { NULL } + }; + + g_set_prgname ("gnome-extensions info"); + + context = g_option_context_new (NULL); + g_option_context_set_help_enabled (context, FALSE); + g_option_context_set_summary (context, _("Show extensions info")); + g_option_context_add_main_entries (context, entries, GETTEXT_PACKAGE); + g_option_context_add_group (context, get_option_group()); + + if (do_help) + { + show_help (context, NULL); + return 0; + } + + if (!g_option_context_parse (context, &argc, &argv, &error)) + { + show_help (context, error->message); + return 1; + } + + if (uuids == NULL) + { + show_help (context, _("No UUID given")); + return 1; + } + else if (g_strv_length (uuids) > 1) + { + show_help (context, _("More than one UUID given")); + return 1; + } + + return show_extension_info (*uuids) ? 0 : 2; +} diff --git a/subprojects/extensions-tool/src/command-install.c b/subprojects/extensions-tool/src/command-install.c new file mode 100644 index 0000000..2eefaba --- /dev/null +++ b/subprojects/extensions-tool/src/command-install.c @@ -0,0 +1,213 @@ +/* command-install.c + * + * Copyright 2018 Florian Müllner <fmuellner@gnome.org> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#include <glib/gi18n.h> +#include <gio/gio.h> + +#include <gnome-autoar/gnome-autoar.h> +#include <json-glib/json-glib.h> + +#include "commands.h" +#include "common.h" +#include "config.h" + +static JsonObject * +load_metadata (GFile *dir, + GError **error) +{ + g_autoptr (JsonParser) parser = NULL; + g_autoptr (GInputStream) stream = NULL; + g_autoptr (GFile) file = NULL; + + file = g_file_get_child (dir, "metadata.json"); + stream = G_INPUT_STREAM (g_file_read (file, NULL, error)); + if (stream == NULL) + return NULL; + + parser = json_parser_new_immutable (); + if (!json_parser_load_from_stream (parser, stream, NULL, error)) + return NULL; + + return json_node_dup_object (json_parser_get_root (parser)); +} + +static void +on_error (AutoarExtractor *extractor, + GError *error, + gpointer data) +{ + *((GError **)data) = g_error_copy (error); +} + +static GFile * +on_decide_destination (AutoarExtractor *extractor, + GFile *dest, + GList *files, + gpointer data) +{ + g_autofree char *dest_path = NULL; + GFile *new_dest; + int copy = 1; + + dest_path = g_file_get_path (dest); + new_dest = g_object_ref (dest); + + while (g_file_query_exists (new_dest, NULL)) + { + g_autofree char *new_path = g_strdup_printf ("%s (%d)", dest_path, copy); + + g_object_unref (new_dest); + new_dest = g_file_new_for_path (new_path); + + copy++; + } + + *((GFile **)data) = g_object_ref (new_dest); + + return new_dest; +} + +static int +install_extension (const char *bundle, + gboolean force) +{ + g_autoptr (AutoarExtractor) extractor = NULL; + g_autoptr (JsonObject) metadata = NULL; + g_autoptr (GFile) cachedir = NULL; + g_autoptr (GFile) tmpdir = NULL; + g_autoptr (GFile) src = NULL; + g_autoptr (GFile) dst = NULL; + g_autoptr (GFile) dstdir = NULL; + g_autoptr (GError) error = NULL; + g_autofree char *cwd = NULL; + const char *uuid; + + cwd = g_get_current_dir (); + src = g_file_new_for_commandline_arg_and_cwd (bundle, cwd); + cachedir = g_file_new_for_path (g_get_user_cache_dir ()); + + extractor = autoar_extractor_new (src, cachedir); + + g_signal_connect (extractor, "error", G_CALLBACK (on_error), &error); + g_signal_connect (extractor, "decide-destination", G_CALLBACK (on_decide_destination), &tmpdir); + + autoar_extractor_start (extractor, NULL); + + if (error != NULL) + goto err; + + metadata = load_metadata (tmpdir, &error); + if (metadata == NULL) + goto err; + + dstdir = g_file_new_build_filename (g_get_user_data_dir (), + "gnome-shell", "extensions", NULL); + + if (!g_file_make_directory_with_parents (dstdir, NULL, &error)) + { + if (error->code == G_IO_ERROR_EXISTS) + g_clear_error (&error); + else + goto err; + } + + uuid = json_object_get_string_member (metadata, "uuid"); + dst = g_file_get_child (dstdir, uuid); + + if (g_file_query_exists (dst, NULL)) + { + if (!force) + { + g_set_error (&error, G_IO_ERROR, G_IO_ERROR_EXISTS, + "%s exists and --force was not specified", uuid); + goto err; + } + else if (!file_delete_recursively (dst, &error)) + { + goto err; + } + } + + if (!g_file_move (tmpdir, dst, G_FILE_COPY_NONE, NULL, NULL, NULL, &error)) + goto err; + + return 0; + +err: + if (error != NULL) + g_printerr ("%s\n", error->message); + + if (tmpdir != NULL) + file_delete_recursively (tmpdir, NULL); + + return 2; +} + +int +handle_install (int argc, char *argv[], gboolean do_help) +{ + g_autoptr (GOptionContext) context = NULL; + g_autoptr (GError) error = NULL; + g_auto (GStrv) filenames = NULL; + gboolean force = FALSE; + GOptionEntry entries[] = { + { .long_name = "force", .short_name = 'f', + .arg = G_OPTION_ARG_NONE, .arg_data = &force, + .description = _("Overwrite an existing extension") }, + { .long_name = G_OPTION_REMAINING, + .arg_description =_("EXTENSION_BUNDLE"), + .arg = G_OPTION_ARG_FILENAME_ARRAY, .arg_data = &filenames }, + { NULL } + }; + + g_set_prgname ("gnome-extensions install"); + + context = g_option_context_new (NULL); + g_option_context_set_help_enabled (context, FALSE); + g_option_context_set_summary (context, _("Install an extension bundle")); + g_option_context_add_main_entries (context, entries, GETTEXT_PACKAGE); + g_option_context_add_group (context, get_option_group()); + + if (do_help) + { + show_help (context, NULL); + return 0; + } + + if (!g_option_context_parse (context, &argc, &argv, &error)) + { + show_help (context, error->message); + return 1; + } + + if (filenames == NULL) + { + show_help (context, _("No extension bundle specified")); + return 1; + } + + if (g_strv_length (filenames) > 1) + { + show_help (context, _("More than one extension bundle specified")); + return 1; + } + + return install_extension (*filenames, force); +} diff --git a/subprojects/extensions-tool/src/command-list.c b/subprojects/extensions-tool/src/command-list.c new file mode 100644 index 0000000..62db4d9 --- /dev/null +++ b/subprojects/extensions-tool/src/command-list.c @@ -0,0 +1,196 @@ +/* command-list.c + * + * Copyright 2018 Florian Müllner <fmuellner@gnome.org> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#include <glib/gi18n.h> +#include <gio/gio.h> + +#include "commands.h" +#include "common.h" +#include "config.h" + + +typedef enum { + LIST_FLAGS_NONE = 0, + LIST_FLAGS_USER = 1 << 0, + LIST_FLAGS_SYSTEM = 1 << 1, + LIST_FLAGS_ENABLED = 1 << 2, + LIST_FLAGS_DISABLED = 1 << 3, + LIST_FLAGS_NO_PREFS = 1 << 4, + LIST_FLAGS_NO_UPDATES = 1 << 5, +} ListFilterFlags; + +static gboolean +list_extensions (ListFilterFlags filter, DisplayFormat format) +{ + g_autoptr (GDBusProxy) proxy = NULL; + g_autoptr (GVariant) response = NULL; + g_autoptr (GVariant) extensions = NULL; + g_autoptr (GError) error = NULL; + gboolean needs_newline = FALSE; + GVariantIter iter; + GVariant *value; + char *uuid; + + proxy = get_shell_proxy (&error); + if (proxy == NULL) + return FALSE; + + response = g_dbus_proxy_call_sync (proxy, + "ListExtensions", + NULL, + 0, + -1, + NULL, + &error); + if (response == NULL) + { + g_printerr (_("Failed to connect to GNOME Shell\n")); + return FALSE; + } + + extensions = g_variant_get_child_value (response, 0); + + g_variant_iter_init (&iter, extensions); + while (g_variant_iter_loop (&iter, "{s@a{sv}}", &uuid, &value)) + { + g_autoptr (GVariantDict) info = NULL; + double type, state; + gboolean has_prefs; + gboolean has_update; + + info = g_variant_dict_new (value); + g_variant_dict_lookup (info, "type", "d", &type); + g_variant_dict_lookup (info, "state", "d", &state); + g_variant_dict_lookup (info, "hasPrefs", "b", &has_prefs); + g_variant_dict_lookup (info, "hasUpdate", "b", &has_update); + + if (type == TYPE_USER && (filter & LIST_FLAGS_USER) == 0) + continue; + + if (type == TYPE_SYSTEM && (filter & LIST_FLAGS_SYSTEM) == 0) + continue; + + if (state == STATE_ENABLED && (filter & LIST_FLAGS_ENABLED) == 0) + continue; + + if (state != STATE_ENABLED && (filter & LIST_FLAGS_DISABLED) == 0) + continue; + + if (!has_prefs && (filter & LIST_FLAGS_NO_PREFS) == 0) + continue; + + if (!has_update && (filter & LIST_FLAGS_NO_UPDATES) == 0) + continue; + + if (needs_newline) + g_print ("\n"); + + print_extension_info (info, format); + needs_newline = (format != DISPLAY_ONELINE); + } + + return TRUE; +} + +int +handle_list (int argc, char *argv[], gboolean do_help) +{ + g_autoptr (GOptionContext) context = NULL; + g_autoptr (GError) error = NULL; + int flags = LIST_FLAGS_NONE; + gboolean details = FALSE; + gboolean user = FALSE; + gboolean system = FALSE; + gboolean enabled = FALSE; + gboolean disabled = FALSE; + gboolean has_prefs = FALSE; + gboolean has_updates = FALSE; + GOptionEntry entries[] = { + { .long_name = "user", + .arg = G_OPTION_ARG_NONE, .arg_data = &user, + .description = _("Show user-installed extensions") }, + { .long_name = "system", + .arg = G_OPTION_ARG_NONE, .arg_data = &system, + .description = _("Show system-installed extensions") }, + { .long_name = "enabled", + .arg = G_OPTION_ARG_NONE, .arg_data = &enabled, + .description = _("Show enabled extensions") }, + { .long_name = "disabled", + .arg = G_OPTION_ARG_NONE, .arg_data = &disabled, + .description = _("Show disabled extensions") }, + { .long_name = "prefs", + .arg = G_OPTION_ARG_NONE, .arg_data = &has_prefs, + .description = _("Show extensions with preferences") }, + { .long_name = "updates", + .arg = G_OPTION_ARG_NONE, .arg_data = &has_updates, + .description = _("Show extensions with updates") }, + { .long_name = "details", .short_name = 'd', + .arg = G_OPTION_ARG_NONE, .arg_data = &details, + .description = _("Print extension details") }, + { NULL } + }; + + g_set_prgname ("gnome-extensions list"); + + context = g_option_context_new (NULL); + g_option_context_set_help_enabled (context, FALSE); + g_option_context_set_summary (context, _("List installed extensions")); + g_option_context_add_main_entries (context, entries, GETTEXT_PACKAGE); + g_option_context_add_group (context, get_option_group()); + + if (do_help) + { + show_help (context, NULL); + return 0; + } + + if (!g_option_context_parse (context, &argc, &argv, &error)) + { + show_help (context, error->message); + return 1; + } + + if (argc > 1) + { + show_help (context, _("Unknown arguments")); + return 1; + } + + if (user || !system) + flags |= LIST_FLAGS_USER; + + if (system || !user) + flags |= LIST_FLAGS_SYSTEM; + + if (enabled || !disabled) + flags |= LIST_FLAGS_ENABLED; + + if (disabled || !enabled) + flags |= LIST_FLAGS_DISABLED; + + if (!has_prefs) + flags |= LIST_FLAGS_NO_PREFS; + + if (!has_updates) + flags |= LIST_FLAGS_NO_UPDATES; + + return list_extensions (flags, details ? DISPLAY_DETAILED + : DISPLAY_ONELINE) ? 0 : 2; +} diff --git a/subprojects/extensions-tool/src/command-pack.c b/subprojects/extensions-tool/src/command-pack.c new file mode 100644 index 0000000..c8d9950 --- /dev/null +++ b/subprojects/extensions-tool/src/command-pack.c @@ -0,0 +1,516 @@ +/* command-pack.c + * + * Copyright 2018 Florian Müllner <fmuellner@gnome.org> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#include <glib/gi18n.h> +#include <gio/gio.h> + +#include <gnome-autoar/gnome-autoar.h> +#include <json-glib/json-glib.h> + +#include "commands.h" +#include "common.h" +#include "config.h" + +typedef struct _ExtensionPack { + GHashTable *files; + JsonObject *metadata; + GFile *tmpdir; + char *srcdir; +} ExtensionPack; + +static void extension_pack_free (ExtensionPack *); +G_DEFINE_AUTOPTR_CLEANUP_FUNC (ExtensionPack, extension_pack_free); + +static ExtensionPack * +extension_pack_new (const char *srcdir) +{ + ExtensionPack *pack = g_new0 (ExtensionPack, 1); + pack->srcdir = g_strdup (srcdir); + pack->files = g_hash_table_new_full (g_str_hash, g_str_equal, + g_free, g_object_unref); + return pack; +} + +static void +extension_pack_free (ExtensionPack *pack) +{ + if (pack->tmpdir) + file_delete_recursively (pack->tmpdir, NULL); + + g_clear_pointer (&pack->files, g_hash_table_destroy); + g_clear_pointer (&pack->metadata, json_object_unref); + g_clear_pointer (&pack->srcdir, g_free); + g_clear_object (&pack->tmpdir); + g_free (pack); +} + +static void +extension_pack_add_source (ExtensionPack *pack, + const char *filename) +{ + g_autoptr (GFile) file = NULL; + file = g_file_new_for_commandline_arg_and_cwd (filename, pack->srcdir); + if (g_file_query_exists (file, NULL)) + g_hash_table_insert (pack->files, + g_path_get_basename (filename), g_steal_pointer (&file)); +} + +static gboolean +extension_pack_check_required_file (ExtensionPack *pack, + const char *filename, + GError **error) +{ + if (!g_hash_table_contains (pack->files, filename)) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND, + "Missing %s in extension pack", filename); + return FALSE; + } + return TRUE; +} + +static gboolean +ensure_tmpdir (ExtensionPack *pack, + GError **error) +{ + g_autofree char *path = NULL; + + if (pack->tmpdir != NULL) + return TRUE; + + path = g_dir_make_tmp ("gnome-extensions.XXXXXX", error); + if (path != NULL) + pack->tmpdir = g_file_new_for_path (path); + + return pack->tmpdir != NULL; +} + +static gboolean +ensure_metadata (ExtensionPack *pack, + GError **error) +{ + g_autoptr (JsonParser) parser = NULL; + g_autoptr (GInputStream) stream = NULL; + GFile *file = NULL; + + if (pack->metadata != NULL) + return TRUE; + + if (!extension_pack_check_required_file (pack, "metadata.json", error)) + return FALSE; + + file = g_hash_table_lookup (pack->files, "metadata.json"); + stream = G_INPUT_STREAM (g_file_read (file, NULL, error)); + + if (stream == NULL) + return FALSE; + + parser = json_parser_new_immutable (); + + if (!json_parser_load_from_stream (parser, stream, NULL, error)) + return FALSE; + + pack->metadata = json_node_dup_object (json_parser_get_root (parser)); + return TRUE; +} + +static gboolean +extension_pack_add_schemas (ExtensionPack *pack, + char **schemas, + GError **error) +{ + g_autoptr (GSubprocess) proc = NULL; + g_autoptr (GFile) dstdir = NULL; + g_autofree char *dstpath = NULL; + char **s; + + if (!ensure_tmpdir (pack, error)) + return FALSE; + + dstdir = g_file_get_child (pack->tmpdir, "schemas"); + if (!g_file_make_directory (dstdir, NULL, error)) + return FALSE; + + for (s = schemas; s && *s; s++) + { + g_autoptr (GFile) src = NULL; + g_autoptr (GFile) dst = NULL; + g_autofree char *basename = NULL; + + src = g_file_new_for_commandline_arg_and_cwd (*s, pack->srcdir); + + basename = g_file_get_basename (src); + dst = g_file_get_child (dstdir, basename); + + if (!g_file_copy (src, dst, G_FILE_COPY_NONE, NULL, NULL, NULL, error)) + return FALSE; + } + + dstpath = g_file_get_path (dstdir); + proc = g_subprocess_new (G_SUBPROCESS_FLAGS_STDERR_SILENCE, error, + "glib-compile-schemas", "--strict", dstpath, NULL); + + if (!g_subprocess_wait_check (proc, NULL, error)) + return FALSE; + + g_hash_table_insert (pack->files, + g_strdup ("schemas"), g_steal_pointer (&dstdir)); + return TRUE; +} + +static gboolean +extension_pack_add_locales (ExtensionPack *pack, + const char *podir, + const char *gettext_domain, + GError **error) +{ + g_autoptr (GFile) dstdir = NULL; + g_autoptr (GFile) srcdir = NULL; + g_autoptr (GFileEnumerator) file_enum = NULL; + g_autofree char *dstpath = NULL; + g_autofree char *moname = NULL; + GFile *child; + GFileInfo *info; + + if (!ensure_tmpdir (pack, error)) + return FALSE; + + dstdir = g_file_get_child (pack->tmpdir, "locale"); + if (!g_file_make_directory (dstdir, NULL, error)) + return FALSE; + + srcdir = g_file_new_for_commandline_arg_and_cwd (podir, pack->srcdir); + file_enum = g_file_enumerate_children (srcdir, + G_FILE_ATTRIBUTE_STANDARD_NAME, + G_FILE_QUERY_INFO_NONE, + NULL, + error); + if (file_enum == NULL) + return FALSE; + + if (gettext_domain == NULL) + { + if (!ensure_metadata (pack, error)) + return FALSE; + + if (json_object_has_member (pack->metadata, "gettext-domain")) + gettext_domain = json_object_get_string_member (pack->metadata, + "gettext-domain"); + else + gettext_domain = json_object_get_string_member (pack->metadata, + "uuid"); + } + + dstpath = g_file_get_path (dstdir); + moname = g_strdup_printf ("%s.mo", gettext_domain); + + while (TRUE) + { + g_autoptr (GSubprocess) proc = NULL; + g_autoptr (GFile) modir = NULL; + g_autofree char *popath = NULL; + g_autofree char *mopath = NULL; + g_autofree char *lang = NULL; + const char *name; + + if (!g_file_enumerator_iterate (file_enum, &info, &child, NULL, error)) + return FALSE; + + if (info == NULL) + break; + + name = g_file_info_get_name (info); + if (!g_str_has_suffix (name, ".po")) + continue; + + lang = g_strndup (name, strlen (name) - 3 /* strlen (".po") */); + modir = g_file_new_build_filename (dstpath, lang, "LC_MESSAGES", NULL); + if (!g_file_make_directory_with_parents (modir, NULL, error)) + return FALSE; + + mopath = g_build_filename (dstpath, lang, "LC_MESSAGES", moname, NULL); + popath = g_file_get_path (child); + + proc = g_subprocess_new (G_SUBPROCESS_FLAGS_STDERR_SILENCE, error, + "msgfmt", "-o", mopath, popath, NULL); + + if (!g_subprocess_wait_check (proc, NULL, error)) + return FALSE; + } + + g_hash_table_insert (pack->files, + g_strdup ("locale"), g_steal_pointer (&dstdir)); + return TRUE; +} + +static void +on_error (AutoarCompressor *compressor, + GError *error, + gpointer data) +{ + *((GError **)data) = g_error_copy (error); +} + +static gboolean +extension_pack_compress (ExtensionPack *pack, + const char *outdir, + gboolean overwrite, + GError **error) +{ + g_autoptr (AutoarCompressor) compressor = NULL; + g_autoptr (GError) err = NULL; + g_autoptr (GFile) outfile = NULL; + g_autofree char *name = NULL; + const char *uuid; + + if (!ensure_metadata (pack, error)) + return FALSE; + + uuid = json_object_get_string_member (pack->metadata, "uuid"); + name = g_strdup_printf ("%s.shell-extension.zip", uuid); + outfile = g_file_new_for_commandline_arg_and_cwd (name, outdir); + + if (g_file_query_exists (outfile, NULL)) + { + if (!overwrite) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_EXISTS, + "%s exists and --force was not specified", name); + return FALSE; + } + else if (!g_file_delete (outfile, NULL, error)) + { + return FALSE; + } + } + + compressor = autoar_compressor_new (g_hash_table_get_values (pack->files), + outfile, + AUTOAR_FORMAT_ZIP, + AUTOAR_FILTER_NONE, + FALSE); + autoar_compressor_set_output_is_dest (compressor, TRUE); + + g_signal_connect (compressor, "error", G_CALLBACK (on_error), err); + + autoar_compressor_start (compressor, NULL); + + if (err != NULL) + { + g_propagate_error (error, err); + return FALSE; + } + + return TRUE; +} + +static char ** +find_schemas (const char *basepath, + GError **error) +{ + g_autoptr (GFile) basedir = NULL; + g_autoptr (GFile) schemadir = NULL; + g_autoptr (GFileEnumerator) file_enum = NULL; + g_autoptr (GPtrArray) schemas = NULL; + GFile *child; + GFileInfo *info; + + basedir = g_file_new_for_path (basepath); + schemadir = g_file_get_child (basedir, "schemas"); + file_enum = g_file_enumerate_children (schemadir, + G_FILE_ATTRIBUTE_STANDARD_NAME, + G_FILE_QUERY_INFO_NONE, + NULL, error); + + if (error && *error) + { + if (g_error_matches (*error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND) || + g_error_matches (*error, G_IO_ERROR, G_IO_ERROR_NOT_DIRECTORY)) + g_clear_error (error); + return NULL; + } + + schemas = g_ptr_array_new_with_free_func (g_free); + + while (TRUE) + { + if (!g_file_enumerator_iterate (file_enum, &info, &child, NULL, error)) + return NULL; + + if (child == NULL) + break; + + if (!g_str_has_suffix (g_file_info_get_name (info), ".gschema.xml")) + continue; + + g_ptr_array_add (schemas, g_file_get_relative_path (basedir, child)); + } + g_ptr_array_add (schemas, NULL); + + return (char **)g_ptr_array_free (g_ptr_array_ref (schemas), FALSE); +} + +static int +pack_extension (char *srcdir, + char *dstdir, + gboolean force, + char **extra_sources, + char **schemas, + char *podir, + char *gettext_domain) +{ + g_autoptr (ExtensionPack) pack = NULL; + g_autoptr (GError) error = NULL; + char **s; + + pack = extension_pack_new (srcdir); + extension_pack_add_source (pack, "extension.js"); + extension_pack_add_source (pack, "metadata.json"); + extension_pack_add_source (pack, "stylesheet.css"); + extension_pack_add_source (pack, "prefs.js"); + + for (s = extra_sources; s && *s; s++) + extension_pack_add_source (pack, *s); + + if (!extension_pack_check_required_file (pack, "extension.js", &error)) + goto err; + + if (!extension_pack_check_required_file (pack, "metadata.json", &error)) + goto err; + + if (schemas == NULL) + schemas = find_schemas (srcdir, &error); + + if (schemas != NULL) + extension_pack_add_schemas (pack, schemas, &error); + + if (error) + goto err; + + if (podir == NULL) + { + g_autoptr (GFile) dir = NULL; + + dir = g_file_new_for_commandline_arg_and_cwd ("po", srcdir); + if (g_file_query_exists (dir, NULL)) + podir = (char *)"po"; + } + + if (podir != NULL) + extension_pack_add_locales (pack, podir, gettext_domain, &error); + + if (error) + goto err; + + extension_pack_compress (pack, dstdir, force, &error); + +err: + if (error) + { + g_printerr ("%s\n", error->message); + return 2; + } + + return 0; +} + +int +handle_pack (int argc, char *argv[], gboolean do_help) +{ + g_autoptr (GOptionContext) context = NULL; + g_autoptr (GError) error = NULL; + g_auto(GStrv) extra_sources = NULL; + g_auto(GStrv) schemas = NULL; + g_auto(GStrv) srcdirs = NULL; + g_autofree char *podir = NULL; + g_autofree char *srcdir = NULL; + g_autofree char *dstdir = NULL; + g_autofree char *gettext_domain = NULL; + gboolean force = FALSE; + GOptionEntry entries[] = { + { .long_name = "extra-source", + .arg = G_OPTION_ARG_FILENAME_ARRAY, .arg_data = &extra_sources, + .arg_description = _("FILE"), + .description = _("Additional source to include in the bundle") }, + { .long_name = "schema", + .arg = G_OPTION_ARG_FILENAME_ARRAY, .arg_data = &schemas, + .arg_description = _("SCHEMA"), + .description = _("A GSettings schema that should be included") }, + { .long_name = "podir", + .arg_description = _("DIRECTORY"), + .arg = G_OPTION_ARG_FILENAME, .arg_data = &podir, + .description = _("The directory where translations are found") }, + { .long_name = "gettext-domain", + .arg_description = _("DOMAIN"), + .arg = G_OPTION_ARG_STRING, .arg_data = &gettext_domain, + .description = _("The gettext domain to use for translations") }, + { .long_name = "force", .short_name = 'f', + .arg = G_OPTION_ARG_NONE, .arg_data = &force, + .description = _("Overwrite an existing pack") }, + { .long_name = "out-dir", .short_name = 'o', + .arg_description = _("DIRECTORY"), + .arg = G_OPTION_ARG_FILENAME, .arg_data = &dstdir, + .description = _("The directory where the pack should be created") }, + { .long_name = G_OPTION_REMAINING, + .arg_description =_("SOURCE_DIRECTORY"), + .arg = G_OPTION_ARG_FILENAME_ARRAY, .arg_data = &srcdirs }, + { NULL } + }; + + g_set_prgname ("gnome-extensions pack"); + + context = g_option_context_new (NULL); + g_option_context_set_help_enabled (context, FALSE); + g_option_context_set_summary (context, _("Create an extension bundle")); + g_option_context_add_main_entries (context, entries, GETTEXT_PACKAGE); + g_option_context_add_group (context, get_option_group()); + + if (do_help) + { + show_help (context, NULL); + return 0; + } + + if (!g_option_context_parse (context, &argc, &argv, &error)) + { + show_help (context, error->message); + return 1; + } + + if (srcdirs) + { + if (g_strv_length (srcdirs) > 1) + { + show_help (context, _("More than one source directory specified")); + return 1; + } + srcdir = g_strdup (*srcdirs); + } + else + { + srcdir = g_get_current_dir (); + } + + if (dstdir == NULL) + dstdir = g_get_current_dir (); + + return pack_extension (srcdir, dstdir, force, + extra_sources, schemas, podir, gettext_domain); +} diff --git a/subprojects/extensions-tool/src/command-prefs.c b/subprojects/extensions-tool/src/command-prefs.c new file mode 100644 index 0000000..01c385e --- /dev/null +++ b/subprojects/extensions-tool/src/command-prefs.c @@ -0,0 +1,115 @@ +/* commands-prefs.c + * + * Copyright 2019 Florian Müllner <fmuellner@gnome.org> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#include <glib/gi18n.h> +#include <gio/gio.h> + +#include "commands.h" +#include "common.h" +#include "config.h" + +static gboolean +launch_extension_prefs (const char *uuid) +{ + g_autoptr (GDBusProxy) proxy = NULL; + g_autoptr (GVariant) info = NULL; + g_autoptr (GError) error = NULL; + gboolean has_prefs; + + proxy = get_shell_proxy (&error); + if (proxy == NULL) + return FALSE; + + info = get_extension_property (proxy, uuid, "hasPrefs"); + if (info == NULL) + return FALSE; + + has_prefs = g_variant_get_boolean (info); + if (!has_prefs) + { + g_printerr (_("Extension “%s” doesn't have preferences\n"), uuid); + return FALSE; + } + + g_dbus_proxy_call_sync (proxy, + "OpenExtensionPrefs", + g_variant_new ("(ssa{sv})", uuid, "", NULL), + 0, + -1, + NULL, + &error); + + if (error) + { + g_dbus_error_strip_remote_error (error); + g_printerr (_("Failed to open prefs for extension “%s”: %s\n"), + uuid, error->message); + return FALSE; + } + + return TRUE; +} + +int +handle_prefs (int argc, char *argv[], gboolean do_help) +{ + g_autoptr (GOptionContext) context = NULL; + g_autoptr (GError) error = NULL; + g_auto(GStrv) uuids = NULL; + GOptionEntry entries[] = { + { .long_name = G_OPTION_REMAINING, + .arg_description = "UUID", + .arg = G_OPTION_ARG_STRING_ARRAY, .arg_data = &uuids }, + { NULL } + }; + + g_set_prgname ("gnome-extensions prefs"); + + context = g_option_context_new (NULL); + g_option_context_set_help_enabled (context, FALSE); + g_option_context_set_summary (context, _("Opens extension preferences")); + g_option_context_add_main_entries (context, entries, GETTEXT_PACKAGE); + g_option_context_add_group (context, get_option_group()); + + if (do_help) + { + show_help (context, NULL); + return 0; + } + + if (!g_option_context_parse (context, &argc, &argv, &error)) + { + show_help (context, error->message); + return 1; + } + + if (uuids == NULL) + { + show_help (context, _("No UUID given")); + return 1; + } + else if (g_strv_length (uuids) > 1) + { + show_help (context, _("More than one UUID given")); + return 1; + } + + return launch_extension_prefs (*uuids) ? 0 : 2; +} diff --git a/subprojects/extensions-tool/src/command-reset.c b/subprojects/extensions-tool/src/command-reset.c new file mode 100644 index 0000000..2615f15 --- /dev/null +++ b/subprojects/extensions-tool/src/command-reset.c @@ -0,0 +1,86 @@ +/* command-reset.c + g_option_context_add_group (context, get_option_group()); + * + * Copyright 2019 Florian Müllner <fmuellner@gnome.org> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#include <glib/gi18n.h> +#include <gio/gio.h> + +#include "commands.h" +#include "common.h" +#include "config.h" + +static gboolean +reset_extension (const char *uuid) +{ + g_autoptr(GSettings) settings = get_shell_settings(); + + if (settings == NULL) + return FALSE; + + return settings_list_remove (settings, "enabled-extensions", uuid) && + settings_list_remove (settings, "disabled-extensions", uuid); +} + +int +handle_reset (int argc, char *argv[], gboolean do_help) +{ + g_autoptr (GOptionContext) context = NULL; + g_autoptr (GError) error = NULL; + g_auto(GStrv) uuids = NULL; + GOptionEntry entries[] = { + { .long_name = G_OPTION_REMAINING, + .arg_description = "UUID", + .arg = G_OPTION_ARG_STRING_ARRAY, .arg_data = &uuids }, + { NULL } + }; + + g_set_prgname ("gnome-extensions reset"); + + context = g_option_context_new (NULL); + g_option_context_set_help_enabled (context, FALSE); + g_option_context_set_summary (context, _("Reset an extension")); + g_option_context_add_main_entries (context, entries, GETTEXT_PACKAGE); + g_option_context_add_group (context, get_option_group()); + + if (do_help) + { + show_help (context, NULL); + return 0; + } + + if (!g_option_context_parse (context, &argc, &argv, &error)) + { + show_help (context, error->message); + return 1; + } + + if (uuids == NULL) + { + show_help (context, _("No UUID given")); + return 1; + } + else if (g_strv_length (uuids) > 1) + { + show_help (context, _("More than one UUID given")); + return 1; + } + + return reset_extension (*uuids) ? 0 : 2; +} diff --git a/subprojects/extensions-tool/src/command-uninstall.c b/subprojects/extensions-tool/src/command-uninstall.c new file mode 100644 index 0000000..344b720 --- /dev/null +++ b/subprojects/extensions-tool/src/command-uninstall.c @@ -0,0 +1,114 @@ +/* commands-uninstall.c + * + * Copyright 2019 Florian Müllner <fmuellner@gnome.org> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#include <glib/gi18n.h> +#include <gio/gio.h> + +#include "commands.h" +#include "common.h" +#include "config.h" + +static gboolean +uninstall_extension (const char *uuid) +{ + g_autoptr (GDBusProxy) proxy = NULL; + g_autoptr (GVariant) info = NULL; + g_autoptr (GVariant) response = NULL; + g_autoptr (GError) error = NULL; + gboolean success = FALSE; + double type; + + proxy = get_shell_proxy (&error); + if (proxy == NULL) + return FALSE; + + info = get_extension_property (proxy, uuid, "type"); + if (info == NULL) + return FALSE; + + type = g_variant_get_double (info); + if (type == TYPE_SYSTEM) + { + g_printerr (_("Cannot uninstall system extensions\n")); + return FALSE; + } + + response = g_dbus_proxy_call_sync (proxy, + "UninstallExtension", + g_variant_new ("(s)", uuid), + 0, + -1, + NULL, + &error); + + g_variant_get (response, "(b)", &success); + + if (!success) + g_printerr (_("Failed to uninstall “%s”\n"), uuid); + + return success; +} + +int +handle_uninstall (int argc, char *argv[], gboolean do_help) +{ + g_autoptr (GOptionContext) context = NULL; + g_autoptr (GError) error = NULL; + g_auto(GStrv) uuids = NULL; + GOptionEntry entries[] = { + { .long_name = G_OPTION_REMAINING, + .arg_description = "UUID", + .arg = G_OPTION_ARG_STRING_ARRAY, .arg_data = &uuids }, + { NULL } + }; + + g_set_prgname ("gnome-extensions uninstall"); + + context = g_option_context_new (NULL); + g_option_context_set_help_enabled (context, FALSE); + g_option_context_set_summary (context, _("Uninstall an extension")); + g_option_context_add_main_entries (context, entries, GETTEXT_PACKAGE); + g_option_context_add_group (context, get_option_group()); + + if (do_help) + { + show_help (context, NULL); + return 0; + } + + if (!g_option_context_parse (context, &argc, &argv, &error)) + { + show_help (context, error->message); + return 1; + } + + if (uuids == NULL) + { + show_help (context, _("No UUID given")); + return 1; + } + else if (g_strv_length (uuids) > 1) + { + show_help (context, _("More than one UUID given")); + return 1; + } + + return uninstall_extension (*uuids) ? 0 : 2; +} diff --git a/subprojects/extensions-tool/src/commands.h b/subprojects/extensions-tool/src/commands.h new file mode 100644 index 0000000..618e841 --- /dev/null +++ b/subprojects/extensions-tool/src/commands.h @@ -0,0 +1,38 @@ +/* commands.h + * + * Copyright 2018 Florian Müllner <fmuellner@gnome.org> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include <glib.h> + +G_BEGIN_DECLS + +int handle_enable (int argc, char *argv[], gboolean do_help); +int handle_disable (int argc, char *argv[], gboolean do_help); +int handle_reset (int argc, char *argv[], gboolean do_help); +int handle_list (int argc, char *argv[], gboolean do_help); +int handle_info (int argc, char *argv[], gboolean do_help); +int handle_prefs (int argc, char *argv[], gboolean do_help); +int handle_create (int argc, char *argv[], gboolean do_help); +int handle_pack (int argc, char *argv[], gboolean do_help); +int handle_install (int argc, char *argv[], gboolean do_help); +int handle_uninstall (int argc, char *argv[], gboolean do_help); + +G_END_DECLS diff --git a/subprojects/extensions-tool/src/common.h b/subprojects/extensions-tool/src/common.h new file mode 100644 index 0000000..2b04484 --- /dev/null +++ b/subprojects/extensions-tool/src/common.h @@ -0,0 +1,73 @@ +/* common.h + * + * Copyright 2018 Florian Müllner <fmuellner@gnome.org> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include <gio/gio.h> + +G_BEGIN_DECLS + +typedef enum { + TYPE_SYSTEM = 1, + TYPE_USER +} ExtensionType; + +typedef enum { + STATE_ENABLED = 1, + STATE_DISABLED, + STATE_ERROR, + STATE_OUT_OF_DATE, + STATE_DOWNLOADING, + STATE_INITIALIZED, + + STATE_UNINSTALLED = 99 +} ExtensionState; + +typedef enum { + DISPLAY_ONELINE, + DISPLAY_DETAILED +} DisplayFormat; + +GOptionGroup *get_option_group (void); + +void show_help (GOptionContext *context, + const char *message); + +void print_extension_info (GVariantDict *info, + DisplayFormat format); + +GDBusProxy *get_shell_proxy (GError **error); +GVariant *get_extension_property (GDBusProxy *proxy, + const char *uuid, + const char *property); + +GSettings *get_shell_settings (void); + +gboolean settings_list_add (GSettings *settings, + const char *key, + const char *value); +gboolean settings_list_remove (GSettings *settings, + const char *key, + const char *value); + +gboolean file_delete_recursively (GFile *file, + GError **error); + +G_END_DECLS diff --git a/subprojects/extensions-tool/src/gnome-extensions-tool.gresource.xml b/subprojects/extensions-tool/src/gnome-extensions-tool.gresource.xml new file mode 100644 index 0000000..0db87c3 --- /dev/null +++ b/subprojects/extensions-tool/src/gnome-extensions-tool.gresource.xml @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="UTF-8"?> +<gresources> + <gresource prefix="/org/gnome/extensions-tool"> + <file>templates/00-plain.desktop</file> + <file>templates/indicator.desktop</file> + <file>templates/indicator/extension.js</file> + <file>templates/indicator/stylesheet.css</file> + <file>templates/plain/extension.js</file> + <file>templates/plain/stylesheet.css</file> + </gresource> +</gresources> diff --git a/subprojects/extensions-tool/src/main.c b/subprojects/extensions-tool/src/main.c new file mode 100644 index 0000000..66a3476 --- /dev/null +++ b/subprojects/extensions-tool/src/main.c @@ -0,0 +1,412 @@ +/* main.c + * + * Copyright 2018 Florian Müllner <fmuellner@gnome.org> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#include <gio/gio.h> +#include <glib/gi18n.h> +#include <locale.h> + +#include "config.h" +#include "commands.h" +#include "common.h" + +static const char * +extension_state_to_string (ExtensionState state) +{ + switch (state) + { + case STATE_ENABLED: + return "ENABLED"; + case STATE_DISABLED: + return "DISABLED"; + case STATE_ERROR: + return "ERROR"; + case STATE_OUT_OF_DATE: + return "OUT OF DATE"; + case STATE_DOWNLOADING: + return "DOWNLOADING"; + case STATE_INITIALIZED: + return "INITIALIZED"; + case STATE_UNINSTALLED: + return "UNINSTALLED"; + } + return "UNKNOWN"; +} + +static void +print_nothing (const char *message) +{ +} + +static gboolean +quiet_cb (const gchar *option_name, + const gchar *value, + gpointer data, + GError **error) +{ + g_set_printerr_handler (print_nothing); + return TRUE; +} + +GOptionGroup * +get_option_group () +{ + GOptionEntry entries[] = { + { .long_name = "quiet", .short_name = 'q', + .description = _("Do not print error messages"), + .arg = G_OPTION_ARG_CALLBACK, .arg_data = &quiet_cb, + .flags = G_OPTION_FLAG_NO_ARG | G_OPTION_FLAG_IN_MAIN }, + { NULL } + }; + GOptionGroup *group; + + group = g_option_group_new ("Common", "common options", "common options", NULL, NULL); + g_option_group_add_entries (group, entries); + + return group; +} + +void +show_help (GOptionContext *context, const char *message) +{ + g_autofree char *help = NULL; + + if (message) + g_printerr ("gnome-extensions: %s\n\n", message); + + help = g_option_context_get_help (context, TRUE, NULL); + g_printerr ("%s", help); +} + +GDBusProxy * +get_shell_proxy (GError **error) +{ + return g_dbus_proxy_new_for_bus_sync (G_BUS_TYPE_SESSION, + G_DBUS_PROXY_FLAGS_NONE, + NULL, + "org.gnome.Shell.Extensions", + "/org/gnome/Shell/Extensions", + "org.gnome.Shell.Extensions", + NULL, + error); +} + +GSettings * +get_shell_settings (void) +{ + g_autoptr (GSettingsSchema) schema = NULL; + GSettingsSchemaSource *schema_source; + + schema_source = g_settings_schema_source_get_default (); + schema = g_settings_schema_source_lookup (schema_source, + "org.gnome.shell", + TRUE); + + if (schema == NULL) + return NULL; + + return g_settings_new_full (schema, NULL, NULL); +} + +GVariant * +get_extension_property (GDBusProxy *proxy, + const char *uuid, + const char *property) +{ + g_autoptr (GVariant) response = NULL; + g_autoptr (GVariant) asv = NULL; + g_autoptr (GVariantDict) info = NULL; + g_autoptr (GError) error = NULL; + + response = g_dbus_proxy_call_sync (proxy, + "GetExtensionInfo", + g_variant_new ("(s)", uuid), + 0, + -1, + NULL, + &error); + if (response == NULL) + { + g_printerr (_("Failed to connect to GNOME Shell\n")); + return NULL; + } + + asv = g_variant_get_child_value (response, 0); + info = g_variant_dict_new (asv); + + if (!g_variant_dict_contains (info, "uuid")) + { + g_printerr (_("Extension “%s” doesn't exist\n"), uuid); + return NULL; + } + + return g_variant_dict_lookup_value (info, property, NULL); +} + +gboolean +settings_list_add (GSettings *settings, + const char *key, + const char *value) +{ + g_auto(GStrv) list = NULL; + g_auto(GStrv) new_value = NULL; + guint n_values; + int i; + + if (!g_settings_is_writable (settings, key)) + return FALSE; + + list = g_settings_get_strv (settings, key); + + if (g_strv_contains ((const char **)list, value)) + return TRUE; + + n_values = g_strv_length (list); + new_value = g_new0 (char *, n_values + 2); + for (i = 0; i < n_values; i++) + new_value[i] = g_strdup (list[i]); + new_value[i] = g_strdup (value); + + g_settings_set_strv (settings, key, (const char **)new_value); + g_settings_sync (); + + return TRUE; +} + +gboolean +settings_list_remove (GSettings *settings, + const char *key, + const char *value) +{ + g_auto(GStrv) list = NULL; + g_auto(GStrv) new_value = NULL; + const char **s; + guint n_values; + int i; + + if (!g_settings_is_writable (settings, key)) + return FALSE; + + list = g_settings_get_strv (settings, key); + + if (!g_strv_contains ((const char **)list, value)) + return TRUE; + + n_values = g_strv_length (list); + new_value = g_new0 (char *, n_values); + i = 0; + for (s = (const char **)list; *s != NULL; s++) + if (!g_str_equal (*s, value)) + new_value[i++] = g_strdup (*s); + + g_settings_set_strv (settings, key, (const char **)new_value); + g_settings_sync (); + + return TRUE; +} + +void +print_extension_info (GVariantDict *info, + DisplayFormat format) +{ + const char *uuid, *name, *desc, *path, *url, *author; + double state, version; + + g_variant_dict_lookup (info, "uuid", "&s", &uuid); + g_print ("%s\n", uuid); + + if (format == DISPLAY_ONELINE) + return; + + g_variant_dict_lookup (info, "name", "&s", &name); + g_print (" %s: %s\n", _("Name"), name); + + g_variant_dict_lookup (info, "description", "&s", &desc); + g_print (" %s: %s\n", _("Description"), desc); + + g_variant_dict_lookup (info, "path", "&s", &path); + g_print (" %s: %s\n", _("Path"), path); + + if (g_variant_dict_lookup (info, "url", "&s", &url)) + g_print (" %s: %s\n", _("URL"), url); + + if (g_variant_dict_lookup (info, "original-author", "&s", &author)) + g_print (" %s: %s\n", _("Original author"), author); + + if (g_variant_dict_lookup (info, "version", "d", &version)) + g_print (" %s: %.0f\n", _("Version"), version); + + g_variant_dict_lookup (info, "state", "d", &state); + g_print (" %s: %s\n", _("State"), extension_state_to_string (state)); +} + +gboolean +file_delete_recursively (GFile *file, + GError **error) +{ + g_autoptr (GFileEnumerator) file_enum = NULL; + GFile *child; + + file_enum = g_file_enumerate_children (file, + G_FILE_ATTRIBUTE_STANDARD_NAME, + G_FILE_QUERY_INFO_NONE, + NULL, + NULL); + if (file_enum) + while (TRUE) + { + if (!g_file_enumerator_iterate (file_enum, NULL, &child, NULL, error)) + return FALSE; + + if (child == NULL) + break; + + if (!file_delete_recursively (child, error)) + return FALSE; + } + + return g_file_delete (file, NULL, error); +} + + +static int +handle_version (int argc, char *argv[], gboolean do_help) +{ + if (do_help || argc > 1) + { + if (!do_help) + g_printerr ("gnome-extensions: %s\n\n", _("“version” takes no arguments")); + + g_printerr ("%s\n", _("Usage:")); + g_printerr (" gnome-extensions version\n"); + g_printerr ("\n"); + g_printerr ("%s\n", _("Print version information and exit.")); + + return do_help ? 0 : 2; + } + + g_print ("%s\n", VERSION); + + return 0; +} + +static void +usage (void) +{ + g_autofree char *help_command = NULL; + + help_command = g_strdup_printf ("gnome-extensions help %s", _("COMMAND")); + + g_printerr ("%s\n", _("Usage:")); + g_printerr (" gnome-extensions %s %s\n", _("COMMAND"), _("[ARGS…]")); + g_printerr ("\n"); + g_printerr ("%s\n", _("Commands:")); + g_printerr (" help %s\n", _("Print help")); + g_printerr (" version %s\n", _("Print version")); + g_printerr (" enable %s\n", _("Enable extension")); + g_printerr (" disable %s\n", _("Disable extension")); + g_printerr (" reset %s\n", _("Reset extension")); + g_printerr (" uninstall %s\n", _("Uninstall extension")); + g_printerr (" list %s\n", _("List extensions")); + g_printerr (" info %s\n", _("Show extension info")); + g_printerr (" show %s\n", _("Show extension info")); + g_printerr (" prefs %s\n", _("Open extension preferences")); + g_printerr (" create %s\n", _("Create extension")); + g_printerr (" pack %s\n", _("Package extension")); + g_printerr (" install %s\n", _("Install extension bundle")); + g_printerr ("\n"); + g_printerr (_("Use “%s” to get detailed help.\n"), help_command); +} + +int +main (int argc, char *argv[]) +{ + const char *command; + gboolean do_help = FALSE; + + setlocale (LC_ALL, ""); + textdomain (GETTEXT_PACKAGE); + bindtextdomain (GETTEXT_PACKAGE, LOCALEDIR); + +#ifdef HAVE_BIND_TEXTDOMAIN_CODESET + bind_textdomain_codeset (GETTEXT_PACKAGE, "UTF-8"); +#endif + + if (argc < 2) + { + usage (); + return 1; + } + + command = argv[1]; + argc--; + argv++; + + if (g_str_equal (command, "help")) + { + if (argc == 1) + { + usage (); + return 0; + } + else + { + command = argv[1]; + do_help = TRUE; + } + } + else if (g_str_equal (command, "--help")) + { + usage (); + return 0; + } + else if (g_str_equal (command, "--version")) + { + command = "version"; + } + + if (g_str_equal (command, "version")) + return handle_version (argc, argv, do_help); + else if (g_str_equal (command, "enable")) + return handle_enable (argc, argv, do_help); + else if (g_str_equal (command, "disable")) + return handle_disable (argc, argv, do_help); + else if (g_str_equal (command, "reset")) + return handle_reset (argc, argv, do_help); + else if (g_str_equal (command, "list")) + return handle_list (argc, argv, do_help); + else if (g_str_equal (command, "info")) + return handle_info (argc, argv, do_help); + else if (g_str_equal (command, "show")) + return handle_info (argc, argv, do_help); + else if (g_str_equal (command, "prefs")) + return handle_prefs (argc, argv, do_help); + else if (g_str_equal (command, "create")) + return handle_create (argc, argv, do_help); + else if (g_str_equal (command, "pack")) + return handle_pack (argc, argv, do_help); + else if (g_str_equal (command, "install")) + return handle_install (argc, argv, do_help); + else if (g_str_equal (command, "uninstall")) + return handle_uninstall (argc, argv, do_help); + else + usage (); + + return 1; +} diff --git a/subprojects/extensions-tool/src/meson.build b/subprojects/extensions-tool/src/meson.build new file mode 100644 index 0000000..a855fef --- /dev/null +++ b/subprojects/extensions-tool/src/meson.build @@ -0,0 +1,37 @@ +config_h = configuration_data() +config_h.set_quoted('GETTEXT_PACKAGE', package_name) +config_h.set_quoted('VERSION', meson.project_version()) +config_h.set_quoted('LOCALEDIR', localedir) +config_h.set('HAVE_BIND_TEXTDOMAIN_CODESET', cc.has_function('bind_textdomain_codeset')) +configure_file( + output: 'config.h', + configuration: config_h, +) + +sources = [ + 'command-create.c', + 'command-disable.c', + 'command-enable.c', + 'command-info.c', + 'command-install.c', + 'command-list.c', + 'command-pack.c', + 'command-prefs.c', + 'command-reset.c', + 'command-uninstall.c', + 'main.c' +] + +subdir('templates') + +resources = gnome.compile_resources('resources', + 'gnome-extensions-tool.gresource.xml', + source_dir: ['.', meson.current_build_dir()], + dependencies: template_deps, +) + +executable('gnome-extensions', + sources, resources, + dependencies: [gio_dep, gio_unix_dep, autoar_dep, json_dep], + install: true +) diff --git a/subprojects/extensions-tool/src/templates/00-plain.desktop.in b/subprojects/extensions-tool/src/templates/00-plain.desktop.in new file mode 100644 index 0000000..36ddf80 --- /dev/null +++ b/subprojects/extensions-tool/src/templates/00-plain.desktop.in @@ -0,0 +1,5 @@ +[Desktop Entry] +Type=Application +Name=Plain +Comment=An empty extension +Path=plain diff --git a/subprojects/extensions-tool/src/templates/indicator.desktop.in b/subprojects/extensions-tool/src/templates/indicator.desktop.in new file mode 100644 index 0000000..1718e94 --- /dev/null +++ b/subprojects/extensions-tool/src/templates/indicator.desktop.in @@ -0,0 +1,5 @@ +[Desktop Entry] +Type=Application +Name=Indicator +Comment=Add an icon to the top bar +Path=indicator diff --git a/subprojects/extensions-tool/src/templates/indicator/extension.js b/subprojects/extensions-tool/src/templates/indicator/extension.js new file mode 100644 index 0000000..9ed2c38 --- /dev/null +++ b/subprojects/extensions-tool/src/templates/indicator/extension.js @@ -0,0 +1,70 @@ +/* extension.js + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +/* exported init */ + +const GETTEXT_DOMAIN = 'my-indicator-extension'; + +const { GObject, St } = imports.gi; + +const ExtensionUtils = imports.misc.extensionUtils; +const Main = imports.ui.main; +const PanelMenu = imports.ui.panelMenu; +const PopupMenu = imports.ui.popupMenu; + +const _ = ExtensionUtils.gettext; + +const Indicator = GObject.registerClass( +class Indicator extends PanelMenu.Button { + _init() { + super._init(0.0, _('My Shiny Indicator')); + + this.add_child(new St.Icon({ + icon_name: 'face-smile-symbolic', + style_class: 'system-status-icon', + })); + + let item = new PopupMenu.PopupMenuItem(_('Show Notification')); + item.connect('activate', () => { + Main.notify(_('Whatʼs up, folks?')); + }); + this.menu.addMenuItem(item); + } +}); + +class Extension { + constructor(uuid) { + this._uuid = uuid; + + ExtensionUtils.initTranslations(GETTEXT_DOMAIN); + } + + enable() { + this._indicator = new Indicator(); + Main.panel.addToStatusArea(this._uuid, this._indicator); + } + + disable() { + this._indicator.destroy(); + this._indicator = null; + } +} + +function init(meta) { + return new Extension(meta.uuid); +} diff --git a/subprojects/extensions-tool/src/templates/indicator/stylesheet.css b/subprojects/extensions-tool/src/templates/indicator/stylesheet.css new file mode 100644 index 0000000..37b93f2 --- /dev/null +++ b/subprojects/extensions-tool/src/templates/indicator/stylesheet.css @@ -0,0 +1 @@ +/* Add your custom extension styling here */ diff --git a/subprojects/extensions-tool/src/templates/meson.build b/subprojects/extensions-tool/src/templates/meson.build new file mode 100644 index 0000000..d693bfa --- /dev/null +++ b/subprojects/extensions-tool/src/templates/meson.build @@ -0,0 +1,13 @@ +template_metas = [ + '00-plain.desktop', + 'indicator.desktop', +] +template_deps = [] +foreach template : template_metas + template_deps += i18n.merge_file( + input: template + '.in', + output: template, + po_dir: po_dir, + type: 'desktop', + ) +endforeach diff --git a/subprojects/extensions-tool/src/templates/plain/extension.js b/subprojects/extensions-tool/src/templates/plain/extension.js new file mode 100644 index 0000000..64857af --- /dev/null +++ b/subprojects/extensions-tool/src/templates/plain/extension.js @@ -0,0 +1,34 @@ +/* extension.js + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +/* exported init */ + +class Extension { + constructor() { + } + + enable() { + } + + disable() { + } +} + +function init() { + return new Extension(); +} diff --git a/subprojects/extensions-tool/src/templates/plain/stylesheet.css b/subprojects/extensions-tool/src/templates/plain/stylesheet.css new file mode 100644 index 0000000..37b93f2 --- /dev/null +++ b/subprojects/extensions-tool/src/templates/plain/stylesheet.css @@ -0,0 +1 @@ +/* Add your custom extension styling here */ diff --git a/subprojects/gvc/.gitignore b/subprojects/gvc/.gitignore new file mode 100644 index 0000000..7d2ebe7 --- /dev/null +++ b/subprojects/gvc/.gitignore @@ -0,0 +1,11 @@ +.deps/ +.libs/ +.dirstamp +Makefile.in +Makefile +*.la +*.lo +*.o +*.gir +*.typelib +test-audio-device-selection diff --git a/subprojects/gvc/.gitlab-ci.yml b/subprojects/gvc/.gitlab-ci.yml new file mode 100644 index 0000000..447a655 --- /dev/null +++ b/subprojects/gvc/.gitlab-ci.yml @@ -0,0 +1,16 @@ +stages: +- test + +build-fedora: + image: fedora:latest + stage: test + before_script: + - dnf install -y redhat-rpm-config gcc clang meson pulseaudio-libs-devel alsa-lib-devel gtk3-devel + script: + - cd .gitlab-ci + - meson _build + - ninja -C _build + - rm -rf _build + - CC=clang meson _build + - ninja -C _build + diff --git a/subprojects/gvc/.gitlab-ci/meson.build b/subprojects/gvc/.gitlab-ci/meson.build new file mode 100644 index 0000000..d54e1dd --- /dev/null +++ b/subprojects/gvc/.gitlab-ci/meson.build @@ -0,0 +1,23 @@ +project('gnome-volume-control-ci', 'c', + version: '1.0.0', + meson_version: '>= 0.47.0', + license: 'GPLv2+' +) + +prefix = get_option('prefix') + +datadir = join_paths(prefix, get_option('datadir')) +libdir = join_paths(prefix, get_option('libdir')) + +pkgdatadir = join_paths(datadir, meson.project_name()) +pkglibdir = join_paths(libdir, meson.project_name()) + +libgvc = subproject('gvc', + default_options: [ + 'package_name=' + meson.project_name(), + 'package_version=' + meson.project_version(), + 'pkgdatadir=' + pkgdatadir, + 'pkglibdir=' + pkglibdir, + 'alsa=true' + ] +) diff --git a/subprojects/gvc/.gitlab-ci/subprojects/gvc b/subprojects/gvc/.gitlab-ci/subprojects/gvc new file mode 120000 index 0000000..6581736 --- /dev/null +++ b/subprojects/gvc/.gitlab-ci/subprojects/gvc @@ -0,0 +1 @@ +../../
\ No newline at end of file diff --git a/subprojects/gvc/README.md b/subprojects/gvc/README.md new file mode 100644 index 0000000..2fabe49 --- /dev/null +++ b/subprojects/gvc/README.md @@ -0,0 +1,12 @@ +# libgnome-volume-control + +libgnome-volume-control is a copy library that's supposed to be used as +a git sub-module. If your project uses some of libgnome-volume-control's +strings in a user-facing manner, don't forget to add those files to your +POTFILES.in for translation. + +## Projects using libgnome-volume-control + +- [gnome-shell](https://gitlab.gnome.org/GNOME/gnome-shell) +- [gnome-settings-daemon](https://gitlab.gnome.org/GNOME/gnome-settings-daemon) +- [gnome-control-center](https://gitlab.gnome.org/GNOME/gnome-control-center) diff --git a/subprojects/gvc/gvc-channel-map-private.h b/subprojects/gvc/gvc-channel-map-private.h new file mode 100644 index 0000000..3949de3 --- /dev/null +++ b/subprojects/gvc/gvc-channel-map-private.h @@ -0,0 +1,39 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2008 Red Hat, Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * + */ + +#ifndef __GVC_CHANNEL_MAP_PRIVATE_H +#define __GVC_CHANNEL_MAP_PRIVATE_H + +#include <glib-object.h> +#include <pulse/pulseaudio.h> + +G_BEGIN_DECLS + +GvcChannelMap * gvc_channel_map_new_from_pa_channel_map (const pa_channel_map *map); +const pa_channel_map * gvc_channel_map_get_pa_channel_map (const GvcChannelMap *map); + +void gvc_channel_map_volume_changed (GvcChannelMap *map, + const pa_cvolume *cv, + gboolean set); +const pa_cvolume * gvc_channel_map_get_cvolume (const GvcChannelMap *map); + +G_END_DECLS + +#endif /* __GVC_CHANNEL_MAP_PRIVATE_H */ diff --git a/subprojects/gvc/gvc-channel-map.c b/subprojects/gvc/gvc-channel-map.c new file mode 100644 index 0000000..688a451 --- /dev/null +++ b/subprojects/gvc/gvc-channel-map.c @@ -0,0 +1,246 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2008 William Jon McCann + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * + */ + +#include "config.h" + +#include <stdlib.h> +#include <stdio.h> +#include <unistd.h> + +#include <glib.h> +#include <glib/gi18n-lib.h> + +#include <pulse/pulseaudio.h> + +#include "gvc-channel-map.h" +#include "gvc-channel-map-private.h" + +struct GvcChannelMapPrivate +{ + pa_channel_map pa_map; + gboolean pa_volume_is_set; + pa_cvolume pa_volume; + gdouble extern_volume[NUM_TYPES]; /* volume, balance, fade, lfe */ + gboolean can_balance; + gboolean can_fade; +}; + +enum { + VOLUME_CHANGED, + LAST_SIGNAL +}; + +static guint signals [LAST_SIGNAL] = { 0, }; + +static void gvc_channel_map_finalize (GObject *object); + +G_DEFINE_TYPE_WITH_PRIVATE (GvcChannelMap, gvc_channel_map, G_TYPE_OBJECT) + +guint +gvc_channel_map_get_num_channels (const GvcChannelMap *map) +{ + g_return_val_if_fail (GVC_IS_CHANNEL_MAP (map), 0); + + if (!pa_channel_map_valid(&map->priv->pa_map)) + return 0; + + return map->priv->pa_map.channels; +} + +const gdouble * +gvc_channel_map_get_volume (GvcChannelMap *map) +{ + g_return_val_if_fail (GVC_IS_CHANNEL_MAP (map), NULL); + + if (!pa_channel_map_valid(&map->priv->pa_map)) + return NULL; + + map->priv->extern_volume[VOLUME] = (gdouble) pa_cvolume_max (&map->priv->pa_volume); + if (gvc_channel_map_can_balance (map)) + map->priv->extern_volume[BALANCE] = (gdouble) pa_cvolume_get_balance (&map->priv->pa_volume, &map->priv->pa_map); + else + map->priv->extern_volume[BALANCE] = 0; + if (gvc_channel_map_can_fade (map)) + map->priv->extern_volume[FADE] = (gdouble) pa_cvolume_get_fade (&map->priv->pa_volume, &map->priv->pa_map); + else + map->priv->extern_volume[FADE] = 0; + if (gvc_channel_map_has_lfe (map)) + map->priv->extern_volume[LFE] = (gdouble) pa_cvolume_get_position (&map->priv->pa_volume, &map->priv->pa_map, PA_CHANNEL_POSITION_LFE); + else + map->priv->extern_volume[LFE] = 0; + + return map->priv->extern_volume; +} + +gboolean +gvc_channel_map_can_balance (const GvcChannelMap *map) +{ + g_return_val_if_fail (GVC_IS_CHANNEL_MAP (map), FALSE); + + return map->priv->can_balance; +} + +gboolean +gvc_channel_map_can_fade (const GvcChannelMap *map) +{ + g_return_val_if_fail (GVC_IS_CHANNEL_MAP (map), FALSE); + + return map->priv->can_fade; +} + +const char * +gvc_channel_map_get_mapping (const GvcChannelMap *map) +{ + g_return_val_if_fail (GVC_IS_CHANNEL_MAP (map), NULL); + + if (!pa_channel_map_valid(&map->priv->pa_map)) + return NULL; + + return pa_channel_map_to_pretty_name (&map->priv->pa_map); +} + +/** + * gvc_channel_map_has_position: (skip) + * @map: + * @position: + * + * Returns: + */ +gboolean +gvc_channel_map_has_position (const GvcChannelMap *map, + pa_channel_position_t position) +{ + g_return_val_if_fail (GVC_IS_CHANNEL_MAP (map), FALSE); + + return pa_channel_map_has_position (&(map->priv->pa_map), position); +} + +const pa_channel_map * +gvc_channel_map_get_pa_channel_map (const GvcChannelMap *map) +{ + g_return_val_if_fail (GVC_IS_CHANNEL_MAP (map), NULL); + + if (!pa_channel_map_valid(&map->priv->pa_map)) + return NULL; + + return &map->priv->pa_map; +} + +const pa_cvolume * +gvc_channel_map_get_cvolume (const GvcChannelMap *map) +{ + g_return_val_if_fail (GVC_IS_CHANNEL_MAP (map), NULL); + + if (!pa_channel_map_valid(&map->priv->pa_map)) + return NULL; + + return &map->priv->pa_volume; +} + +static void +gvc_channel_map_class_init (GvcChannelMapClass *klass) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS (klass); + + gobject_class->finalize = gvc_channel_map_finalize; + + signals [VOLUME_CHANGED] = + g_signal_new ("volume-changed", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (GvcChannelMapClass, volume_changed), + NULL, NULL, NULL, + G_TYPE_NONE, 1, G_TYPE_BOOLEAN); +} + +void +gvc_channel_map_volume_changed (GvcChannelMap *map, + const pa_cvolume *cv, + gboolean set) +{ + g_return_if_fail (GVC_IS_CHANNEL_MAP (map)); + g_return_if_fail (cv != NULL); + g_return_if_fail (pa_cvolume_compatible_with_channel_map(cv, &map->priv->pa_map)); + + if (pa_cvolume_equal(cv, &map->priv->pa_volume)) + return; + + map->priv->pa_volume = *cv; + + if (map->priv->pa_volume_is_set == FALSE) { + map->priv->pa_volume_is_set = TRUE; + return; + } + g_signal_emit (map, signals[VOLUME_CHANGED], 0, set); +} + +static void +gvc_channel_map_init (GvcChannelMap *map) +{ + map->priv = gvc_channel_map_get_instance_private (map); + map->priv->pa_volume_is_set = FALSE; +} + +static void +gvc_channel_map_finalize (GObject *object) +{ + GvcChannelMap *channel_map; + + g_return_if_fail (object != NULL); + g_return_if_fail (GVC_IS_CHANNEL_MAP (object)); + + channel_map = GVC_CHANNEL_MAP (object); + + g_return_if_fail (channel_map->priv != NULL); + + G_OBJECT_CLASS (gvc_channel_map_parent_class)->finalize (object); +} + +GvcChannelMap * +gvc_channel_map_new (void) +{ + GObject *map; + map = g_object_new (GVC_TYPE_CHANNEL_MAP, NULL); + return GVC_CHANNEL_MAP (map); +} + +static void +set_from_pa_map (GvcChannelMap *map, + const pa_channel_map *pa_map) +{ + g_assert (pa_channel_map_valid(pa_map)); + + map->priv->can_balance = pa_channel_map_can_balance (pa_map); + map->priv->can_fade = pa_channel_map_can_fade (pa_map); + + map->priv->pa_map = *pa_map; + pa_cvolume_set(&map->priv->pa_volume, pa_map->channels, PA_VOLUME_NORM); +} + +GvcChannelMap * +gvc_channel_map_new_from_pa_channel_map (const pa_channel_map *pa_map) +{ + GObject *map; + map = g_object_new (GVC_TYPE_CHANNEL_MAP, NULL); + + set_from_pa_map (GVC_CHANNEL_MAP (map), pa_map); + + return GVC_CHANNEL_MAP (map); +} diff --git a/subprojects/gvc/gvc-channel-map.h b/subprojects/gvc/gvc-channel-map.h new file mode 100644 index 0000000..85c5772 --- /dev/null +++ b/subprojects/gvc/gvc-channel-map.h @@ -0,0 +1,73 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2008 Red Hat, Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * + */ + +#ifndef __GVC_CHANNEL_MAP_H +#define __GVC_CHANNEL_MAP_H + +#include <glib-object.h> +#include <gvc-pulseaudio-fake.h> + +G_BEGIN_DECLS + +#define GVC_TYPE_CHANNEL_MAP (gvc_channel_map_get_type ()) +#define GVC_CHANNEL_MAP(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), GVC_TYPE_CHANNEL_MAP, GvcChannelMap)) +#define GVC_CHANNEL_MAP_CLASS(k) (G_TYPE_CHECK_CLASS_CAST((k), GVC_TYPE_CHANNEL_MAP, GvcChannelMapClass)) +#define GVC_IS_CHANNEL_MAP(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), GVC_TYPE_CHANNEL_MAP)) +#define GVC_IS_CHANNEL_MAP_CLASS(k) (G_TYPE_CHECK_CLASS_TYPE ((k), GVC_TYPE_CHANNEL_MAP)) +#define GVC_CHANNEL_MAP_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), GVC_TYPE_CHANNEL_MAP, GvcChannelMapClass)) + +typedef struct GvcChannelMapPrivate GvcChannelMapPrivate; + +typedef struct +{ + GObject parent; + GvcChannelMapPrivate *priv; +} GvcChannelMap; + +typedef struct +{ + GObjectClass parent_class; + void (*volume_changed) (GvcChannelMap *channel_map, gboolean set); +} GvcChannelMapClass; + +enum { + VOLUME, + BALANCE, + FADE, + LFE, + NUM_TYPES +}; + +GType gvc_channel_map_get_type (void); + +GvcChannelMap * gvc_channel_map_new (void); +guint gvc_channel_map_get_num_channels (const GvcChannelMap *map); +const gdouble * gvc_channel_map_get_volume (GvcChannelMap *map); +gboolean gvc_channel_map_can_balance (const GvcChannelMap *map); +gboolean gvc_channel_map_can_fade (const GvcChannelMap *map); +gboolean gvc_channel_map_has_position (const GvcChannelMap *map, + pa_channel_position_t position); +#define gvc_channel_map_has_lfe(x) gvc_channel_map_has_position (x, PA_CHANNEL_POSITION_LFE) + +const char * gvc_channel_map_get_mapping (const GvcChannelMap *map); + +G_END_DECLS + +#endif /* __GVC_CHANNEL_MAP_H */ diff --git a/subprojects/gvc/gvc-mixer-card-private.h b/subprojects/gvc/gvc-mixer-card-private.h new file mode 100644 index 0000000..e190f7f --- /dev/null +++ b/subprojects/gvc/gvc-mixer-card-private.h @@ -0,0 +1,35 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2008-2009 Red Hat, Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * + */ + +#ifndef __GVC_MIXER_CARD_PRIVATE_H +#define __GVC_MIXER_CARD_PRIVATE_H + +#include <pulse/pulseaudio.h> +#include "gvc-mixer-card.h" + +G_BEGIN_DECLS + +GvcMixerCard * gvc_mixer_card_new (pa_context *context, + guint index); +pa_context * gvc_mixer_card_get_pa_context (GvcMixerCard *card); + +G_END_DECLS + +#endif /* __GVC_MIXER_CARD_PRIVATE_H */ diff --git a/subprojects/gvc/gvc-mixer-card.c b/subprojects/gvc/gvc-mixer-card.c new file mode 100644 index 0000000..39f59ca --- /dev/null +++ b/subprojects/gvc/gvc-mixer-card.c @@ -0,0 +1,574 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2008 William Jon McCann + * Copyright (C) 2009 Bastien Nocera + * Copyright (C) Conor Curran 2011 <conor.curran@canonical.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * + */ + +#include "config.h" + +#include <stdlib.h> +#include <stdio.h> +#include <unistd.h> + +#include <glib.h> +#include <glib/gi18n-lib.h> + +#include <pulse/pulseaudio.h> + +#include "gvc-mixer-card.h" +#include "gvc-mixer-card-private.h" + +static guint32 card_serial = 1; + +struct GvcMixerCardPrivate +{ + pa_context *pa_context; + guint id; + guint index; + char *name; + char *icon_name; + char *profile; + char *target_profile; + char *human_profile; + GList *profiles; + pa_operation *profile_op; + GList *ports; +}; + +enum +{ + PROP_0, + PROP_ID, + PROP_PA_CONTEXT, + PROP_INDEX, + PROP_NAME, + PROP_ICON_NAME, + PROP_PROFILE, + PROP_HUMAN_PROFILE, + N_PROPS +}; +static GParamSpec *obj_props[N_PROPS] = { NULL, }; + +static void gvc_mixer_card_finalize (GObject *object); + +G_DEFINE_TYPE_WITH_PRIVATE (GvcMixerCard, gvc_mixer_card, G_TYPE_OBJECT) + +static guint32 +get_next_card_serial (void) +{ + guint32 serial; + + serial = card_serial++; + + if ((gint32)card_serial < 0) { + card_serial = 1; + } + + return serial; +} + +pa_context * +gvc_mixer_card_get_pa_context (GvcMixerCard *card) +{ + g_return_val_if_fail (GVC_IS_MIXER_CARD (card), 0); + return card->priv->pa_context; +} + +guint +gvc_mixer_card_get_index (GvcMixerCard *card) +{ + g_return_val_if_fail (GVC_IS_MIXER_CARD (card), 0); + return card->priv->index; +} + +guint +gvc_mixer_card_get_id (GvcMixerCard *card) +{ + g_return_val_if_fail (GVC_IS_MIXER_CARD (card), 0); + return card->priv->id; +} + +const char * +gvc_mixer_card_get_name (GvcMixerCard *card) +{ + g_return_val_if_fail (GVC_IS_MIXER_CARD (card), NULL); + return card->priv->name; +} + +gboolean +gvc_mixer_card_set_name (GvcMixerCard *card, + const char *name) +{ + g_return_val_if_fail (GVC_IS_MIXER_CARD (card), FALSE); + + g_free (card->priv->name); + card->priv->name = g_strdup (name); + g_object_notify_by_pspec (G_OBJECT (card), obj_props[PROP_NAME]); + + return TRUE; +} + +const char * +gvc_mixer_card_get_icon_name (GvcMixerCard *card) +{ + g_return_val_if_fail (GVC_IS_MIXER_CARD (card), NULL); + return card->priv->icon_name; +} + +gboolean +gvc_mixer_card_set_icon_name (GvcMixerCard *card, + const char *icon_name) +{ + g_return_val_if_fail (GVC_IS_MIXER_CARD (card), FALSE); + + g_free (card->priv->icon_name); + card->priv->icon_name = g_strdup (icon_name); + g_object_notify_by_pspec (G_OBJECT (card), obj_props[PROP_ICON_NAME]); + + return TRUE; +} + +/** + * gvc_mixer_card_get_profile: (skip) + * @card: + * + * Returns: + */ +GvcMixerCardProfile * +gvc_mixer_card_get_profile (GvcMixerCard *card) +{ + GList *l; + + g_return_val_if_fail (GVC_IS_MIXER_CARD (card), NULL); + g_return_val_if_fail (card->priv->profiles != NULL, NULL); + + for (l = card->priv->profiles; l != NULL; l = l->next) { + GvcMixerCardProfile *p = l->data; + if (g_str_equal (card->priv->profile, p->profile)) { + return p; + } + } + + g_assert_not_reached (); + + return NULL; +} + +gboolean +gvc_mixer_card_set_profile (GvcMixerCard *card, + const char *profile) +{ + GList *l; + + g_return_val_if_fail (GVC_IS_MIXER_CARD (card), FALSE); + g_return_val_if_fail (card->priv->profiles != NULL, FALSE); + + g_free (card->priv->profile); + card->priv->profile = g_strdup (profile); + + g_free (card->priv->human_profile); + card->priv->human_profile = NULL; + + for (l = card->priv->profiles; l != NULL; l = l->next) { + GvcMixerCardProfile *p = l->data; + if (g_str_equal (card->priv->profile, p->profile)) { + card->priv->human_profile = g_strdup (p->human_profile); + break; + } + } + + g_object_notify_by_pspec (G_OBJECT (card), obj_props[PROP_PROFILE]); + + return TRUE; +} + +static void +_pa_context_set_card_profile_by_index_cb (pa_context *context, + int success, + void *userdata) +{ + GvcMixerCard *card = GVC_MIXER_CARD (userdata); + + g_assert (card->priv->target_profile); + + if (success > 0) { + gvc_mixer_card_set_profile (card, card->priv->target_profile); + } else { + g_debug ("Failed to switch profile on '%s' from '%s' to '%s'", + card->priv->name, + card->priv->profile, + card->priv->target_profile); + } + g_free (card->priv->target_profile); + card->priv->target_profile = NULL; + + pa_operation_unref (card->priv->profile_op); + card->priv->profile_op = NULL; +} + +/** + * gvc_mixer_card_change_profile: + * @card: a #GvcMixerCard + * @profile: (allow-none): the profile to change to or %NULL. + * + * Change the profile in use on this card. + * + * Returns: %TRUE if profile successfully changed or already using this profile. + */ +gboolean +gvc_mixer_card_change_profile (GvcMixerCard *card, + const char *profile) +{ + g_return_val_if_fail (GVC_IS_MIXER_CARD (card), FALSE); + g_return_val_if_fail (card->priv->profiles != NULL, FALSE); + + /* Same profile, or already requested? */ + if (g_strcmp0 (card->priv->profile, profile) == 0) + return TRUE; + if (g_strcmp0 (profile, card->priv->target_profile) == 0) + return TRUE; + if (card->priv->profile_op != NULL) { + pa_operation_cancel (card->priv->profile_op); + pa_operation_unref (card->priv->profile_op); + card->priv->profile_op = NULL; + } + + if (card->priv->profile != NULL) { + g_free (card->priv->target_profile); + card->priv->target_profile = g_strdup (profile); + + card->priv->profile_op = pa_context_set_card_profile_by_index (card->priv->pa_context, + card->priv->index, + card->priv->target_profile, + _pa_context_set_card_profile_by_index_cb, + card); + + if (card->priv->profile_op == NULL) { + g_warning ("pa_context_set_card_profile_by_index() failed"); + return FALSE; + } + } else { + g_assert (card->priv->human_profile == NULL); + card->priv->profile = g_strdup (profile); + } + + return TRUE; +} + +/** + * gvc_mixer_card_get_profiles: + * + * Return value: (transfer none) (element-type GvcMixerCardProfile): + */ +const GList * +gvc_mixer_card_get_profiles (GvcMixerCard *card) +{ + g_return_val_if_fail (GVC_IS_MIXER_CARD (card), NULL); + return card->priv->profiles; +} + +/** + * gvc_mixer_card_get_ports: + * + * Return value: (transfer none) (element-type GvcMixerCardPort): + */ +const GList * +gvc_mixer_card_get_ports (GvcMixerCard *card) +{ + g_return_val_if_fail (GVC_IS_MIXER_CARD (card), NULL); + return card->priv->ports; +} + +/** + * gvc_mixer_card_profile_compare: + * + * Return value: 1 if @a has a higher priority, -1 if @b has a higher + * priority, 0 if @a and @b have the same priority. + */ +int +gvc_mixer_card_profile_compare (GvcMixerCardProfile *a, + GvcMixerCardProfile *b) +{ + if (a->priority == b->priority) + return 0; + if (a->priority > b->priority) + return 1; + return -1; +} + +/** + * gvc_mixer_card_set_profiles: + * @profiles: (transfer full) (element-type GvcMixerCardProfile): + */ +gboolean +gvc_mixer_card_set_profiles (GvcMixerCard *card, + GList *profiles) +{ + g_return_val_if_fail (GVC_IS_MIXER_CARD (card), FALSE); + g_return_val_if_fail (card->priv->profiles == NULL, FALSE); + + card->priv->profiles = g_list_sort (profiles, (GCompareFunc) gvc_mixer_card_profile_compare); + + return TRUE; +} + +/** + * gvc_mixer_card_get_gicon: + * @card: + * + * Return value: (transfer full): + */ +GIcon * +gvc_mixer_card_get_gicon (GvcMixerCard *card) +{ + g_return_val_if_fail (GVC_IS_MIXER_CARD (card), NULL); + + if (card->priv->icon_name == NULL) + return NULL; + + return g_themed_icon_new_with_default_fallbacks (card->priv->icon_name); +} + +static void +free_port (GvcMixerCardPort *port) +{ + g_free (port->port); + g_free (port->human_port); + g_free (port->icon_name); + g_list_free (port->profiles); + + g_free (port); +} + +/** + * gvc_mixer_card_set_ports: + * @ports: (transfer full) (element-type GvcMixerCardPort): + */ +gboolean +gvc_mixer_card_set_ports (GvcMixerCard *card, + GList *ports) +{ + g_return_val_if_fail (GVC_IS_MIXER_CARD (card), FALSE); + g_return_val_if_fail (card->priv->ports == NULL, FALSE); + + g_list_free_full (card->priv->ports, (GDestroyNotify) free_port); + card->priv->ports = ports; + + return TRUE; +} + +static void +gvc_mixer_card_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + GvcMixerCard *self = GVC_MIXER_CARD (object); + + switch (prop_id) { + case PROP_PA_CONTEXT: + self->priv->pa_context = g_value_get_pointer (value); + break; + case PROP_INDEX: + self->priv->index = g_value_get_ulong (value); + break; + case PROP_ID: + self->priv->id = g_value_get_ulong (value); + break; + case PROP_NAME: + gvc_mixer_card_set_name (self, g_value_get_string (value)); + break; + case PROP_ICON_NAME: + gvc_mixer_card_set_icon_name (self, g_value_get_string (value)); + break; + case PROP_PROFILE: + gvc_mixer_card_set_profile (self, g_value_get_string (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +gvc_mixer_card_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + GvcMixerCard *self = GVC_MIXER_CARD (object); + + switch (prop_id) { + case PROP_PA_CONTEXT: + g_value_set_pointer (value, self->priv->pa_context); + break; + case PROP_INDEX: + g_value_set_ulong (value, self->priv->index); + break; + case PROP_ID: + g_value_set_ulong (value, self->priv->id); + break; + case PROP_NAME: + g_value_set_string (value, self->priv->name); + break; + case PROP_ICON_NAME: + g_value_set_string (value, self->priv->icon_name); + break; + case PROP_PROFILE: + g_value_set_string (value, self->priv->profile); + break; + case PROP_HUMAN_PROFILE: + g_value_set_string (value, self->priv->human_profile); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static GObject * +gvc_mixer_card_constructor (GType type, + guint n_construct_properties, + GObjectConstructParam *construct_params) +{ + GObject *object; + GvcMixerCard *self; + + object = G_OBJECT_CLASS (gvc_mixer_card_parent_class)->constructor (type, n_construct_properties, construct_params); + + self = GVC_MIXER_CARD (object); + + self->priv->id = get_next_card_serial (); + + return object; +} + +static void +gvc_mixer_card_class_init (GvcMixerCardClass *klass) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS (klass); + + gobject_class->constructor = gvc_mixer_card_constructor; + gobject_class->finalize = gvc_mixer_card_finalize; + + gobject_class->set_property = gvc_mixer_card_set_property; + gobject_class->get_property = gvc_mixer_card_get_property; + + obj_props[PROP_INDEX] = g_param_spec_ulong ("index", + "Index", + "The index for this card", + 0, G_MAXULONG, 0, + G_PARAM_READWRITE|G_PARAM_CONSTRUCT_ONLY|G_PARAM_STATIC_STRINGS); + obj_props[PROP_ID] = g_param_spec_ulong ("id", + "id", + "The id for this card", + 0, G_MAXULONG, 0, + G_PARAM_READWRITE|G_PARAM_CONSTRUCT_ONLY|G_PARAM_STATIC_STRINGS); + obj_props[PROP_PA_CONTEXT] = g_param_spec_pointer ("pa-context", + "PulseAudio context", + "The PulseAudio context for this card", + G_PARAM_READWRITE|G_PARAM_CONSTRUCT_ONLY|G_PARAM_STATIC_STRINGS); + obj_props[PROP_NAME] = g_param_spec_string ("name", + "Name", + "Name to display for this card", + NULL, + G_PARAM_READWRITE|G_PARAM_CONSTRUCT|G_PARAM_STATIC_STRINGS); + obj_props[PROP_ICON_NAME] = g_param_spec_string ("icon-name", + "Icon Name", + "Name of icon to display for this card", + NULL, + G_PARAM_READWRITE|G_PARAM_CONSTRUCT|G_PARAM_STATIC_STRINGS); + obj_props[PROP_PROFILE] = g_param_spec_string ("profile", + "Profile", + "Name of current profile for this card", + NULL, + G_PARAM_READWRITE|G_PARAM_STATIC_STRINGS); + obj_props[PROP_HUMAN_PROFILE] = g_param_spec_string ("human-profile", + "Profile (Human readable)", + "Name of current profile for this card in human readable form", + NULL, + G_PARAM_READABLE|G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (gobject_class, N_PROPS, obj_props); +} + +static void +gvc_mixer_card_init (GvcMixerCard *card) +{ + card->priv = gvc_mixer_card_get_instance_private (card); +} + +GvcMixerCard * +gvc_mixer_card_new (pa_context *context, + guint index) +{ + GObject *object; + + object = g_object_new (GVC_TYPE_MIXER_CARD, + "index", index, + "pa-context", context, + NULL); + return GVC_MIXER_CARD (object); +} + +static void +free_profile (GvcMixerCardProfile *p) +{ + g_free (p->profile); + g_free (p->human_profile); + g_free (p->status); + g_free (p); +} + +static void +gvc_mixer_card_finalize (GObject *object) +{ + GvcMixerCard *mixer_card; + + g_return_if_fail (object != NULL); + g_return_if_fail (GVC_IS_MIXER_CARD (object)); + + mixer_card = GVC_MIXER_CARD (object); + + g_return_if_fail (mixer_card->priv != NULL); + + g_free (mixer_card->priv->name); + mixer_card->priv->name = NULL; + + g_free (mixer_card->priv->icon_name); + mixer_card->priv->icon_name = NULL; + + g_free (mixer_card->priv->target_profile); + mixer_card->priv->target_profile = NULL; + + g_free (mixer_card->priv->profile); + mixer_card->priv->profile = NULL; + + g_free (mixer_card->priv->human_profile); + mixer_card->priv->human_profile = NULL; + + g_list_free_full (mixer_card->priv->profiles, (GDestroyNotify) free_profile); + mixer_card->priv->profiles = NULL; + + g_list_free_full (mixer_card->priv->ports, (GDestroyNotify) free_port); + mixer_card->priv->ports = NULL; + + G_OBJECT_CLASS (gvc_mixer_card_parent_class)->finalize (object); +} + diff --git a/subprojects/gvc/gvc-mixer-card.h b/subprojects/gvc/gvc-mixer-card.h new file mode 100644 index 0000000..814f8d4 --- /dev/null +++ b/subprojects/gvc/gvc-mixer-card.h @@ -0,0 +1,102 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2008-2009 Red Hat, Inc. + * Copyright (C) Conor Curran 2011 <conor.curran@canonical.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * + */ + +#ifndef __GVC_MIXER_CARD_H +#define __GVC_MIXER_CARD_H + +#include <glib-object.h> +#include <gio/gio.h> + +G_BEGIN_DECLS + +#define GVC_TYPE_MIXER_CARD (gvc_mixer_card_get_type ()) +#define GVC_MIXER_CARD(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), GVC_TYPE_MIXER_CARD, GvcMixerCard)) +#define GVC_MIXER_CARD_CLASS(k) (G_TYPE_CHECK_CLASS_CAST((k), GVC_TYPE_MIXER_CARD, GvcMixerCardClass)) +#define GVC_IS_MIXER_CARD(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), GVC_TYPE_MIXER_CARD)) +#define GVC_IS_MIXER_CARD_CLASS(k) (G_TYPE_CHECK_CLASS_TYPE ((k), GVC_TYPE_MIXER_CARD)) +#define GVC_MIXER_CARD_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), GVC_TYPE_MIXER_CARD, GvcMixerCardClass)) + +typedef struct GvcMixerCardPrivate GvcMixerCardPrivate; + +typedef struct +{ + GObject parent; + GvcMixerCardPrivate *priv; +} GvcMixerCard; + +typedef struct +{ + GObjectClass parent_class; + + /* vtable */ +} GvcMixerCardClass; + +typedef struct +{ + char *profile; + char *human_profile; + char *status; + guint priority; + guint n_sinks, n_sources; +} GvcMixerCardProfile; + +typedef struct +{ + char *port; + char *human_port; + char *icon_name; + guint priority; + gint available; + gint direction; + GList *profiles; +} GvcMixerCardPort; + +GType gvc_mixer_card_get_type (void); + +guint gvc_mixer_card_get_id (GvcMixerCard *card); +guint gvc_mixer_card_get_index (GvcMixerCard *card); +const char * gvc_mixer_card_get_name (GvcMixerCard *card); +const char * gvc_mixer_card_get_icon_name (GvcMixerCard *card); +GvcMixerCardProfile * gvc_mixer_card_get_profile (GvcMixerCard *card); +const GList * gvc_mixer_card_get_profiles (GvcMixerCard *card); +const GList * gvc_mixer_card_get_ports (GvcMixerCard *card); +gboolean gvc_mixer_card_change_profile (GvcMixerCard *card, + const char *profile); +GIcon * gvc_mixer_card_get_gicon (GvcMixerCard *card); + +int gvc_mixer_card_profile_compare (GvcMixerCardProfile *a, + GvcMixerCardProfile *b); + +/* private */ +gboolean gvc_mixer_card_set_name (GvcMixerCard *card, + const char *name); +gboolean gvc_mixer_card_set_icon_name (GvcMixerCard *card, + const char *name); +gboolean gvc_mixer_card_set_profile (GvcMixerCard *card, + const char *profile); +gboolean gvc_mixer_card_set_profiles (GvcMixerCard *card, + GList *profiles); +gboolean gvc_mixer_card_set_ports (GvcMixerCard *stream, + GList *ports); + +G_END_DECLS + +#endif /* __GVC_MIXER_CARD_H */ diff --git a/subprojects/gvc/gvc-mixer-control-private.h b/subprojects/gvc/gvc-mixer-control-private.h new file mode 100644 index 0000000..ac79975 --- /dev/null +++ b/subprojects/gvc/gvc-mixer-control-private.h @@ -0,0 +1,35 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2008 Red Hat, Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * + */ + +#ifndef __GVC_MIXER_CONTROL_PRIVATE_H +#define __GVC_MIXER_CONTROL_PRIVATE_H + +#include <glib-object.h> +#include <pulse/pulseaudio.h> +#include "gvc-mixer-stream.h" +#include "gvc-mixer-card.h" + +G_BEGIN_DECLS + +pa_context * gvc_mixer_control_get_pa_context (GvcMixerControl *control); + +G_END_DECLS + +#endif /* __GVC_MIXER_CONTROL_PRIVATE_H */ diff --git a/subprojects/gvc/gvc-mixer-control.c b/subprojects/gvc/gvc-mixer-control.c new file mode 100644 index 0000000..b603b77 --- /dev/null +++ b/subprojects/gvc/gvc-mixer-control.c @@ -0,0 +1,3881 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2006-2008 Lennart Poettering + * Copyright (C) 2008 Sjoerd Simons <sjoerd@luon.net> + * Copyright (C) 2008 William Jon McCann + * Copyright (C) 2012 Conor Curran + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * + */ + +#include "config.h" + +#include <stdlib.h> +#include <stdio.h> +#include <unistd.h> + +#include <glib.h> +#include <glib/gi18n-lib.h> + +#include <pulse/pulseaudio.h> +#include <pulse/glib-mainloop.h> +#include <pulse/ext-stream-restore.h> + +#ifdef HAVE_ALSA +#include <alsa/asoundlib.h> +#endif /* HAVE_ALSA */ + +#include "gvc-mixer-control.h" +#include "gvc-mixer-sink.h" +#include "gvc-mixer-source.h" +#include "gvc-mixer-sink-input.h" +#include "gvc-mixer-source-output.h" +#include "gvc-mixer-event-role.h" +#include "gvc-mixer-card.h" +#include "gvc-mixer-card-private.h" +#include "gvc-channel-map-private.h" +#include "gvc-mixer-control-private.h" +#include "gvc-mixer-ui-device.h" + +#define RECONNECT_DELAY 5 + +enum { + PROP_0, + PROP_NAME, + N_PROPS +}; +static GParamSpec *obj_props[N_PROPS] = { NULL, }; + +struct GvcMixerControlPrivate +{ + pa_glib_mainloop *pa_mainloop; + pa_mainloop_api *pa_api; + pa_context *pa_context; + guint server_protocol_version; + int n_outstanding; + guint reconnect_id; + char *name; + + gboolean default_sink_is_set; + guint default_sink_id; + char *default_sink_name; + gboolean default_source_is_set; + guint default_source_id; + char *default_source_name; + + gboolean event_sink_input_is_set; + guint event_sink_input_id; + + GHashTable *all_streams; + GHashTable *sinks; /* fixed outputs */ + GHashTable *sources; /* fixed inputs */ + GHashTable *sink_inputs; /* routable output streams */ + GHashTable *source_outputs; /* routable input streams */ + GHashTable *clients; + GHashTable *cards; + + GvcMixerStream *new_default_sink_stream; /* new default sink stream, used in gvc_mixer_control_set_default_sink () */ + GvcMixerStream *new_default_source_stream; /* new default source stream, used in gvc_mixer_control_set_default_source () */ + + GHashTable *ui_outputs; /* UI visible outputs */ + GHashTable *ui_inputs; /* UI visible inputs */ + + /* When we change profile on a device that is not the server default sink, + * it will jump back to the default sink set by the server to prevent the + * audio setup from being 'outputless'. + * + * All well and good but then when we get the new stream created for the + * new profile how do we know that this is the intended default or selected + * device the user wishes to use. */ + guint profile_swapping_device_id; + +#ifdef HAVE_ALSA + int headset_card; + gboolean has_headsetmic; + gboolean has_headphonemic; + gboolean headset_plugged_in; + char *headphones_name; + char *headsetmic_name; + char *headphonemic_name; + char *internalspk_name; + char *internalmic_name; +#endif /* HAVE_ALSA */ + + GvcMixerControlState state; +}; + +enum { + STATE_CHANGED, + STREAM_ADDED, + STREAM_REMOVED, + STREAM_CHANGED, + CARD_ADDED, + CARD_REMOVED, + DEFAULT_SINK_CHANGED, + DEFAULT_SOURCE_CHANGED, + ACTIVE_OUTPUT_UPDATE, + ACTIVE_INPUT_UPDATE, + OUTPUT_ADDED, + INPUT_ADDED, + OUTPUT_REMOVED, + INPUT_REMOVED, + AUDIO_DEVICE_SELECTION_NEEDED, + LAST_SIGNAL +}; + +static guint signals [LAST_SIGNAL] = { 0, }; + +static void gvc_mixer_control_finalize (GObject *object); + +G_DEFINE_TYPE_WITH_PRIVATE (GvcMixerControl, gvc_mixer_control, G_TYPE_OBJECT) + +pa_context * +gvc_mixer_control_get_pa_context (GvcMixerControl *control) +{ + g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), NULL); + return control->priv->pa_context; +} + +/** + * gvc_mixer_control_get_event_sink_input: + * @control: + * + * Returns: (transfer none): + */ +GvcMixerStream * +gvc_mixer_control_get_event_sink_input (GvcMixerControl *control) +{ + GvcMixerStream *stream; + + g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), NULL); + + stream = g_hash_table_lookup (control->priv->all_streams, + GUINT_TO_POINTER (control->priv->event_sink_input_id)); + + return stream; +} + +static void +gvc_mixer_control_stream_restore_cb (pa_context *c, + GvcMixerStream *new_stream, + const pa_ext_stream_restore_info *info, + GvcMixerControl *control) +{ + pa_operation *o; + pa_ext_stream_restore_info new_info; + + if (new_stream == NULL) + return; + + new_info.name = info->name; + new_info.channel_map = info->channel_map; + new_info.volume = info->volume; + new_info.mute = info->mute; + + new_info.device = gvc_mixer_stream_get_name (new_stream); + + o = pa_ext_stream_restore_write (control->priv->pa_context, + PA_UPDATE_REPLACE, + &new_info, 1, + TRUE, NULL, NULL); + + if (o == NULL) { + g_warning ("pa_ext_stream_restore_write() failed: %s", + pa_strerror (pa_context_errno (control->priv->pa_context))); + return; + } + + g_debug ("Changed default device for %s to %s", info->name, new_info.device); + + pa_operation_unref (o); +} + +static void +gvc_mixer_control_stream_restore_sink_cb (pa_context *c, + const pa_ext_stream_restore_info *info, + int eol, + void *userdata) +{ + GvcMixerControl *control = (GvcMixerControl *) userdata; + if (eol || info == NULL || !g_str_has_prefix(info->name, "sink-input-by")) + return; + gvc_mixer_control_stream_restore_cb (c, control->priv->new_default_sink_stream, info, control); +} + +static void +gvc_mixer_control_stream_restore_source_cb (pa_context *c, + const pa_ext_stream_restore_info *info, + int eol, + void *userdata) +{ + GvcMixerControl *control = (GvcMixerControl *) userdata; + if (eol || info == NULL || !g_str_has_prefix(info->name, "source-output-by")) + return; + gvc_mixer_control_stream_restore_cb (c, control->priv->new_default_source_stream, info, control); +} + +/** + * gvc_mixer_control_lookup_device_from_stream: + * @control: + * @stream: + * + * Returns: (transfer none): a #GvcUIDevice or %NULL + */ +GvcMixerUIDevice * +gvc_mixer_control_lookup_device_from_stream (GvcMixerControl *control, + GvcMixerStream *stream) +{ + GList *devices, *d; + gboolean is_network_stream; + const GList *ports; + GvcMixerUIDevice *ret; + + g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), NULL); + g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), NULL); + + if (GVC_IS_MIXER_SOURCE (stream)) + devices = g_hash_table_get_values (control->priv->ui_inputs); + else + devices = g_hash_table_get_values (control->priv->ui_outputs); + + ret = NULL; + ports = gvc_mixer_stream_get_ports (stream); + is_network_stream = (ports == NULL); + + for (d = devices; d != NULL; d = d->next) { + GvcMixerUIDevice *device = d->data; + guint stream_id = G_MAXUINT; + + g_object_get (G_OBJECT (device), + "stream-id", &stream_id, + NULL); + + if (is_network_stream && + stream_id == gvc_mixer_stream_get_id (stream)) { + g_debug ("lookup device from stream - %s - it is a network_stream ", + gvc_mixer_ui_device_get_description (device)); + ret = device; + break; + } else if (!is_network_stream) { + const GvcMixerStreamPort *port; + port = gvc_mixer_stream_get_port (stream); + + if (stream_id == gvc_mixer_stream_get_id (stream) && + g_strcmp0 (gvc_mixer_ui_device_get_port (device), + port->port) == 0) { + g_debug ("lookup-device-from-stream found device: device description '%s', device port = '%s', device stream id %i AND stream port = '%s' stream id '%u' and stream description '%s'", + gvc_mixer_ui_device_get_description (device), + gvc_mixer_ui_device_get_port (device), + stream_id, + port->port, + gvc_mixer_stream_get_id (stream), + gvc_mixer_stream_get_description (stream)); + ret = device; + break; + } + } + } + + g_debug ("gvc_mixer_control_lookup_device_from_stream - Could not find a device for stream '%s'",gvc_mixer_stream_get_description (stream)); + + g_list_free (devices); + + return ret; +} + +gboolean +gvc_mixer_control_set_default_sink (GvcMixerControl *control, + GvcMixerStream *stream) +{ + pa_operation *o; + + g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), FALSE); + g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), FALSE); + + g_debug ("about to set default sink on server"); + o = pa_context_set_default_sink (control->priv->pa_context, + gvc_mixer_stream_get_name (stream), + NULL, + NULL); + if (o == NULL) { + g_warning ("pa_context_set_default_sink() failed: %s", + pa_strerror (pa_context_errno (control->priv->pa_context))); + return FALSE; + } + + pa_operation_unref (o); + + control->priv->new_default_sink_stream = stream; + g_object_add_weak_pointer (G_OBJECT (stream), (gpointer *) &control->priv->new_default_sink_stream); + + o = pa_ext_stream_restore_read (control->priv->pa_context, + gvc_mixer_control_stream_restore_sink_cb, + control); + + if (o == NULL) { + g_warning ("pa_ext_stream_restore_read() failed: %s", + pa_strerror (pa_context_errno (control->priv->pa_context))); + return FALSE; + } + + pa_operation_unref (o); + + return TRUE; +} + +gboolean +gvc_mixer_control_set_default_source (GvcMixerControl *control, + GvcMixerStream *stream) +{ + GvcMixerUIDevice* input; + pa_operation *o; + + g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), FALSE); + g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), FALSE); + + o = pa_context_set_default_source (control->priv->pa_context, + gvc_mixer_stream_get_name (stream), + NULL, + NULL); + if (o == NULL) { + g_warning ("pa_context_set_default_source() failed"); + return FALSE; + } + + pa_operation_unref (o); + + control->priv->new_default_source_stream = stream; + g_object_add_weak_pointer (G_OBJECT (stream), (gpointer *) &control->priv->new_default_source_stream); + + o = pa_ext_stream_restore_read (control->priv->pa_context, + gvc_mixer_control_stream_restore_source_cb, + control); + + if (o == NULL) { + g_warning ("pa_ext_stream_restore_read() failed: %s", + pa_strerror (pa_context_errno (control->priv->pa_context))); + return FALSE; + } + + pa_operation_unref (o); + + /* source change successful, update the UI. */ + input = gvc_mixer_control_lookup_device_from_stream (control, stream); + g_signal_emit (G_OBJECT (control), + signals[ACTIVE_INPUT_UPDATE], + 0, + gvc_mixer_ui_device_get_id (input)); + + return TRUE; +} + +/** + * gvc_mixer_control_get_default_sink: + * @control: + * + * Returns: (transfer none): + */ +GvcMixerStream * +gvc_mixer_control_get_default_sink (GvcMixerControl *control) +{ + GvcMixerStream *stream; + + g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), NULL); + + if (control->priv->default_sink_is_set) { + stream = g_hash_table_lookup (control->priv->all_streams, + GUINT_TO_POINTER (control->priv->default_sink_id)); + } else { + stream = NULL; + } + + return stream; +} + +/** + * gvc_mixer_control_get_default_source: + * @control: + * + * Returns: (transfer none): + */ +GvcMixerStream * +gvc_mixer_control_get_default_source (GvcMixerControl *control) +{ + GvcMixerStream *stream; + + g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), NULL); + + if (control->priv->default_source_is_set) { + stream = g_hash_table_lookup (control->priv->all_streams, + GUINT_TO_POINTER (control->priv->default_source_id)); + } else { + stream = NULL; + } + + return stream; +} + +static gpointer +gvc_mixer_control_lookup_id (GHashTable *hash_table, + guint id) +{ + return g_hash_table_lookup (hash_table, + GUINT_TO_POINTER (id)); +} + +/** + * gvc_mixer_control_lookup_stream_id: + * @control: + * @id: + * + * Returns: (transfer none): + */ +GvcMixerStream * +gvc_mixer_control_lookup_stream_id (GvcMixerControl *control, + guint id) +{ + g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), NULL); + + return gvc_mixer_control_lookup_id (control->priv->all_streams, id); +} + +/** + * gvc_mixer_control_lookup_card_id: + * @control: + * @id: + * + * Returns: (transfer none): + */ +GvcMixerCard * +gvc_mixer_control_lookup_card_id (GvcMixerControl *control, + guint id) +{ + g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), NULL); + + return gvc_mixer_control_lookup_id (control->priv->cards, id); +} + +/** + * gvc_mixer_control_lookup_output_id: + * @control: + * @id: + * + * Returns: (transfer none): + */ +GvcMixerUIDevice * +gvc_mixer_control_lookup_output_id (GvcMixerControl *control, + guint id) +{ + g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), NULL); + + return gvc_mixer_control_lookup_id (control->priv->ui_outputs, id); +} + +/** + * gvc_mixer_control_lookup_input_id: + * @control: + * @id: + * + * Returns: (transfer none): + */ +GvcMixerUIDevice * +gvc_mixer_control_lookup_input_id (GvcMixerControl *control, + guint id) +{ + g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), NULL); + + return gvc_mixer_control_lookup_id (control->priv->ui_inputs, id); +} + +/** + * gvc_mixer_control_get_stream_from_device: + * @control: + * @device: + * + * Returns: (transfer none): + */ +GvcMixerStream * +gvc_mixer_control_get_stream_from_device (GvcMixerControl *control, + GvcMixerUIDevice *device) +{ + gint stream_id; + + g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), NULL); + g_return_val_if_fail (GVC_IS_MIXER_UI_DEVICE (device), NULL); + + stream_id = gvc_mixer_ui_device_get_stream_id (device); + + if (stream_id == GVC_MIXER_UI_DEVICE_INVALID) { + g_debug ("gvc_mixer_control_get_stream_from_device - device has a null stream"); + return NULL; + } + return gvc_mixer_control_lookup_stream_id (control, stream_id); +} + +/** + * gvc_mixer_control_change_profile_on_selected_device: + * @control: + * @device: + * @profile: (allow-none): Can be %NULL if any profile present on this port is okay + * + * Returns: This method will attempt to swap the profile on the card of + * the device with given profile name. If successfull it will set the + * preferred profile on that device so as we know the next time the user + * moves to that device it should have this profile active. + */ +gboolean +gvc_mixer_control_change_profile_on_selected_device (GvcMixerControl *control, + GvcMixerUIDevice *device, + const gchar *profile) +{ + const gchar *best_profile; + GvcMixerCardProfile *current_profile; + GvcMixerCard *card; + + g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), FALSE); + g_return_val_if_fail (GVC_IS_MIXER_UI_DEVICE (device), FALSE); + + g_object_get (G_OBJECT (device), "card", &card, NULL); + current_profile = gvc_mixer_card_get_profile (card); + + if (current_profile) + best_profile = gvc_mixer_ui_device_get_best_profile (device, profile, current_profile->profile); + else + best_profile = profile; + + g_assert (best_profile); + + g_debug ("Selected '%s', moving to profile '%s' on card '%s' on stream id %i", + profile ? profile : "(any)", best_profile, + gvc_mixer_card_get_name (card), + gvc_mixer_ui_device_get_stream_id (device)); + + g_debug ("default sink name = %s and default sink id %u", + control->priv->default_sink_name, + control->priv->default_sink_id); + + control->priv->profile_swapping_device_id = gvc_mixer_ui_device_get_id (device); + + if (gvc_mixer_card_change_profile (card, best_profile)) { + gvc_mixer_ui_device_set_user_preferred_profile (device, best_profile); + return TRUE; + } + return FALSE; +} + +/** + * gvc_mixer_control_change_output: + * @control: + * @output: + * This method is called from the UI when the user selects a previously unselected device. + * - Firstly it queries the stream from the device. + * - It assumes that if the stream is null that it cannot be a bluetooth or network stream (they never show unless they have valid sinks and sources) + * In the scenario of a NULL stream on the device + * - It fetches the device's preferred profile or if NUll the profile with the highest priority on that device. + * - It then caches this device in control->priv->cached_desired_output_id so that when the update_sink triggered + * from when we attempt to change profile we will know exactly what device to highlight on that stream. + * - It attempts to swap the profile on the card from that device and returns. + * - Next, it handles network or bluetooth streams that only require their stream to be made the default. + * - Next it deals with port changes so if the stream's active port is not the same as the port on the device + * it will attempt to change the port on that stream to be same as the device. If this fails it will return. + * - Finally it will set this new stream to be the default stream and emit a signal for the UI confirming the active output device. + */ +void +gvc_mixer_control_change_output (GvcMixerControl *control, + GvcMixerUIDevice* output) +{ + GvcMixerStream *stream; + GvcMixerStream *default_stream; + const GvcMixerStreamPort *active_port; + const gchar *output_port; + + g_return_if_fail (GVC_IS_MIXER_CONTROL (control)); + g_return_if_fail (GVC_IS_MIXER_UI_DEVICE (output)); + + g_debug ("control change output"); + + stream = gvc_mixer_control_get_stream_from_device (control, output); + if (stream == NULL) { + gvc_mixer_control_change_profile_on_selected_device (control, + output, NULL); + return; + } + + /* Handle a network sink as a portless or cardless device */ + if (!gvc_mixer_ui_device_has_ports (output)) { + g_debug ("Did we try to move to a software/bluetooth sink ?"); + if (gvc_mixer_control_set_default_sink (control, stream)) { + /* sink change was successful, update the UI.*/ + g_signal_emit (G_OBJECT (control), + signals[ACTIVE_OUTPUT_UPDATE], + 0, + gvc_mixer_ui_device_get_id (output)); + } + else { + g_warning ("Failed to set default sink with stream from output %s", + gvc_mixer_ui_device_get_description (output)); + } + return; + } + + active_port = gvc_mixer_stream_get_port (stream); + output_port = gvc_mixer_ui_device_get_port (output); + /* First ensure the correct port is active on the sink */ + if (g_strcmp0 (active_port->port, output_port) != 0) { + g_debug ("Port change, switch to = %s", output_port); + if (gvc_mixer_stream_change_port (stream, output_port) == FALSE) { + g_warning ("Could not change port !"); + return; + } + } + + default_stream = gvc_mixer_control_get_default_sink (control); + + /* Finally if we are not on the correct stream, swap over. */ + if (stream != default_stream) { + GvcMixerUIDevice* device; + + g_debug ("Attempting to swap over to stream %s ", + gvc_mixer_stream_get_description (stream)); + if (gvc_mixer_control_set_default_sink (control, stream)) { + device = gvc_mixer_control_lookup_device_from_stream (control, stream); + g_signal_emit (G_OBJECT (control), + signals[ACTIVE_OUTPUT_UPDATE], + 0, + gvc_mixer_ui_device_get_id (device)); + } else { + /* If the move failed for some reason reset the UI. */ + device = gvc_mixer_control_lookup_device_from_stream (control, default_stream); + g_signal_emit (G_OBJECT (control), + signals[ACTIVE_OUTPUT_UPDATE], + 0, + gvc_mixer_ui_device_get_id (device)); + } + } +} + + +/** + * gvc_mixer_control_change_input: + * @control: + * @input: + * This method is called from the UI when the user selects a previously unselected device. + * - Firstly it queries the stream from the device. + * - It assumes that if the stream is null that it cannot be a bluetooth or network stream (they never show unless they have valid sinks and sources) + * In the scenario of a NULL stream on the device + * - It fetches the device's preferred profile or if NUll the profile with the highest priority on that device. + * - It then caches this device in control->priv->cached_desired_input_id so that when the update_source triggered + * from when we attempt to change profile we will know exactly what device to highlight on that stream. + * - It attempts to swap the profile on the card from that device and returns. + * - Next, it handles network or bluetooth streams that only require their stream to be made the default. + * - Next it deals with port changes so if the stream's active port is not the same as the port on the device + * it will attempt to change the port on that stream to be same as the device. If this fails it will return. + * - Finally it will set this new stream to be the default stream and emit a signal for the UI confirming the active input device. + */ +void +gvc_mixer_control_change_input (GvcMixerControl *control, + GvcMixerUIDevice* input) +{ + GvcMixerStream *stream; + GvcMixerStream *default_stream; + const GvcMixerStreamPort *active_port; + const gchar *input_port; + + g_return_if_fail (GVC_IS_MIXER_CONTROL (control)); + g_return_if_fail (GVC_IS_MIXER_UI_DEVICE (input)); + + stream = gvc_mixer_control_get_stream_from_device (control, input); + if (stream == NULL) { + gvc_mixer_control_change_profile_on_selected_device (control, + input, NULL); + return; + } + + /* Handle a network sink as a portless/cardless device */ + if (!gvc_mixer_ui_device_has_ports (input)) { + g_debug ("Did we try to move to a software/bluetooth source ?"); + if (! gvc_mixer_control_set_default_source (control, stream)) { + g_warning ("Failed to set default source with stream from input %s", + gvc_mixer_ui_device_get_description (input)); + } + return; + } + + active_port = gvc_mixer_stream_get_port (stream); + input_port = gvc_mixer_ui_device_get_port (input); + /* First ensure the correct port is active on the sink */ + if (g_strcmp0 (active_port->port, input_port) != 0) { + g_debug ("Port change, switch to = %s", input_port); + if (gvc_mixer_stream_change_port (stream, input_port) == FALSE) { + g_warning ("Could not change port!"); + return; + } + } + + default_stream = gvc_mixer_control_get_default_source (control); + + /* Finally if we are not on the correct stream, swap over. */ + if (stream != default_stream) { + g_debug ("change-input - attempting to swap over to stream %s", + gvc_mixer_stream_get_description (stream)); + gvc_mixer_control_set_default_source (control, stream); + } +} + + +static void +listify_hash_values_hfunc (gpointer key, + gpointer value, + gpointer user_data) +{ + GSList **list = user_data; + + *list = g_slist_prepend (*list, value); +} + +static int +gvc_name_collate (const char *namea, + const char *nameb) +{ + if (nameb == NULL && namea == NULL) + return 0; + if (nameb == NULL) + return 1; + if (namea == NULL) + return -1; + + return g_utf8_collate (namea, nameb); +} + +static int +gvc_card_collate (GvcMixerCard *a, + GvcMixerCard *b) +{ + const char *namea; + const char *nameb; + + g_return_val_if_fail (a == NULL || GVC_IS_MIXER_CARD (a), 0); + g_return_val_if_fail (b == NULL || GVC_IS_MIXER_CARD (b), 0); + + namea = gvc_mixer_card_get_name (a); + nameb = gvc_mixer_card_get_name (b); + + return gvc_name_collate (namea, nameb); +} + +/** + * gvc_mixer_control_get_cards: + * @control: + * + * Returns: (transfer container) (element-type Gvc.MixerCard): + */ +GSList * +gvc_mixer_control_get_cards (GvcMixerControl *control) +{ + GSList *retval; + + g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), NULL); + + retval = NULL; + g_hash_table_foreach (control->priv->cards, + listify_hash_values_hfunc, + &retval); + return g_slist_sort (retval, (GCompareFunc) gvc_card_collate); +} + +static int +gvc_stream_collate (GvcMixerStream *a, + GvcMixerStream *b) +{ + const char *namea; + const char *nameb; + + g_return_val_if_fail (a == NULL || GVC_IS_MIXER_STREAM (a), 0); + g_return_val_if_fail (b == NULL || GVC_IS_MIXER_STREAM (b), 0); + + namea = gvc_mixer_stream_get_name (a); + nameb = gvc_mixer_stream_get_name (b); + + return gvc_name_collate (namea, nameb); +} + +/** + * gvc_mixer_control_get_streams: + * @control: + * + * Returns: (transfer container) (element-type Gvc.MixerStream): + */ +GSList * +gvc_mixer_control_get_streams (GvcMixerControl *control) +{ + GSList *retval; + + g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), NULL); + + retval = NULL; + g_hash_table_foreach (control->priv->all_streams, + listify_hash_values_hfunc, + &retval); + return g_slist_sort (retval, (GCompareFunc) gvc_stream_collate); +} + +/** + * gvc_mixer_control_get_sinks: + * @control: + * + * Returns: (transfer container) (element-type Gvc.MixerSink): + */ +GSList * +gvc_mixer_control_get_sinks (GvcMixerControl *control) +{ + GSList *retval; + + g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), NULL); + + retval = NULL; + g_hash_table_foreach (control->priv->sinks, + listify_hash_values_hfunc, + &retval); + return g_slist_sort (retval, (GCompareFunc) gvc_stream_collate); +} + +/** + * gvc_mixer_control_get_sources: + * @control: + * + * Returns: (transfer container) (element-type Gvc.MixerSource): + */ +GSList * +gvc_mixer_control_get_sources (GvcMixerControl *control) +{ + GSList *retval; + + g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), NULL); + + retval = NULL; + g_hash_table_foreach (control->priv->sources, + listify_hash_values_hfunc, + &retval); + return g_slist_sort (retval, (GCompareFunc) gvc_stream_collate); +} + +/** + * gvc_mixer_control_get_sink_inputs: + * @control: + * + * Returns: (transfer container) (element-type Gvc.MixerSinkInput): + */ +GSList * +gvc_mixer_control_get_sink_inputs (GvcMixerControl *control) +{ + GSList *retval; + + g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), NULL); + + retval = NULL; + g_hash_table_foreach (control->priv->sink_inputs, + listify_hash_values_hfunc, + &retval); + return g_slist_sort (retval, (GCompareFunc) gvc_stream_collate); +} + +/** + * gvc_mixer_control_get_source_outputs: + * @control: + * + * Returns: (transfer container) (element-type Gvc.MixerSourceOutput): + */ +GSList * +gvc_mixer_control_get_source_outputs (GvcMixerControl *control) +{ + GSList *retval; + + g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), NULL); + + retval = NULL; + g_hash_table_foreach (control->priv->source_outputs, + listify_hash_values_hfunc, + &retval); + return g_slist_sort (retval, (GCompareFunc) gvc_stream_collate); +} + +static void +dec_outstanding (GvcMixerControl *control) +{ + if (control->priv->n_outstanding <= 0) { + return; + } + + if (--control->priv->n_outstanding <= 0) { + control->priv->state = GVC_STATE_READY; + g_signal_emit (G_OBJECT (control), signals[STATE_CHANGED], 0, GVC_STATE_READY); + } +} + +GvcMixerControlState +gvc_mixer_control_get_state (GvcMixerControl *control) +{ + g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), GVC_STATE_CLOSED); + + return control->priv->state; +} + +static void +on_default_source_port_notify (GObject *object, + GParamSpec *pspec, + GvcMixerControl *control) +{ + char *port; + GvcMixerUIDevice *input; + + g_object_get (object, "port", &port, NULL); + input = gvc_mixer_control_lookup_device_from_stream (control, + GVC_MIXER_STREAM (object)); + + g_debug ("on_default_source_port_notify - moved to port '%s' which SHOULD ?? correspond to output '%s'", + port, + gvc_mixer_ui_device_get_description (input)); + + g_signal_emit (G_OBJECT (control), + signals[ACTIVE_INPUT_UPDATE], + 0, + gvc_mixer_ui_device_get_id (input)); + + g_free (port); +} + + +static void +_set_default_source (GvcMixerControl *control, + GvcMixerStream *stream) +{ + guint new_id; + + if (stream == NULL) { + control->priv->default_source_id = 0; + control->priv->default_source_is_set = FALSE; + g_signal_emit (control, + signals[DEFAULT_SOURCE_CHANGED], + 0, + PA_INVALID_INDEX); + return; + } + + new_id = gvc_mixer_stream_get_id (stream); + + if (control->priv->default_source_id != new_id) { + GvcMixerUIDevice *input; + control->priv->default_source_id = new_id; + control->priv->default_source_is_set = TRUE; + g_signal_emit (control, + signals[DEFAULT_SOURCE_CHANGED], + 0, + new_id); + + if (control->priv->default_source_is_set) { + g_signal_handlers_disconnect_by_func (gvc_mixer_control_get_default_source (control), + on_default_source_port_notify, + control); + } + + g_signal_connect (stream, + "notify::port", + G_CALLBACK (on_default_source_port_notify), + control); + + input = gvc_mixer_control_lookup_device_from_stream (control, stream); + + g_signal_emit (G_OBJECT (control), + signals[ACTIVE_INPUT_UPDATE], + 0, + gvc_mixer_ui_device_get_id (input)); + } +} + +static void +on_default_sink_port_notify (GObject *object, + GParamSpec *pspec, + GvcMixerControl *control) +{ + char *port; + GvcMixerUIDevice *output; + + g_object_get (object, "port", &port, NULL); + + output = gvc_mixer_control_lookup_device_from_stream (control, + GVC_MIXER_STREAM (object)); + if (output != NULL) { + g_debug ("on_default_sink_port_notify - moved to port %s - which SHOULD correspond to output %s", + port, + gvc_mixer_ui_device_get_description (output)); + g_signal_emit (G_OBJECT (control), + signals[ACTIVE_OUTPUT_UPDATE], + 0, + gvc_mixer_ui_device_get_id (output)); + } + g_free (port); +} + +static void +_set_default_sink (GvcMixerControl *control, + GvcMixerStream *stream) +{ + guint new_id; + + if (stream == NULL) { + /* Don't tell front-ends about an unset default + * sink if it's already unset */ + if (control->priv->default_sink_is_set == FALSE) + return; + control->priv->default_sink_id = 0; + control->priv->default_sink_is_set = FALSE; + g_signal_emit (control, + signals[DEFAULT_SINK_CHANGED], + 0, + PA_INVALID_INDEX); + return; + } + + new_id = gvc_mixer_stream_get_id (stream); + + if (control->priv->default_sink_id != new_id) { + GvcMixerUIDevice *output; + if (control->priv->default_sink_is_set) { + g_signal_handlers_disconnect_by_func (gvc_mixer_control_get_default_sink (control), + on_default_sink_port_notify, + control); + } + + control->priv->default_sink_id = new_id; + + control->priv->default_sink_is_set = TRUE; + g_signal_emit (control, + signals[DEFAULT_SINK_CHANGED], + 0, + new_id); + + g_signal_connect (stream, + "notify::port", + G_CALLBACK (on_default_sink_port_notify), + control); + + output = gvc_mixer_control_lookup_device_from_stream (control, stream); + + g_debug ("active_sink change"); + + g_signal_emit (G_OBJECT (control), + signals[ACTIVE_OUTPUT_UPDATE], + 0, + gvc_mixer_ui_device_get_id (output)); + } +} + +static gboolean +_stream_has_name (gpointer key, + GvcMixerStream *stream, + const char *name) +{ + const char *t_name; + + t_name = gvc_mixer_stream_get_name (stream); + + if (t_name != NULL + && name != NULL + && strcmp (t_name, name) == 0) { + return TRUE; + } + + return FALSE; +} + +static GvcMixerStream * +find_stream_for_name (GvcMixerControl *control, + const char *name) +{ + GvcMixerStream *stream; + + stream = g_hash_table_find (control->priv->all_streams, + (GHRFunc)_stream_has_name, + (char *)name); + return stream; +} + +static void +update_default_source_from_name (GvcMixerControl *control, + const char *name) +{ + gboolean changed = FALSE; + + if ((control->priv->default_source_name == NULL + && name != NULL) + || (control->priv->default_source_name != NULL + && name == NULL) + || (name != NULL && strcmp (control->priv->default_source_name, name) != 0)) { + changed = TRUE; + } + + if (changed) { + GvcMixerStream *stream; + + g_free (control->priv->default_source_name); + control->priv->default_source_name = g_strdup (name); + + stream = find_stream_for_name (control, name); + _set_default_source (control, stream); + } +} + +static void +update_default_sink_from_name (GvcMixerControl *control, + const char *name) +{ + gboolean changed = FALSE; + + if ((control->priv->default_sink_name == NULL + && name != NULL) + || (control->priv->default_sink_name != NULL + && name == NULL) + || (name != NULL && strcmp (control->priv->default_sink_name, name) != 0)) { + changed = TRUE; + } + + if (changed) { + GvcMixerStream *stream; + g_free (control->priv->default_sink_name); + control->priv->default_sink_name = g_strdup (name); + + stream = find_stream_for_name (control, name); + _set_default_sink (control, stream); + } +} + +static void +update_server (GvcMixerControl *control, + const pa_server_info *info) +{ + if (info->default_source_name != NULL) { + update_default_source_from_name (control, info->default_source_name); + } + if (info->default_sink_name != NULL) { + g_debug ("update server"); + update_default_sink_from_name (control, info->default_sink_name); + } +} + +static void +remove_stream (GvcMixerControl *control, + GvcMixerStream *stream) +{ + guint id; + + g_object_ref (stream); + + id = gvc_mixer_stream_get_id (stream); + + if (id == control->priv->default_sink_id) { + _set_default_sink (control, NULL); + } else if (id == control->priv->default_source_id) { + _set_default_source (control, NULL); + } + + g_hash_table_remove (control->priv->all_streams, + GUINT_TO_POINTER (id)); + g_signal_emit (G_OBJECT (control), + signals[STREAM_REMOVED], + 0, + gvc_mixer_stream_get_id (stream)); + g_object_unref (stream); +} + +static void +add_stream (GvcMixerControl *control, + GvcMixerStream *stream) +{ + g_hash_table_insert (control->priv->all_streams, + GUINT_TO_POINTER (gvc_mixer_stream_get_id (stream)), + stream); + g_signal_emit (G_OBJECT (control), + signals[STREAM_ADDED], + 0, + gvc_mixer_stream_get_id (stream)); +} + +/* This method will match individual stream ports against its corresponding device + * It does this by: + * - iterates through our devices and finds the one where the card-id on the device is the same as the card-id on the stream + * and the port-name on the device is the same as the streamport-name. + * This should always find a match and is used exclusively by sync_devices(). + */ +static gboolean +match_stream_with_devices (GvcMixerControl *control, + GvcMixerStreamPort *stream_port, + GvcMixerStream *stream) +{ + GList *devices, *d; + guint stream_card_id; + guint stream_id; + gboolean in_possession = FALSE; + + stream_id = gvc_mixer_stream_get_id (stream); + stream_card_id = gvc_mixer_stream_get_card_index (stream); + + devices = g_hash_table_get_values (GVC_IS_MIXER_SOURCE (stream) ? control->priv->ui_inputs : control->priv->ui_outputs); + + for (d = devices; d != NULL; d = d->next) { + GvcMixerUIDevice *device; + guint device_stream_id; + gchar *device_port_name; + gchar *origin; + gchar *description; + GvcMixerCard *card; + guint card_id; + + device = d->data; + g_object_get (G_OBJECT (device), + "stream-id", &device_stream_id, + "card", &card, + "origin", &origin, + "description", &description, + "port-name", &device_port_name, + NULL); + + if (card == NULL) { + if (device_stream_id == stream_id) { + g_debug ("Matched stream %u with card-less device '%s', with stream already setup", + stream_id, description); + in_possession = TRUE; + } + } else { + card_id = gvc_mixer_card_get_index (card); + + g_debug ("Attempt to match_stream update_with_existing_outputs - Try description : '%s', origin : '%s', device port name : '%s', card : %p, AGAINST stream port: '%s', sink card id %i", + description, + origin, + device_port_name, + card, + stream_port->port, + stream_card_id); + + if (stream_card_id == card_id && + g_strcmp0 (device_port_name, stream_port->port) == 0) { + g_debug ("Match device with stream: We have a match with description: '%s', origin: '%s', cached already with device id %u, so set stream id to %i", + description, + origin, + gvc_mixer_ui_device_get_id (device), + stream_id); + + g_object_set (G_OBJECT (device), + "stream-id", stream_id, + NULL); + in_possession = TRUE; + } + } + + g_free (device_port_name); + g_free (origin); + g_free (description); + + if (in_possession == TRUE) + break; + } + + g_list_free (devices); + return in_possession; +} + +/* + * This method attempts to match a sink or source with its relevant UI device. + * GvcMixerStream can represent both a sink or source. + * Using static card port introspection implies that we know beforehand what + * outputs and inputs are available to the user. + * But that does not mean that all of these inputs and outputs are available to be used. + * For instance we might be able to see that there is a HDMI port available but if + * we are on the default analog stereo output profile there is no valid sink for + * that HDMI device. We first need to change profile and when update_sink() is called + * only then can we match the new hdmi sink with its corresponding device. + * + * Firstly it checks to see if the incoming stream has no ports. + * - If a stream has no ports but has a valid card ID (bluetooth), it will attempt + * to match the device with the stream using the card id. + * - If a stream has no ports and no valid card id, it goes ahead and makes a new + * device (software/network devices are only detectable at the sink/source level) + * If the stream has ports it will match each port against the stream using match_stream_with_devices(). + * + * This method should always find a match. + */ +static void +sync_devices (GvcMixerControl *control, + GvcMixerStream* stream) +{ + /* Go through ports to see what outputs can be created. */ + const GList *stream_ports; + const GList *n = NULL; + gboolean is_output = !GVC_IS_MIXER_SOURCE (stream); + + stream_ports = gvc_mixer_stream_get_ports (stream); + + if (stream_ports == NULL) { + GvcMixerUIDevice *device; + /* Bluetooth, no ports but a valid card */ + if (gvc_mixer_stream_get_card_index (stream) != PA_INVALID_INDEX) { + GList *devices, *d; + gboolean in_possession = FALSE; + + devices = g_hash_table_get_values (is_output ? control->priv->ui_outputs : control->priv->ui_inputs); + + for (d = devices; d != NULL; d = d->next) { + GvcMixerCard *card; + guint card_id; + + device = d->data; + + g_object_get (G_OBJECT (device), + "card", &card, + NULL); + card_id = gvc_mixer_card_get_index (card); + g_debug ("sync devices, device description - '%s', device card id - %i, stream description - %s, stream card id - %i", + gvc_mixer_ui_device_get_description (device), + card_id, + gvc_mixer_stream_get_description (stream), + gvc_mixer_stream_get_card_index (stream)); + if (card_id == gvc_mixer_stream_get_card_index (stream)) { + in_possession = TRUE; + break; + } + } + g_list_free (devices); + + if (!in_possession) { + g_warning ("Couldn't match the portless stream (with card) - '%s' is it an input ? -> %i, streams card id -> %i", + gvc_mixer_stream_get_description (stream), + GVC_IS_MIXER_SOURCE (stream), + gvc_mixer_stream_get_card_index (stream)); + return; + } + + g_object_set (G_OBJECT (device), + "stream-id", gvc_mixer_stream_get_id (stream), + "description", gvc_mixer_stream_get_description (stream), + "origin", "", /*Leave it empty for these special cases*/ + "port-name", NULL, + "port-available", TRUE, + NULL); + } else { /* Network sink/source has no ports and no card. */ + GObject *object; + + object = g_object_new (GVC_TYPE_MIXER_UI_DEVICE, + "stream-id", gvc_mixer_stream_get_id (stream), + "description", gvc_mixer_stream_get_description (stream), + "origin", "", /* Leave it empty for these special cases */ + "port-name", NULL, + "port-available", TRUE, + NULL); + device = GVC_MIXER_UI_DEVICE (object); + + g_hash_table_insert (is_output ? control->priv->ui_outputs : control->priv->ui_inputs, + GUINT_TO_POINTER (gvc_mixer_ui_device_get_id (device)), + g_object_ref (device)); + + } + g_signal_emit (G_OBJECT (control), + signals[is_output ? OUTPUT_ADDED : INPUT_ADDED], + 0, + gvc_mixer_ui_device_get_id (device)); + + return; + } + + /* Go ahead and make sure to match each port against a previously created device */ + for (n = stream_ports; n != NULL; n = n->next) { + + GvcMixerStreamPort *stream_port; + stream_port = n->data; + + if (match_stream_with_devices (control, stream_port, stream)) + continue; + + g_warning ("Sync_devices: Failed to match stream id: %u, description: '%s', origin: '%s'", + gvc_mixer_stream_get_id (stream), + stream_port->human_port, + gvc_mixer_stream_get_description (stream)); + } +} + +static void +set_icon_name_from_proplist (GvcMixerStream *stream, + pa_proplist *l, + const char *default_icon_name) +{ + const char *t; + + if ((t = pa_proplist_gets (l, PA_PROP_DEVICE_ICON_NAME))) { + goto finish; + } + + if ((t = pa_proplist_gets (l, PA_PROP_MEDIA_ICON_NAME))) { + goto finish; + } + + if ((t = pa_proplist_gets (l, PA_PROP_WINDOW_ICON_NAME))) { + goto finish; + } + + if ((t = pa_proplist_gets (l, PA_PROP_APPLICATION_ICON_NAME))) { + goto finish; + } + + if ((t = pa_proplist_gets (l, PA_PROP_MEDIA_ROLE))) { + + if (strcmp (t, "video") == 0 || + strcmp (t, "phone") == 0) { + goto finish; + } + + if (strcmp (t, "music") == 0) { + t = "audio"; + goto finish; + } + + if (strcmp (t, "game") == 0) { + t = "applications-games"; + goto finish; + } + + if (strcmp (t, "event") == 0) { + t = "dialog-information"; + goto finish; + } + } + + t = default_icon_name; + + finish: + gvc_mixer_stream_set_icon_name (stream, t); +} + +static GvcMixerStreamState +translate_pa_state (pa_sink_state_t state) { + switch (state) { + case PA_SINK_RUNNING: + return GVC_STREAM_STATE_RUNNING; + case PA_SINK_IDLE: + return GVC_STREAM_STATE_IDLE; + case PA_SINK_SUSPENDED: + return GVC_STREAM_STATE_SUSPENDED; + case PA_SINK_INIT: + case PA_SINK_INVALID_STATE: + case PA_SINK_UNLINKED: + default: + return GVC_STREAM_STATE_INVALID; + } +} + +/* + * Called when anything changes with a sink. + */ +static void +update_sink (GvcMixerControl *control, + const pa_sink_info *info) +{ + GvcMixerStream *stream; + gboolean is_new; + pa_volume_t max_volume; + GvcChannelMap *map; + char map_buff[PA_CHANNEL_MAP_SNPRINT_MAX]; + + pa_channel_map_snprint (map_buff, PA_CHANNEL_MAP_SNPRINT_MAX, &info->channel_map); +#if 1 + g_debug ("Updating sink: index=%u name='%s' description='%s' map='%s'", + info->index, + info->name, + info->description, + map_buff); +#endif + + map = NULL; + is_new = FALSE; + stream = g_hash_table_lookup (control->priv->sinks, + GUINT_TO_POINTER (info->index)); + + if (stream == NULL) { + GList *list = NULL; + guint i; + + map = gvc_channel_map_new_from_pa_channel_map (&info->channel_map); + stream = gvc_mixer_sink_new (control->priv->pa_context, + info->index, + map); + + for (i = 0; i < info->n_ports; i++) { + GvcMixerStreamPort *port; + + port = g_slice_new0 (GvcMixerStreamPort); + port->port = g_strdup (info->ports[i]->name); + port->human_port = g_strdup (info->ports[i]->description); + port->priority = info->ports[i]->priority; + port->available = info->ports[i]->available != PA_PORT_AVAILABLE_NO; + + list = g_list_prepend (list, port); + } + gvc_mixer_stream_set_ports (stream, list); + + g_object_unref (map); + is_new = TRUE; + + } else if (gvc_mixer_stream_is_running (stream)) { + /* Ignore events if volume changes are outstanding */ + g_debug ("Ignoring event, volume changes are outstanding"); + return; + } + + max_volume = pa_cvolume_max (&info->volume); + gvc_mixer_stream_set_name (stream, info->name); + gvc_mixer_stream_set_card_index (stream, info->card); + gvc_mixer_stream_set_description (stream, info->description); + set_icon_name_from_proplist (stream, info->proplist, "audio-card"); + gvc_mixer_stream_set_form_factor (stream, pa_proplist_gets (info->proplist, PA_PROP_DEVICE_FORM_FACTOR)); + gvc_mixer_stream_set_sysfs_path (stream, pa_proplist_gets (info->proplist, "sysfs.path")); + gvc_mixer_stream_set_volume (stream, (guint)max_volume); + gvc_mixer_stream_set_is_muted (stream, info->mute); + gvc_mixer_stream_set_can_decibel (stream, !!(info->flags & PA_SINK_DECIBEL_VOLUME)); + gvc_mixer_stream_set_base_volume (stream, (guint32) info->base_volume); + gvc_mixer_stream_set_state (stream, translate_pa_state (info->state)); + + /* Messy I know but to set the port everytime regardless of whether it has changed will cost us a + * port change notify signal which causes the frontend to resync. + * Only update the UI when something has changed. */ + if (info->active_port != NULL) { + if (is_new) + gvc_mixer_stream_set_port (stream, info->active_port->name); + else { + const GvcMixerStreamPort *active_port; + active_port = gvc_mixer_stream_get_port (stream); + if (active_port == NULL || + g_strcmp0 (active_port->port, info->active_port->name) != 0) { + g_debug ("update sink - apparently a port update"); + gvc_mixer_stream_set_port (stream, info->active_port->name); + } + } + } + + if (is_new) { + g_debug ("update sink - is new"); + + g_hash_table_insert (control->priv->sinks, + GUINT_TO_POINTER (info->index), + g_object_ref (stream)); + add_stream (control, stream); + /* Always sink on a new stream to able to assign the right stream id + * to the appropriate outputs (multiple potential outputs per stream). */ + sync_devices (control, stream); + } else { + g_signal_emit (G_OBJECT (control), + signals[STREAM_CHANGED], + 0, + gvc_mixer_stream_get_id (stream)); + } + + /* + * When we change profile on a device that is not the server default sink, + * it will jump back to the default sink set by the server to prevent the audio setup from being 'outputless'. + * All well and good but then when we get the new stream created for the new profile how do we know + * that this is the intended default or selected device the user wishes to use. + * This is messy but it's the only reliable way that it can be done without ripping the whole thing apart. + */ + if (control->priv->profile_swapping_device_id != GVC_MIXER_UI_DEVICE_INVALID) { + GvcMixerUIDevice *dev = NULL; + dev = gvc_mixer_control_lookup_output_id (control, control->priv->profile_swapping_device_id); + if (dev != NULL) { + /* now check to make sure this new stream is the same stream just matched and set on the device object */ + if (gvc_mixer_ui_device_get_stream_id (dev) == gvc_mixer_stream_get_id (stream)) { + g_debug ("Looks like we profile swapped on a non server default sink"); + gvc_mixer_control_set_default_sink (control, stream); + control->priv->profile_swapping_device_id = GVC_MIXER_UI_DEVICE_INVALID; + } + } + } + + if (control->priv->default_sink_name != NULL + && info->name != NULL + && strcmp (control->priv->default_sink_name, info->name) == 0) { + _set_default_sink (control, stream); + } + + if (map == NULL) + map = (GvcChannelMap *) gvc_mixer_stream_get_channel_map (stream); + + gvc_channel_map_volume_changed (map, &info->volume, FALSE); +} + +static void +update_source (GvcMixerControl *control, + const pa_source_info *info) +{ + GvcMixerStream *stream; + gboolean is_new; + pa_volume_t max_volume; + +#if 1 + g_debug ("Updating source: index=%u name='%s' description='%s'", + info->index, + info->name, + info->description); +#endif + + /* completely ignore monitors, they're not real sources */ + if (info->monitor_of_sink != PA_INVALID_INDEX) { + return; + } + + is_new = FALSE; + + stream = g_hash_table_lookup (control->priv->sources, + GUINT_TO_POINTER (info->index)); + if (stream == NULL) { + GList *list = NULL; + guint i; + GvcChannelMap *map; + + map = gvc_channel_map_new_from_pa_channel_map (&info->channel_map); + stream = gvc_mixer_source_new (control->priv->pa_context, + info->index, + map); + + for (i = 0; i < info->n_ports; i++) { + GvcMixerStreamPort *port; + + port = g_slice_new0 (GvcMixerStreamPort); + port->port = g_strdup (info->ports[i]->name); + port->human_port = g_strdup (info->ports[i]->description); + port->priority = info->ports[i]->priority; + list = g_list_prepend (list, port); + } + gvc_mixer_stream_set_ports (stream, list); + + g_object_unref (map); + is_new = TRUE; + } else if (gvc_mixer_stream_is_running (stream)) { + /* Ignore events if volume changes are outstanding */ + g_debug ("Ignoring event, volume changes are outstanding"); + return; + } + + max_volume = pa_cvolume_max (&info->volume); + + gvc_mixer_stream_set_name (stream, info->name); + gvc_mixer_stream_set_card_index (stream, info->card); + gvc_mixer_stream_set_description (stream, info->description); + set_icon_name_from_proplist (stream, info->proplist, "audio-input-microphone"); + gvc_mixer_stream_set_form_factor (stream, pa_proplist_gets (info->proplist, PA_PROP_DEVICE_FORM_FACTOR)); + gvc_mixer_stream_set_volume (stream, (guint)max_volume); + gvc_mixer_stream_set_is_muted (stream, info->mute); + gvc_mixer_stream_set_can_decibel (stream, !!(info->flags & PA_SOURCE_DECIBEL_VOLUME)); + gvc_mixer_stream_set_base_volume (stream, (guint32) info->base_volume); + g_debug ("update source"); + + if (info->active_port != NULL) { + if (is_new) + gvc_mixer_stream_set_port (stream, info->active_port->name); + else { + const GvcMixerStreamPort *active_port; + active_port = gvc_mixer_stream_get_port (stream); + if (active_port == NULL || + g_strcmp0 (active_port->port, info->active_port->name) != 0) { + g_debug ("update source - apparently a port update"); + gvc_mixer_stream_set_port (stream, info->active_port->name); + } + } + } + + if (is_new) { + g_hash_table_insert (control->priv->sources, + GUINT_TO_POINTER (info->index), + g_object_ref (stream)); + add_stream (control, stream); + sync_devices (control, stream); + } else { + g_signal_emit (G_OBJECT (control), + signals[STREAM_CHANGED], + 0, + gvc_mixer_stream_get_id (stream)); + } + + if (control->priv->profile_swapping_device_id != GVC_MIXER_UI_DEVICE_INVALID) { + GvcMixerUIDevice *dev = NULL; + + dev = gvc_mixer_control_lookup_input_id (control, control->priv->profile_swapping_device_id); + + if (dev != NULL) { + /* now check to make sure this new stream is the same stream just matched and set on the device object */ + if (gvc_mixer_ui_device_get_stream_id (dev) == gvc_mixer_stream_get_id (stream)) { + g_debug ("Looks like we profile swapped on a non server default source"); + gvc_mixer_control_set_default_source (control, stream); + control->priv->profile_swapping_device_id = GVC_MIXER_UI_DEVICE_INVALID; + } + } + } + if (control->priv->default_source_name != NULL + && info->name != NULL + && strcmp (control->priv->default_source_name, info->name) == 0) { + _set_default_source (control, stream); + } +} + +static void +set_is_event_stream_from_proplist (GvcMixerStream *stream, + pa_proplist *l) +{ + const char *t; + gboolean is_event_stream; + + is_event_stream = FALSE; + + if ((t = pa_proplist_gets (l, PA_PROP_MEDIA_ROLE))) { + if (g_str_equal (t, "event")) + is_event_stream = TRUE; + } + + gvc_mixer_stream_set_is_event_stream (stream, is_event_stream); +} + +static void +set_application_id_from_proplist (GvcMixerStream *stream, + pa_proplist *l) +{ + const char *t; + + if ((t = pa_proplist_gets (l, PA_PROP_APPLICATION_ID))) { + gvc_mixer_stream_set_application_id (stream, t); + } +} + +static void +update_sink_input (GvcMixerControl *control, + const pa_sink_input_info *info) +{ + GvcMixerStream *stream; + gboolean is_new; + pa_volume_t max_volume; + const char *name; + +#if 0 + g_debug ("Updating sink input: index=%u name='%s' client=%u sink=%u", + info->index, + info->name, + info->client, + info->sink); +#endif + + is_new = FALSE; + + stream = g_hash_table_lookup (control->priv->sink_inputs, + GUINT_TO_POINTER (info->index)); + if (stream == NULL) { + GvcChannelMap *map; + map = gvc_channel_map_new_from_pa_channel_map (&info->channel_map); + stream = gvc_mixer_sink_input_new (control->priv->pa_context, + info->index, + map); + g_object_unref (map); + is_new = TRUE; + } else if (gvc_mixer_stream_is_running (stream)) { + /* Ignore events if volume changes are outstanding */ + g_debug ("Ignoring event, volume changes are outstanding"); + return; + } + + max_volume = pa_cvolume_max (&info->volume); + + name = (const char *)g_hash_table_lookup (control->priv->clients, + GUINT_TO_POINTER (info->client)); + gvc_mixer_stream_set_name (stream, name); + gvc_mixer_stream_set_description (stream, info->name); + + set_application_id_from_proplist (stream, info->proplist); + set_is_event_stream_from_proplist (stream, info->proplist); + set_icon_name_from_proplist (stream, info->proplist, "application-x-executable"); + gvc_mixer_stream_set_volume (stream, (guint)max_volume); + gvc_mixer_stream_set_is_muted (stream, info->mute); + gvc_mixer_stream_set_is_virtual (stream, info->client == PA_INVALID_INDEX); + + if (is_new) { + g_hash_table_insert (control->priv->sink_inputs, + GUINT_TO_POINTER (info->index), + g_object_ref (stream)); + add_stream (control, stream); + } else { + g_signal_emit (G_OBJECT (control), + signals[STREAM_CHANGED], + 0, + gvc_mixer_stream_get_id (stream)); + } +} + +static void +update_source_output (GvcMixerControl *control, + const pa_source_output_info *info) +{ + GvcMixerStream *stream; + gboolean is_new; + pa_volume_t max_volume; + const char *name; + +#if 1 + g_debug ("Updating source output: index=%u name='%s' client=%u source=%u", + info->index, + info->name, + info->client, + info->source); +#endif + + is_new = FALSE; + stream = g_hash_table_lookup (control->priv->source_outputs, + GUINT_TO_POINTER (info->index)); + if (stream == NULL) { + GvcChannelMap *map; + map = gvc_channel_map_new_from_pa_channel_map (&info->channel_map); + stream = gvc_mixer_source_output_new (control->priv->pa_context, + info->index, + map); + g_object_unref (map); + is_new = TRUE; + } + + name = (const char *)g_hash_table_lookup (control->priv->clients, + GUINT_TO_POINTER (info->client)); + + max_volume = pa_cvolume_max (&info->volume); + + gvc_mixer_stream_set_name (stream, name); + gvc_mixer_stream_set_description (stream, info->name); + set_application_id_from_proplist (stream, info->proplist); + set_is_event_stream_from_proplist (stream, info->proplist); + gvc_mixer_stream_set_volume (stream, (guint)max_volume); + gvc_mixer_stream_set_is_muted (stream, info->mute); + set_icon_name_from_proplist (stream, info->proplist, "audio-input-microphone"); + + if (is_new) { + g_hash_table_insert (control->priv->source_outputs, + GUINT_TO_POINTER (info->index), + g_object_ref (stream)); + add_stream (control, stream); + } else { + g_signal_emit (G_OBJECT (control), + signals[STREAM_CHANGED], + 0, + gvc_mixer_stream_get_id (stream)); + } +} + +static void +update_client (GvcMixerControl *control, + const pa_client_info *info) +{ +#if 1 + g_debug ("Updating client: index=%u name='%s'", + info->index, + info->name); +#endif + g_hash_table_insert (control->priv->clients, + GUINT_TO_POINTER (info->index), + g_strdup (info->name)); +} + +static char * +card_num_streams_to_status (guint sinks, + guint sources) +{ + char *sinks_str; + char *sources_str; + char *ret; + + if (sinks == 0 && sources == 0) { + /* translators: + * The device has been disabled */ + return g_strdup (_("Disabled")); + } + if (sinks == 0) { + sinks_str = NULL; + } else { + /* translators: + * The number of sound outputs on a particular device */ + sinks_str = g_strdup_printf (ngettext ("%u Output", + "%u Outputs", + sinks), + sinks); + } + if (sources == 0) { + sources_str = NULL; + } else { + /* translators: + * The number of sound inputs on a particular device */ + sources_str = g_strdup_printf (ngettext ("%u Input", + "%u Inputs", + sources), + sources); + } + if (sources_str == NULL) + return sinks_str; + if (sinks_str == NULL) + return sources_str; + ret = g_strdup_printf ("%s / %s", sinks_str, sources_str); + g_free (sinks_str); + g_free (sources_str); + return ret; +} + +/* + * A utility method to gather which card profiles are relevant to the port . + */ +static GList * +determine_profiles_for_port (pa_card_port_info *port, + GList* card_profiles) +{ + guint i; + GList *supported_profiles = NULL; + GList *p; + for (i = 0; i < port->n_profiles; i++) { + for (p = card_profiles; p != NULL; p = p->next) { + GvcMixerCardProfile *prof; + prof = p->data; + if (g_strcmp0 (port->profiles[i]->name, prof->profile) == 0) + supported_profiles = g_list_append (supported_profiles, prof); + } + } + g_debug ("%i profiles supported on port %s", + g_list_length (supported_profiles), + port->description); + return g_list_sort (supported_profiles, (GCompareFunc) gvc_mixer_card_profile_compare); +} + +static gboolean +is_card_port_an_output (GvcMixerCardPort* port) +{ + return port->direction == PA_DIRECTION_OUTPUT ? TRUE : FALSE; +} + +/* + * This method will create a ui device for the given port. + */ +static void +create_ui_device_from_port (GvcMixerControl* control, + GvcMixerCardPort* port, + GvcMixerCard* card) +{ + GvcMixerUIDeviceDirection direction; + GObject *object; + GvcMixerUIDevice *uidevice; + gboolean available = port->available != PA_PORT_AVAILABLE_NO; + + direction = (is_card_port_an_output (port) == TRUE) ? UIDeviceOutput : UIDeviceInput; + + object = g_object_new (GVC_TYPE_MIXER_UI_DEVICE, + "type", (guint)direction, + "card", card, + "port-name", port->port, + "description", port->human_port, + "origin", gvc_mixer_card_get_name (card), + "port-available", available, + "icon-name", port->icon_name, + NULL); + + uidevice = GVC_MIXER_UI_DEVICE (object); + gvc_mixer_ui_device_set_profiles (uidevice, port->profiles); + + g_hash_table_insert (is_card_port_an_output (port) ? control->priv->ui_outputs : control->priv->ui_inputs, + GUINT_TO_POINTER (gvc_mixer_ui_device_get_id (uidevice)), + uidevice); + + + if (available) { + g_signal_emit (G_OBJECT (control), + signals[is_card_port_an_output (port) ? OUTPUT_ADDED : INPUT_ADDED], + 0, + gvc_mixer_ui_device_get_id (uidevice)); + } + + g_debug ("create_ui_device_from_port, direction %u, description '%s', origin '%s', port available %i", + direction, + port->human_port, + gvc_mixer_card_get_name (card), + available); +} + +/* + * This method will match up GvcMixerCardPorts with existing devices. + * A match is achieved if the device's card-id and the port's card-id are the same + * && the device's port-name and the card-port's port member are the same. + * A signal is then sent adding or removing that device from the UI depending on the availability of the port. + */ +static void +match_card_port_with_existing_device (GvcMixerControl *control, + GvcMixerCardPort *card_port, + GvcMixerCard *card, + gboolean available) +{ + GList *d; + GList *devices; + GvcMixerUIDevice *device; + gboolean is_output = is_card_port_an_output (card_port); + + devices = g_hash_table_get_values (is_output ? control->priv->ui_outputs : control->priv->ui_inputs); + + for (d = devices; d != NULL; d = d->next) { + GvcMixerCard *device_card; + gchar *device_port_name; + + device = d->data; + g_object_get (G_OBJECT (device), + "card", &device_card, + "port-name", &device_port_name, + NULL); + + if (g_strcmp0 (card_port->port, device_port_name) == 0 && + device_card == card) { + g_debug ("Found the relevant device %s, update its port availability flag to %i, is_output %i", + device_port_name, + available, + is_output); + g_object_set (G_OBJECT (device), + "port-available", available, NULL); + g_signal_emit (G_OBJECT (control), + is_output ? signals[available ? OUTPUT_ADDED : OUTPUT_REMOVED] : signals[available ? INPUT_ADDED : INPUT_REMOVED], + 0, + gvc_mixer_ui_device_get_id (device)); + } + g_free (device_port_name); + } + + g_list_free (devices); +} + +static void +create_ui_device_from_card (GvcMixerControl *control, + GvcMixerCard *card) +{ + GObject *object; + GvcMixerUIDevice *in; + GvcMixerUIDevice *out; + const GList *profiles; + + /* For now just create two devices and presume this device is multi directional + * Ensure to remove both on card removal (available to false by default) */ + profiles = gvc_mixer_card_get_profiles (card); + + g_debug ("Portless card just registered - %i", gvc_mixer_card_get_index (card)); + + object = g_object_new (GVC_TYPE_MIXER_UI_DEVICE, + "type", UIDeviceInput, + "description", gvc_mixer_card_get_name (card), + "origin", "", /* Leave it empty for these special cases */ + "port-name", NULL, + "port-available", FALSE, + "card", card, + NULL); + in = GVC_MIXER_UI_DEVICE (object); + gvc_mixer_ui_device_set_profiles (in, profiles); + + g_hash_table_insert (control->priv->ui_inputs, + GUINT_TO_POINTER (gvc_mixer_ui_device_get_id (in)), + g_object_ref (in)); + object = g_object_new (GVC_TYPE_MIXER_UI_DEVICE, + "type", UIDeviceOutput, + "description", gvc_mixer_card_get_name (card), + "origin", "", /* Leave it empty for these special cases */ + "port-name", NULL, + "port-available", FALSE, + "card", card, + NULL); + out = GVC_MIXER_UI_DEVICE (object); + gvc_mixer_ui_device_set_profiles (out, profiles); + + g_hash_table_insert (control->priv->ui_outputs, + GUINT_TO_POINTER (gvc_mixer_ui_device_get_id (out)), + g_object_ref (out)); +} + +#ifdef HAVE_ALSA +typedef struct { + char *port_name_to_set; + guint32 headset_card; +} PortStatusData; + +static void +port_status_data_free (PortStatusData *data) +{ + if (data == NULL) + return; + g_free (data->port_name_to_set); + g_free (data); +} + +/* + We need to re-enumerate sources and sinks every time the user makes a choice, + because they can change due to use interaction in other software (or policy + changes inside PulseAudio). Enumeration means PulseAudio will do a series of + callbacks, one for every source/sink. + Set the port when we find the correct source/sink. + */ + +static void +sink_info_cb (pa_context *c, + const pa_sink_info *i, + int eol, + void *userdata) +{ + PortStatusData *data = userdata; + pa_operation *o; + guint j; + const char *s; + + if (eol != 0) { + port_status_data_free (data); + return; + } + + if (i->card != data->headset_card) + return; + + s = data->port_name_to_set; + + if (i->active_port && + strcmp (i->active_port->name, s) == 0) + return; + + for (j = 0; j < i->n_ports; j++) + if (strcmp (i->ports[j]->name, s) == 0) + break; + + if (j >= i->n_ports) + return; + + o = pa_context_set_sink_port_by_index (c, i->index, s, NULL, NULL); + g_clear_pointer (&o, pa_operation_unref); +} + +static void +source_info_cb (pa_context *c, + const pa_source_info *i, + int eol, + void *userdata) +{ + PortStatusData *data = userdata; + pa_operation *o; + guint j; + const char *s; + + if (eol != 0) { + port_status_data_free (data); + return; + } + + if (i->card != data->headset_card) + return; + + s = data->port_name_to_set; + + for (j = 0; j < i->n_ports; j++) { + if (g_str_equal (i->ports[j]->name, s)) { + o = pa_context_set_default_source (c, + i->name, + NULL, + NULL); + if (o == NULL) { + g_warning ("pa_context_set_default_source() failed"); + return; + } + } + } + + if (i->active_port && strcmp (i->active_port->name, s) == 0) + return; + + for (j = 0; j < i->n_ports; j++) + if (strcmp (i->ports[j]->name, s) == 0) + break; + + if (j >= i->n_ports) + return; + + o = pa_context_set_source_port_by_index(c, i->index, s, NULL, NULL); + g_clear_pointer (&o, pa_operation_unref); +} + +static void +gvc_mixer_control_set_port_status_for_headset (GvcMixerControl *control, + guint id, + const char *port_name, + gboolean is_output) +{ + pa_operation *o; + PortStatusData *data; + + if (port_name == NULL) + return; + + data = g_new0 (PortStatusData, 1); + data->port_name_to_set = g_strdup (port_name); + data->headset_card = id; + + if (is_output) + o = pa_context_get_sink_info_list (control->priv->pa_context, sink_info_cb, data); + else + o = pa_context_get_source_info_list (control->priv->pa_context, source_info_cb, data); + + g_clear_pointer (&o, pa_operation_unref); +} +#endif /* HAVE_ALSA */ + +static void +free_priv_port_names (GvcMixerControl *control) +{ +#ifdef HAVE_ALSA + g_clear_pointer (&control->priv->headphones_name, g_free); + g_clear_pointer (&control->priv->headsetmic_name, g_free); + g_clear_pointer (&control->priv->headphonemic_name, g_free); + g_clear_pointer (&control->priv->internalspk_name, g_free); + g_clear_pointer (&control->priv->internalmic_name, g_free); +#endif +} + +void +gvc_mixer_control_set_headset_port (GvcMixerControl *control, + guint id, + GvcHeadsetPortChoice choice) +{ + g_return_if_fail (GVC_IS_MIXER_CONTROL (control)); + +#ifdef HAVE_ALSA + switch (choice) { + case GVC_HEADSET_PORT_CHOICE_HEADPHONES: + gvc_mixer_control_set_port_status_for_headset (control, id, control->priv->headphones_name, TRUE); + gvc_mixer_control_set_port_status_for_headset (control, id, control->priv->internalmic_name, FALSE); + break; + case GVC_HEADSET_PORT_CHOICE_HEADSET: + gvc_mixer_control_set_port_status_for_headset (control, id, control->priv->headphones_name, TRUE); + gvc_mixer_control_set_port_status_for_headset (control, id, control->priv->headsetmic_name, FALSE); + break; + case GVC_HEADSET_PORT_CHOICE_MIC: + gvc_mixer_control_set_port_status_for_headset (control, id, control->priv->internalspk_name, TRUE); + gvc_mixer_control_set_port_status_for_headset (control, id, control->priv->headphonemic_name, FALSE); + break; + case GVC_HEADSET_PORT_CHOICE_NONE: + default: + g_assert_not_reached (); + } +#else + g_warning ("BUG: libgnome-volume-control compiled without ALSA support"); +#endif /* HAVE_ALSA */ +} + +#ifdef HAVE_ALSA +typedef struct { + const pa_card_port_info *headphones; + const pa_card_port_info *headsetmic; + const pa_card_port_info *headphonemic; + const pa_card_port_info *internalmic; + const pa_card_port_info *internalspk; +} headset_ports; + +/* + In PulseAudio without ucm, ports will show up with the following names: + Headphones - analog-output-headphones + Headset mic - analog-input-headset-mic (was: analog-input-microphone-headset) + Jack in mic-in mode - analog-input-headphone-mic (was: analog-input-microphone) + + However, since regular mics also show up as analog-input-microphone, + we need to check for certain controls on alsa mixer level too, to know + if we deal with a separate mic jack, or a multi-function jack with a + mic-in mode (also called "headphone mic"). + We check for the following names: + + Headphone Mic Jack - indicates headphone and mic-in mode share the same jack, + i e, not two separate jacks. Hardware cannot distinguish between a + headphone and a mic. + Headset Mic Phantom Jack - indicates headset jack where hardware can not + distinguish between headphones and headsets + Headset Mic Jack - indicates headset jack where hardware can distinguish + between headphones and headsets. There is no use popping up a dialog in + this case, unless we already need to do this for the mic-in mode. + + From the PA_PROCOTOL_VERSION=34, The device_port structure adds 2 members + availability_group and type, with the help of these 2 members, we could + consolidate the port checking and port setting for non-ucm and with-ucm + cases. +*/ + +#define HEADSET_PORT_SET(dst, src) \ + do { \ + if (!(dst) || (dst)->priority < (src)->priority) \ + dst = src; \ + } while (0) + +#define GET_PORT_NAME(x) (x ? g_strdup (x->name) : NULL) + +static headset_ports * +get_headset_ports (GvcMixerControl *control, + const pa_card_info *c) +{ + headset_ports *h; + guint i; + + h = g_new0 (headset_ports, 1); + + for (i = 0; i < c->n_ports; i++) { + pa_card_port_info *p = c->ports[i]; + if (control->priv->server_protocol_version < 34) { + if (g_str_equal (p->name, "analog-output-headphones")) + h->headphones = p; + else if (g_str_equal (p->name, "analog-input-headset-mic")) + h->headsetmic = p; + else if (g_str_equal (p->name, "analog-input-headphone-mic")) + h->headphonemic = p; + else if (g_str_equal (p->name, "analog-input-internal-mic")) + h->internalmic = p; + else if (g_str_equal (p->name, "analog-output-speaker")) + h->internalspk = p; + } else { +#if (PA_PROTOCOL_VERSION >= 34) + /* in the first loop, set only headphones */ + /* the microphone ports are assigned in the second loop */ + if (p->type == PA_DEVICE_PORT_TYPE_HEADPHONES) { + if (p->availability_group) + HEADSET_PORT_SET (h->headphones, p); + } else if (p->type == PA_DEVICE_PORT_TYPE_SPEAKER) { + HEADSET_PORT_SET (h->internalspk, p); + } else if (p->type == PA_DEVICE_PORT_TYPE_MIC) { + if (!p->availability_group) + HEADSET_PORT_SET (h->internalmic, p); + } +#else + g_warning_once ("libgnome-volume-control running against PulseAudio %u, " + "but compiled against older %d, report a bug to your distribution", + control->priv->server_protocol_version, + PA_PROTOCOL_VERSION); +#endif + } + } + +#if (PA_PROTOCOL_VERSION >= 34) + if (h->headphones && (control->priv->server_protocol_version >= 34)) { + for (i = 0; i < c->n_ports; i++) { + pa_card_port_info *p = c->ports[i]; + if (g_strcmp0(h->headphones->availability_group, p->availability_group)) + continue; + if (p->direction != PA_DIRECTION_INPUT) + continue; + if (p->type == PA_DEVICE_PORT_TYPE_HEADSET) + HEADSET_PORT_SET (h->headsetmic, p); + else if (p->type == PA_DEVICE_PORT_TYPE_MIC) + HEADSET_PORT_SET (h->headphonemic, p); + } + } +#endif + + return h; +} + +static gboolean +verify_alsa_card (int cardindex, + gboolean *headsetmic, + gboolean *headphonemic) +{ + char *ctlstr; + snd_hctl_t *hctl; + snd_ctl_elem_id_t *id; + int err; + + *headsetmic = FALSE; + *headphonemic = FALSE; + + ctlstr = g_strdup_printf ("hw:%i", cardindex); + if ((err = snd_hctl_open (&hctl, ctlstr, 0)) < 0) { + g_warning ("snd_hctl_open failed: %s", snd_strerror(err)); + g_free (ctlstr); + return FALSE; + } + g_free (ctlstr); + + if ((err = snd_hctl_load (hctl)) < 0) { + g_warning ("snd_hctl_load failed: %s", snd_strerror(err)); + snd_hctl_close (hctl); + return FALSE; + } + + snd_ctl_elem_id_alloca (&id); + + snd_ctl_elem_id_clear (id); + snd_ctl_elem_id_set_interface (id, SND_CTL_ELEM_IFACE_CARD); + snd_ctl_elem_id_set_name (id, "Headphone Mic Jack"); + if (snd_hctl_find_elem (hctl, id)) + *headphonemic = TRUE; + + snd_ctl_elem_id_clear (id); + snd_ctl_elem_id_set_interface (id, SND_CTL_ELEM_IFACE_CARD); + snd_ctl_elem_id_set_name (id, "Headset Mic Phantom Jack"); + if (snd_hctl_find_elem (hctl, id)) + *headsetmic = TRUE; + + if (*headphonemic) { + snd_ctl_elem_id_clear (id); + snd_ctl_elem_id_set_interface (id, SND_CTL_ELEM_IFACE_CARD); + snd_ctl_elem_id_set_name (id, "Headset Mic Jack"); + if (snd_hctl_find_elem (hctl, id)) + *headsetmic = TRUE; + } + + snd_hctl_close (hctl); + return *headsetmic || *headphonemic; +} + +static void +check_audio_device_selection_needed (GvcMixerControl *control, + const pa_card_info *info) +{ + headset_ports *h; + gboolean start_dialog, stop_dialog; + + start_dialog = FALSE; + stop_dialog = FALSE; + h = get_headset_ports (control, info); + + if (!h->headphones || + (!h->headsetmic && !h->headphonemic)) { + /* Not a headset jack */ + goto out; + } + + if (control->priv->headset_card != (int) info->index) { + int cardindex; + gboolean hsmic = TRUE; + gboolean hpmic = TRUE; + const char *s; + + s = pa_proplist_gets (info->proplist, "alsa.card"); + if (!s) + goto out; + + cardindex = strtol (s, NULL, 10); + if (cardindex == 0 && strcmp(s, "0") != 0) + goto out; + + if (control->priv->server_protocol_version < 34) { + if (!verify_alsa_card(cardindex, &hsmic, &hpmic)) + goto out; + } + + control->priv->headset_card = info->index; + control->priv->has_headsetmic = hsmic && h->headsetmic; + control->priv->has_headphonemic = hpmic && h->headphonemic; + } else { + start_dialog = (h->headphones->available != PA_PORT_AVAILABLE_NO) && !control->priv->headset_plugged_in; + stop_dialog = (h->headphones->available == PA_PORT_AVAILABLE_NO) && control->priv->headset_plugged_in; + } + + control->priv->headset_plugged_in = h->headphones->available != PA_PORT_AVAILABLE_NO; + free_priv_port_names (control); + control->priv->headphones_name = GET_PORT_NAME(h->headphones); + control->priv->headsetmic_name = GET_PORT_NAME(h->headsetmic); + control->priv->headphonemic_name = GET_PORT_NAME(h->headphonemic); + control->priv->internalspk_name = GET_PORT_NAME(h->internalspk); + control->priv->internalmic_name = GET_PORT_NAME(h->internalmic); + + if (!start_dialog && + !stop_dialog) + goto out; + + if (stop_dialog) { + g_signal_emit (G_OBJECT (control), + signals[AUDIO_DEVICE_SELECTION_NEEDED], + 0, + info->index, + FALSE, + GVC_HEADSET_PORT_CHOICE_NONE); + } else { + GvcHeadsetPortChoice choices; + + choices = GVC_HEADSET_PORT_CHOICE_HEADPHONES; + if (control->priv->has_headsetmic) + choices |= GVC_HEADSET_PORT_CHOICE_HEADSET; + if (control->priv->has_headphonemic) + choices |= GVC_HEADSET_PORT_CHOICE_MIC; + + g_signal_emit (G_OBJECT (control), + signals[AUDIO_DEVICE_SELECTION_NEEDED], + 0, + info->index, + TRUE, + choices); + } + +out: + g_free (h); +} +#endif /* HAVE_ALSA */ + +/* + * At this point we can determine all devices available to us (besides network 'ports') + * This is done by the following: + * + * - gvc_mixer_card and gvc_mixer_card_ports are created and relevant setters are called. + * - First it checks to see if it's a portless card. Bluetooth devices are portless AFAIHS. + * If so it creates two devices, an input and an output. + * - If it's a 'normal' card with ports it will create a new ui-device or + * synchronise port availability with the existing device cached for that port on this card. */ + +static void +update_card (GvcMixerControl *control, + const pa_card_info *info) +{ + const GList *card_ports = NULL; + const GList *m = NULL; + GvcMixerCard *card; + gboolean is_new = FALSE; +#if 1 + guint i; + const char *key; + void *state; + + g_debug ("Updating card %s (index: %u driver: %s):", + info->name, info->index, info->driver); + + for (i = 0; i < info->n_profiles; i++) { + struct pa_card_profile_info pi = info->profiles[i]; + gboolean is_default; + + is_default = (g_strcmp0 (pi.name, info->active_profile->name) == 0); + g_debug ("\tProfile '%s': %d sources %d sinks%s", + pi.name, pi.n_sources, pi.n_sinks, + is_default ? " (Current)" : ""); + } + state = NULL; + key = pa_proplist_iterate (info->proplist, &state); + while (key != NULL) { + g_debug ("\tProperty: '%s' = '%s'", + key, pa_proplist_gets (info->proplist, key)); + key = pa_proplist_iterate (info->proplist, &state); + } +#endif + card = g_hash_table_lookup (control->priv->cards, + GUINT_TO_POINTER (info->index)); + if (card == NULL) { + GList *profile_list = NULL; + GList *port_list = NULL; + + for (i = 0; i < info->n_profiles; i++) { + GvcMixerCardProfile *profile; + struct pa_card_profile_info pi = info->profiles[i]; + + profile = g_new0 (GvcMixerCardProfile, 1); + profile->profile = g_strdup (pi.name); + profile->human_profile = g_strdup (pi.description); + profile->status = card_num_streams_to_status (pi.n_sinks, pi.n_sources); + profile->n_sinks = pi.n_sinks; + profile->n_sources = pi.n_sources; + profile->priority = pi.priority; + profile_list = g_list_prepend (profile_list, profile); + } + card = gvc_mixer_card_new (control->priv->pa_context, + info->index); + + for (i = 0; i < info->n_ports; i++) { + GvcMixerCardPort *port; + port = g_new0 (GvcMixerCardPort, 1); + port->port = g_strdup (info->ports[i]->name); + port->human_port = g_strdup (info->ports[i]->description); + port->priority = info->ports[i]->priority; + port->available = info->ports[i]->available; + port->direction = info->ports[i]->direction; + port->icon_name = g_strdup (pa_proplist_gets (info->ports[i]->proplist, "device.icon_name")); + port->profiles = determine_profiles_for_port (info->ports[i], profile_list); + port_list = g_list_prepend (port_list, port); + } + + gvc_mixer_card_set_profiles (card, profile_list); + gvc_mixer_card_set_ports (card, port_list); + is_new = TRUE; + } + + gvc_mixer_card_set_name (card, pa_proplist_gets (info->proplist, "device.description")); + gvc_mixer_card_set_icon_name (card, pa_proplist_gets (info->proplist, "device.icon_name")); + gvc_mixer_card_set_profile (card, info->active_profile->name); + + if (is_new) { + g_hash_table_insert (control->priv->cards, + GUINT_TO_POINTER (info->index), + card); + } + + card_ports = gvc_mixer_card_get_ports (card); + + if (card_ports == NULL && is_new) { + g_debug ("Portless card just registered - %s", gvc_mixer_card_get_name (card)); + create_ui_device_from_card (control, card); + } + + for (m = card_ports; m != NULL; m = m->next) { + GvcMixerCardPort *card_port; + card_port = m->data; + if (is_new) + create_ui_device_from_port (control, card_port, card); + else { + for (i = 0; i < info->n_ports; i++) { + if (g_strcmp0 (card_port->port, info->ports[i]->name) == 0) { + if ((card_port->available == PA_PORT_AVAILABLE_NO) != (info->ports[i]->available == PA_PORT_AVAILABLE_NO)) { + card_port->available = info->ports[i]->available; + g_debug ("sync port availability on card %i, card port name '%s', new available value %i", + gvc_mixer_card_get_index (card), + card_port->port, + card_port->available); + match_card_port_with_existing_device (control, + card_port, + card, + card_port->available != PA_PORT_AVAILABLE_NO); + } + } + } + } + } + +#ifdef HAVE_ALSA + check_audio_device_selection_needed (control, info); +#endif /* HAVE_ALSA */ + + g_signal_emit (G_OBJECT (control), + signals[CARD_ADDED], + 0, + info->index); +} + +static void +_pa_context_get_sink_info_cb (pa_context *context, + const pa_sink_info *i, + int eol, + void *userdata) +{ + GvcMixerControl *control = GVC_MIXER_CONTROL (userdata); + + if (eol < 0) { + if (pa_context_errno (context) == PA_ERR_NOENTITY) { + return; + } + + g_warning ("Sink callback failure"); + return; + } + + if (eol > 0) { + dec_outstanding (control); + return; + } + + update_sink (control, i); +} + +static void +_pa_context_get_source_info_cb (pa_context *context, + const pa_source_info *i, + int eol, + void *userdata) +{ + GvcMixerControl *control = GVC_MIXER_CONTROL (userdata); + + if (eol < 0) { + if (pa_context_errno (context) == PA_ERR_NOENTITY) { + return; + } + + g_warning ("Source callback failure"); + return; + } + + if (eol > 0) { + dec_outstanding (control); + return; + } + + update_source (control, i); +} + +static void +_pa_context_get_sink_input_info_cb (pa_context *context, + const pa_sink_input_info *i, + int eol, + void *userdata) +{ + GvcMixerControl *control = GVC_MIXER_CONTROL (userdata); + + if (eol < 0) { + if (pa_context_errno (context) == PA_ERR_NOENTITY) { + return; + } + + g_warning ("Sink input callback failure"); + return; + } + + if (eol > 0) { + dec_outstanding (control); + return; + } + + update_sink_input (control, i); +} + +static void +_pa_context_get_source_output_info_cb (pa_context *context, + const pa_source_output_info *i, + int eol, + void *userdata) +{ + GvcMixerControl *control = GVC_MIXER_CONTROL (userdata); + + if (eol < 0) { + if (pa_context_errno (context) == PA_ERR_NOENTITY) { + return; + } + + g_warning ("Source output callback failure"); + return; + } + + if (eol > 0) { + dec_outstanding (control); + return; + } + + update_source_output (control, i); +} + +static void +_pa_context_get_client_info_cb (pa_context *context, + const pa_client_info *i, + int eol, + void *userdata) +{ + GvcMixerControl *control = GVC_MIXER_CONTROL (userdata); + + if (eol < 0) { + if (pa_context_errno (context) == PA_ERR_NOENTITY) { + return; + } + + g_warning ("Client callback failure"); + return; + } + + if (eol > 0) { + dec_outstanding (control); + return; + } + + update_client (control, i); +} + +static void +_pa_context_get_card_info_by_index_cb (pa_context *context, + const pa_card_info *i, + int eol, + void *userdata) +{ + GvcMixerControl *control = GVC_MIXER_CONTROL (userdata); + + if (eol < 0) { + if (pa_context_errno (context) == PA_ERR_NOENTITY) + return; + + g_warning ("Card callback failure"); + return; + } + + if (eol > 0) { + dec_outstanding (control); + return; + } + + update_card (control, i); +} + +static void +_pa_context_get_server_info_cb (pa_context *context, + const pa_server_info *i, + void *userdata) +{ + GvcMixerControl *control = GVC_MIXER_CONTROL (userdata); + + if (i == NULL) { + g_warning ("Server info callback failure"); + return; + } + g_debug ("get server info"); + update_server (control, i); + dec_outstanding (control); +} + +static void +remove_event_role_stream (GvcMixerControl *control) +{ + g_debug ("Removing event role"); +} + +static void +update_event_role_stream (GvcMixerControl *control, + const pa_ext_stream_restore_info *info) +{ + GvcMixerStream *stream; + gboolean is_new; + pa_volume_t max_volume; + + if (strcmp (info->name, "sink-input-by-media-role:event") != 0) { + return; + } + +#if 0 + g_debug ("Updating event role: name='%s' device='%s'", + info->name, + info->device); +#endif + + is_new = FALSE; + + if (!control->priv->event_sink_input_is_set) { + pa_channel_map pa_map; + GvcChannelMap *map; + + pa_map.channels = 1; + pa_map.map[0] = PA_CHANNEL_POSITION_MONO; + map = gvc_channel_map_new_from_pa_channel_map (&pa_map); + + stream = gvc_mixer_event_role_new (control->priv->pa_context, + info->device, + map); + control->priv->event_sink_input_id = gvc_mixer_stream_get_id (stream); + control->priv->event_sink_input_is_set = TRUE; + + is_new = TRUE; + } else { + stream = g_hash_table_lookup (control->priv->all_streams, + GUINT_TO_POINTER (control->priv->event_sink_input_id)); + } + + max_volume = pa_cvolume_max (&info->volume); + + gvc_mixer_stream_set_name (stream, _("System Sounds")); + gvc_mixer_stream_set_icon_name (stream, "audio-x-generic"); + gvc_mixer_stream_set_volume (stream, (guint)max_volume); + gvc_mixer_stream_set_is_muted (stream, info->mute); + + if (is_new) { + add_stream (control, stream); + } +} + +static void +_pa_ext_stream_restore_read_cb (pa_context *context, + const pa_ext_stream_restore_info *i, + int eol, + void *userdata) +{ + GvcMixerControl *control = GVC_MIXER_CONTROL (userdata); + + if (eol < 0) { + g_debug ("Failed to initialized stream_restore extension: %s", + pa_strerror (pa_context_errno (context))); + remove_event_role_stream (control); + return; + } + + if (eol > 0) { + dec_outstanding (control); + /* If we don't have an event stream to restore, then + * set one up with a default 100% volume */ + if (!control->priv->event_sink_input_is_set) { + pa_ext_stream_restore_info info; + + memset (&info, 0, sizeof(info)); + info.name = "sink-input-by-media-role:event"; + info.volume.channels = 1; + info.volume.values[0] = PA_VOLUME_NORM; + update_event_role_stream (control, &info); + } + return; + } + + update_event_role_stream (control, i); +} + +static void +_pa_ext_stream_restore_subscribe_cb (pa_context *context, + void *userdata) +{ + GvcMixerControl *control = GVC_MIXER_CONTROL (userdata); + pa_operation *o; + + o = pa_ext_stream_restore_read (context, + _pa_ext_stream_restore_read_cb, + control); + if (o == NULL) { + g_warning ("pa_ext_stream_restore_read() failed"); + return; + } + + pa_operation_unref (o); +} + +static void +req_update_server_info (GvcMixerControl *control, + int index) +{ + pa_operation *o; + + o = pa_context_get_server_info (control->priv->pa_context, + _pa_context_get_server_info_cb, + control); + if (o == NULL) { + g_warning ("pa_context_get_server_info() failed"); + return; + } + pa_operation_unref (o); +} + +static void +req_update_client_info (GvcMixerControl *control, + int index) +{ + pa_operation *o; + + if (index < 0) { + o = pa_context_get_client_info_list (control->priv->pa_context, + _pa_context_get_client_info_cb, + control); + } else { + o = pa_context_get_client_info (control->priv->pa_context, + index, + _pa_context_get_client_info_cb, + control); + } + + if (o == NULL) { + g_warning ("pa_context_client_info_list() failed"); + return; + } + pa_operation_unref (o); +} + +static void +req_update_card (GvcMixerControl *control, + int index) +{ + pa_operation *o; + + if (index < 0) { + o = pa_context_get_card_info_list (control->priv->pa_context, + _pa_context_get_card_info_by_index_cb, + control); + } else { + o = pa_context_get_card_info_by_index (control->priv->pa_context, + index, + _pa_context_get_card_info_by_index_cb, + control); + } + + if (o == NULL) { + g_warning ("pa_context_get_card_info_by_index() failed"); + return; + } + pa_operation_unref (o); +} + +static void +req_update_sink_info (GvcMixerControl *control, + int index) +{ + pa_operation *o; + + if (index < 0) { + o = pa_context_get_sink_info_list (control->priv->pa_context, + _pa_context_get_sink_info_cb, + control); + } else { + o = pa_context_get_sink_info_by_index (control->priv->pa_context, + index, + _pa_context_get_sink_info_cb, + control); + } + + if (o == NULL) { + g_warning ("pa_context_get_sink_info_list() failed"); + return; + } + pa_operation_unref (o); +} + +static void +req_update_source_info (GvcMixerControl *control, + int index) +{ + pa_operation *o; + + if (index < 0) { + o = pa_context_get_source_info_list (control->priv->pa_context, + _pa_context_get_source_info_cb, + control); + } else { + o = pa_context_get_source_info_by_index(control->priv->pa_context, + index, + _pa_context_get_source_info_cb, + control); + } + + if (o == NULL) { + g_warning ("pa_context_get_source_info_list() failed"); + return; + } + pa_operation_unref (o); +} + +static void +req_update_sink_input_info (GvcMixerControl *control, + int index) +{ + pa_operation *o; + + if (index < 0) { + o = pa_context_get_sink_input_info_list (control->priv->pa_context, + _pa_context_get_sink_input_info_cb, + control); + } else { + o = pa_context_get_sink_input_info (control->priv->pa_context, + index, + _pa_context_get_sink_input_info_cb, + control); + } + + if (o == NULL) { + g_warning ("pa_context_get_sink_input_info_list() failed"); + return; + } + pa_operation_unref (o); +} + +static void +req_update_source_output_info (GvcMixerControl *control, + int index) +{ + pa_operation *o; + + if (index < 0) { + o = pa_context_get_source_output_info_list (control->priv->pa_context, + _pa_context_get_source_output_info_cb, + control); + } else { + o = pa_context_get_source_output_info (control->priv->pa_context, + index, + _pa_context_get_source_output_info_cb, + control); + } + + if (o == NULL) { + g_warning ("pa_context_get_source_output_info_list() failed"); + return; + } + pa_operation_unref (o); +} + +static void +remove_client (GvcMixerControl *control, + guint index) +{ + g_hash_table_remove (control->priv->clients, + GUINT_TO_POINTER (index)); +} + +static void +remove_card (GvcMixerControl *control, + guint index) +{ + + GList *devices, *d; + + devices = g_list_concat (g_hash_table_get_values (control->priv->ui_inputs), + g_hash_table_get_values (control->priv->ui_outputs)); + + for (d = devices; d != NULL; d = d->next) { + GvcMixerCard *card; + GvcMixerUIDevice *device = d->data; + + g_object_get (G_OBJECT (device), "card", &card, NULL); + + if (card == NULL) + continue; + + if (gvc_mixer_card_get_index (card) == index) { + g_signal_emit (G_OBJECT (control), + signals[gvc_mixer_ui_device_is_output (device) ? OUTPUT_REMOVED : INPUT_REMOVED], + 0, + gvc_mixer_ui_device_get_id (device)); + g_debug ("Card removal remove device %s", + gvc_mixer_ui_device_get_description (device)); + g_hash_table_remove (gvc_mixer_ui_device_is_output (device) ? control->priv->ui_outputs : control->priv->ui_inputs, + GUINT_TO_POINTER (gvc_mixer_ui_device_get_id (device))); + } + } + + g_list_free (devices); + + g_hash_table_remove (control->priv->cards, + GUINT_TO_POINTER (index)); + + g_signal_emit (G_OBJECT (control), + signals[CARD_REMOVED], + 0, + index); +} + +static void +remove_sink (GvcMixerControl *control, + guint index) +{ + GvcMixerStream *stream; + GvcMixerUIDevice *device; + + g_debug ("Removing sink: index=%u", index); + + stream = g_hash_table_lookup (control->priv->sinks, + GUINT_TO_POINTER (index)); + if (stream == NULL) + return; + + device = gvc_mixer_control_lookup_device_from_stream (control, stream); + + if (device != NULL) { + gvc_mixer_ui_device_invalidate_stream (device); + if (!gvc_mixer_ui_device_has_ports (device)) { + g_signal_emit (G_OBJECT (control), + signals[OUTPUT_REMOVED], + 0, + gvc_mixer_ui_device_get_id (device)); + } else { + GList *devices, *d; + + devices = g_hash_table_get_values (control->priv->ui_outputs); + + for (d = devices; d != NULL; d = d->next) { + guint stream_id = GVC_MIXER_UI_DEVICE_INVALID; + device = d->data; + g_object_get (G_OBJECT (device), + "stream-id", &stream_id, + NULL); + if (stream_id == gvc_mixer_stream_get_id (stream)) + gvc_mixer_ui_device_invalidate_stream (device); + } + + g_list_free (devices); + } + } + + g_hash_table_remove (control->priv->sinks, + GUINT_TO_POINTER (index)); + + remove_stream (control, stream); +} + +static void +remove_source (GvcMixerControl *control, + guint index) +{ + GvcMixerStream *stream; + GvcMixerUIDevice *device; + + g_debug ("Removing source: index=%u", index); + + stream = g_hash_table_lookup (control->priv->sources, + GUINT_TO_POINTER (index)); + if (stream == NULL) + return; + + device = gvc_mixer_control_lookup_device_from_stream (control, stream); + + if (device != NULL) { + gvc_mixer_ui_device_invalidate_stream (device); + if (!gvc_mixer_ui_device_has_ports (device)) { + g_signal_emit (G_OBJECT (control), + signals[INPUT_REMOVED], + 0, + gvc_mixer_ui_device_get_id (device)); + } else { + GList *devices, *d; + + devices = g_hash_table_get_values (control->priv->ui_inputs); + + for (d = devices; d != NULL; d = d->next) { + guint stream_id = GVC_MIXER_UI_DEVICE_INVALID; + device = d->data; + g_object_get (G_OBJECT (device), + "stream-id", &stream_id, + NULL); + if (stream_id == gvc_mixer_stream_get_id (stream)) + gvc_mixer_ui_device_invalidate_stream (device); + } + + g_list_free (devices); + } + } + + g_hash_table_remove (control->priv->sources, + GUINT_TO_POINTER (index)); + + remove_stream (control, stream); +} + +static void +remove_sink_input (GvcMixerControl *control, + guint index) +{ + GvcMixerStream *stream; + + g_debug ("Removing sink input: index=%u", index); + + stream = g_hash_table_lookup (control->priv->sink_inputs, + GUINT_TO_POINTER (index)); + if (stream == NULL) { + return; + } + g_hash_table_remove (control->priv->sink_inputs, + GUINT_TO_POINTER (index)); + + remove_stream (control, stream); +} + +static void +remove_source_output (GvcMixerControl *control, + guint index) +{ + GvcMixerStream *stream; + + g_debug ("Removing source output: index=%u", index); + + stream = g_hash_table_lookup (control->priv->source_outputs, + GUINT_TO_POINTER (index)); + if (stream == NULL) { + return; + } + g_hash_table_remove (control->priv->source_outputs, + GUINT_TO_POINTER (index)); + + remove_stream (control, stream); +} + +static void +_pa_context_subscribe_cb (pa_context *context, + pa_subscription_event_type_t t, + uint32_t index, + void *userdata) +{ + GvcMixerControl *control = GVC_MIXER_CONTROL (userdata); + + switch (t & PA_SUBSCRIPTION_EVENT_FACILITY_MASK) { + case PA_SUBSCRIPTION_EVENT_SINK: + if ((t & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_REMOVE) { + remove_sink (control, index); + } else { + req_update_sink_info (control, index); + } + break; + + case PA_SUBSCRIPTION_EVENT_SOURCE: + if ((t & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_REMOVE) { + remove_source (control, index); + } else { + req_update_source_info (control, index); + } + break; + + case PA_SUBSCRIPTION_EVENT_SINK_INPUT: + if ((t & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_REMOVE) { + remove_sink_input (control, index); + } else { + req_update_sink_input_info (control, index); + } + break; + + case PA_SUBSCRIPTION_EVENT_SOURCE_OUTPUT: + if ((t & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_REMOVE) { + remove_source_output (control, index); + } else { + req_update_source_output_info (control, index); + } + break; + + case PA_SUBSCRIPTION_EVENT_CLIENT: + if ((t & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_REMOVE) { + remove_client (control, index); + } else { + req_update_client_info (control, index); + } + break; + + case PA_SUBSCRIPTION_EVENT_SERVER: + req_update_server_info (control, index); + break; + + case PA_SUBSCRIPTION_EVENT_CARD: + if ((t & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_REMOVE) { + remove_card (control, index); + } else { + req_update_card (control, index); + } + break; + default: + break; + } +} + +static void +gvc_mixer_control_ready (GvcMixerControl *control) +{ + pa_operation *o; + + pa_context_set_subscribe_callback (control->priv->pa_context, + _pa_context_subscribe_cb, + control); + o = pa_context_subscribe (control->priv->pa_context, + (pa_subscription_mask_t) + (PA_SUBSCRIPTION_MASK_SINK| + PA_SUBSCRIPTION_MASK_SOURCE| + PA_SUBSCRIPTION_MASK_SINK_INPUT| + PA_SUBSCRIPTION_MASK_SOURCE_OUTPUT| + PA_SUBSCRIPTION_MASK_CLIENT| + PA_SUBSCRIPTION_MASK_SERVER| + PA_SUBSCRIPTION_MASK_CARD), + NULL, + NULL); + + if (o == NULL) { + g_warning ("pa_context_subscribe() failed"); + return; + } + pa_operation_unref (o); + + req_update_server_info (control, -1); + req_update_card (control, -1); + req_update_client_info (control, -1); + req_update_sink_info (control, -1); + req_update_source_info (control, -1); + req_update_sink_input_info (control, -1); + req_update_source_output_info (control, -1); + + control->priv->server_protocol_version = pa_context_get_server_protocol_version (control->priv->pa_context); + + control->priv->n_outstanding = 6; + + /* This call is not always supported */ + o = pa_ext_stream_restore_read (control->priv->pa_context, + _pa_ext_stream_restore_read_cb, + control); + if (o != NULL) { + pa_operation_unref (o); + control->priv->n_outstanding++; + + pa_ext_stream_restore_set_subscribe_cb (control->priv->pa_context, + _pa_ext_stream_restore_subscribe_cb, + control); + + o = pa_ext_stream_restore_subscribe (control->priv->pa_context, + 1, + NULL, + NULL); + if (o != NULL) { + pa_operation_unref (o); + } + + } else { + g_debug ("Failed to initialized stream_restore extension: %s", + pa_strerror (pa_context_errno (control->priv->pa_context))); + } +} + +static void +gvc_mixer_new_pa_context (GvcMixerControl *self) +{ + pa_proplist *proplist; + + g_return_if_fail (self); + g_return_if_fail (!self->priv->pa_context); + + proplist = pa_proplist_new (); + pa_proplist_sets (proplist, + PA_PROP_APPLICATION_NAME, + self->priv->name); + pa_proplist_sets (proplist, + PA_PROP_APPLICATION_ID, + "org.gnome.VolumeControl"); + pa_proplist_sets (proplist, + PA_PROP_APPLICATION_ICON_NAME, + "multimedia-volume-control"); + pa_proplist_sets (proplist, + PA_PROP_APPLICATION_VERSION, + PACKAGE_VERSION); + + self->priv->pa_context = pa_context_new_with_proplist (self->priv->pa_api, NULL, proplist); + + pa_proplist_free (proplist); + g_assert (self->priv->pa_context); +} + +static void +remove_all_items (GvcMixerControl *control, + GHashTable *hash_table, + void (*remove_item)(GvcMixerControl *control, guint index)) +{ + GHashTableIter iter; + gpointer key, value; + + g_hash_table_iter_init (&iter, hash_table); + while (g_hash_table_iter_next (&iter, &key, &value)) { + if (remove_item) { + remove_item (control, GPOINTER_TO_UINT (key)); + g_hash_table_remove (hash_table, key); + g_hash_table_iter_init (&iter, hash_table); + } else { + g_hash_table_iter_remove (&iter); + } + } +} + +static gboolean +idle_reconnect (gpointer data) +{ + GvcMixerControl *control = GVC_MIXER_CONTROL (data); + + g_return_val_if_fail (control, FALSE); + + g_debug ("Reconnect: clean up all objects"); + + remove_all_items (control, control->priv->sinks, remove_sink); + remove_all_items (control, control->priv->sources, remove_source); + remove_all_items (control, control->priv->sink_inputs, remove_sink_input); + remove_all_items (control, control->priv->source_outputs, remove_source_output); + remove_all_items (control, control->priv->cards, remove_card); + remove_all_items (control, control->priv->ui_inputs, NULL); + remove_all_items (control, control->priv->ui_outputs, NULL); + remove_all_items (control, control->priv->clients, remove_client); + + g_debug ("Reconnect: make new connection"); + + if (control->priv->pa_context) { + pa_context_unref (control->priv->pa_context); + control->priv->pa_context = NULL; + control->priv->server_protocol_version = 0; + gvc_mixer_new_pa_context (control); + } + + gvc_mixer_control_open (control); /* cannot fail */ + + control->priv->reconnect_id = 0; + return FALSE; +} + +static void +_pa_context_state_cb (pa_context *context, + void *userdata) +{ + GvcMixerControl *control = GVC_MIXER_CONTROL (userdata); + + switch (pa_context_get_state (context)) { + case PA_CONTEXT_UNCONNECTED: + case PA_CONTEXT_CONNECTING: + case PA_CONTEXT_AUTHORIZING: + case PA_CONTEXT_SETTING_NAME: + break; + + case PA_CONTEXT_READY: + gvc_mixer_control_ready (control); + break; + + case PA_CONTEXT_FAILED: + control->priv->state = GVC_STATE_FAILED; + g_signal_emit (control, signals[STATE_CHANGED], 0, GVC_STATE_FAILED); + if (control->priv->reconnect_id == 0) + control->priv->reconnect_id = g_timeout_add_seconds (RECONNECT_DELAY, idle_reconnect, control); + break; + + case PA_CONTEXT_TERMINATED: + default: + /* FIXME: */ + break; + } +} + +gboolean +gvc_mixer_control_open (GvcMixerControl *control) +{ + int res; + + g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), FALSE); + g_return_val_if_fail (control->priv->pa_context != NULL, FALSE); + g_return_val_if_fail (pa_context_get_state (control->priv->pa_context) == PA_CONTEXT_UNCONNECTED, FALSE); + + pa_context_set_state_callback (control->priv->pa_context, + _pa_context_state_cb, + control); + + control->priv->state = GVC_STATE_CONNECTING; + g_signal_emit (G_OBJECT (control), signals[STATE_CHANGED], 0, GVC_STATE_CONNECTING); + res = pa_context_connect (control->priv->pa_context, NULL, (pa_context_flags_t) PA_CONTEXT_NOFAIL, NULL); + if (res < 0) { + g_warning ("Failed to connect context: %s", + pa_strerror (pa_context_errno (control->priv->pa_context))); + } + + return res; +} + +gboolean +gvc_mixer_control_close (GvcMixerControl *control) +{ + g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), FALSE); + g_return_val_if_fail (control->priv->pa_context != NULL, FALSE); + + pa_context_disconnect (control->priv->pa_context); + + control->priv->state = GVC_STATE_CLOSED; + g_signal_emit (G_OBJECT (control), signals[STATE_CHANGED], 0, GVC_STATE_CLOSED); + return TRUE; +} + +static void +gvc_mixer_control_dispose (GObject *object) +{ + GvcMixerControl *control = GVC_MIXER_CONTROL (object); + + if (control->priv->reconnect_id != 0) { + g_source_remove (control->priv->reconnect_id); + control->priv->reconnect_id = 0; + } + + if (control->priv->pa_context != NULL) { + pa_context_unref (control->priv->pa_context); + control->priv->pa_context = NULL; + } + + if (control->priv->default_source_name != NULL) { + g_free (control->priv->default_source_name); + control->priv->default_source_name = NULL; + } + if (control->priv->default_sink_name != NULL) { + g_free (control->priv->default_sink_name); + control->priv->default_sink_name = NULL; + } + + if (control->priv->pa_mainloop != NULL) { + pa_glib_mainloop_free (control->priv->pa_mainloop); + control->priv->pa_mainloop = NULL; + } + + if (control->priv->all_streams != NULL) { + g_hash_table_destroy (control->priv->all_streams); + control->priv->all_streams = NULL; + } + + if (control->priv->sinks != NULL) { + g_hash_table_destroy (control->priv->sinks); + control->priv->sinks = NULL; + } + if (control->priv->sources != NULL) { + g_hash_table_destroy (control->priv->sources); + control->priv->sources = NULL; + } + if (control->priv->sink_inputs != NULL) { + g_hash_table_destroy (control->priv->sink_inputs); + control->priv->sink_inputs = NULL; + } + if (control->priv->source_outputs != NULL) { + g_hash_table_destroy (control->priv->source_outputs); + control->priv->source_outputs = NULL; + } + if (control->priv->clients != NULL) { + g_hash_table_destroy (control->priv->clients); + control->priv->clients = NULL; + } + if (control->priv->cards != NULL) { + g_hash_table_destroy (control->priv->cards); + control->priv->cards = NULL; + } + if (control->priv->ui_outputs != NULL) { + g_hash_table_destroy (control->priv->ui_outputs); + control->priv->ui_outputs = NULL; + } + if (control->priv->ui_inputs != NULL) { + g_hash_table_destroy (control->priv->ui_inputs); + control->priv->ui_inputs = NULL; + } + + free_priv_port_names (control); + G_OBJECT_CLASS (gvc_mixer_control_parent_class)->dispose (object); +} + +static void +gvc_mixer_control_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + GvcMixerControl *self = GVC_MIXER_CONTROL (object); + + switch (prop_id) { + case PROP_NAME: + g_free (self->priv->name); + self->priv->name = g_value_dup_string (value); + g_object_notify_by_pspec (G_OBJECT (self), obj_props[PROP_NAME]); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +gvc_mixer_control_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + GvcMixerControl *self = GVC_MIXER_CONTROL (object); + + switch (prop_id) { + case PROP_NAME: + g_value_set_string (value, self->priv->name); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + + +static GObject * +gvc_mixer_control_constructor (GType type, + guint n_construct_properties, + GObjectConstructParam *construct_params) +{ + GObject *object; + GvcMixerControl *self; + + object = G_OBJECT_CLASS (gvc_mixer_control_parent_class)->constructor (type, n_construct_properties, construct_params); + + self = GVC_MIXER_CONTROL (object); + + gvc_mixer_new_pa_context (self); + self->priv->profile_swapping_device_id = GVC_MIXER_UI_DEVICE_INVALID; + + return object; +} + +static void +gvc_mixer_control_class_init (GvcMixerControlClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->constructor = gvc_mixer_control_constructor; + object_class->dispose = gvc_mixer_control_dispose; + object_class->finalize = gvc_mixer_control_finalize; + object_class->set_property = gvc_mixer_control_set_property; + object_class->get_property = gvc_mixer_control_get_property; + + obj_props[PROP_NAME] = g_param_spec_string ("name", + "Name", + "Name to display for this mixer control", + NULL, + G_PARAM_READWRITE|G_PARAM_CONSTRUCT_ONLY|G_PARAM_STATIC_STRINGS); + g_object_class_install_properties (object_class, N_PROPS, obj_props); + + signals [STATE_CHANGED] = + g_signal_new ("state-changed", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (GvcMixerControlClass, state_changed), + NULL, NULL, NULL, + G_TYPE_NONE, 1, G_TYPE_UINT); + signals [STREAM_ADDED] = + g_signal_new ("stream-added", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (GvcMixerControlClass, stream_added), + NULL, NULL, NULL, + G_TYPE_NONE, 1, G_TYPE_UINT); + signals [STREAM_REMOVED] = + g_signal_new ("stream-removed", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (GvcMixerControlClass, stream_removed), + NULL, NULL, NULL, + G_TYPE_NONE, 1, G_TYPE_UINT); + signals [STREAM_CHANGED] = + g_signal_new ("stream-changed", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (GvcMixerControlClass, stream_changed), + NULL, NULL, NULL, + G_TYPE_NONE, 1, G_TYPE_UINT); + signals [AUDIO_DEVICE_SELECTION_NEEDED] = + g_signal_new ("audio-device-selection-needed", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, + NULL, NULL, NULL, + G_TYPE_NONE, 3, G_TYPE_UINT, G_TYPE_BOOLEAN, G_TYPE_UINT); + signals [CARD_ADDED] = + g_signal_new ("card-added", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (GvcMixerControlClass, card_added), + NULL, NULL, NULL, + G_TYPE_NONE, 1, G_TYPE_UINT); + signals [CARD_REMOVED] = + g_signal_new ("card-removed", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (GvcMixerControlClass, card_removed), + NULL, NULL, NULL, + G_TYPE_NONE, 1, G_TYPE_UINT); + signals [DEFAULT_SINK_CHANGED] = + g_signal_new ("default-sink-changed", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (GvcMixerControlClass, default_sink_changed), + NULL, NULL, NULL, + G_TYPE_NONE, 1, G_TYPE_UINT); + signals [DEFAULT_SOURCE_CHANGED] = + g_signal_new ("default-source-changed", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (GvcMixerControlClass, default_source_changed), + NULL, NULL, NULL, + G_TYPE_NONE, 1, G_TYPE_UINT); + signals [ACTIVE_OUTPUT_UPDATE] = + g_signal_new ("active-output-update", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (GvcMixerControlClass, active_output_update), + NULL, NULL, NULL, + G_TYPE_NONE, 1, G_TYPE_UINT); + signals [ACTIVE_INPUT_UPDATE] = + g_signal_new ("active-input-update", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (GvcMixerControlClass, active_input_update), + NULL, NULL, NULL, + G_TYPE_NONE, 1, G_TYPE_UINT); + signals [OUTPUT_ADDED] = + g_signal_new ("output-added", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (GvcMixerControlClass, output_added), + NULL, NULL, NULL, + G_TYPE_NONE, 1, G_TYPE_UINT); + signals [INPUT_ADDED] = + g_signal_new ("input-added", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (GvcMixerControlClass, input_added), + NULL, NULL, NULL, + G_TYPE_NONE, 1, G_TYPE_UINT); + signals [OUTPUT_REMOVED] = + g_signal_new ("output-removed", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (GvcMixerControlClass, output_removed), + NULL, NULL, NULL, + G_TYPE_NONE, 1, G_TYPE_UINT); + signals [INPUT_REMOVED] = + g_signal_new ("input-removed", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (GvcMixerControlClass, input_removed), + NULL, NULL, NULL, + G_TYPE_NONE, 1, G_TYPE_UINT); +} + + +static void +gvc_mixer_control_init (GvcMixerControl *control) +{ + control->priv = gvc_mixer_control_get_instance_private (control); + + control->priv->pa_mainloop = pa_glib_mainloop_new (g_main_context_default ()); + g_assert (control->priv->pa_mainloop); + + control->priv->pa_api = pa_glib_mainloop_get_api (control->priv->pa_mainloop); + g_assert (control->priv->pa_api); + + control->priv->all_streams = g_hash_table_new_full (NULL, NULL, NULL, (GDestroyNotify)g_object_unref); + control->priv->sinks = g_hash_table_new_full (NULL, NULL, NULL, (GDestroyNotify)g_object_unref); + control->priv->sources = g_hash_table_new_full (NULL, NULL, NULL, (GDestroyNotify)g_object_unref); + control->priv->sink_inputs = g_hash_table_new_full (NULL, NULL, NULL, (GDestroyNotify)g_object_unref); + control->priv->source_outputs = g_hash_table_new_full (NULL, NULL, NULL, (GDestroyNotify)g_object_unref); + control->priv->cards = g_hash_table_new_full (NULL, NULL, NULL, (GDestroyNotify)g_object_unref); + control->priv->ui_outputs = g_hash_table_new_full (NULL, NULL, NULL, (GDestroyNotify)g_object_unref); + control->priv->ui_inputs = g_hash_table_new_full (NULL, NULL, NULL, (GDestroyNotify)g_object_unref); + + control->priv->clients = g_hash_table_new_full (NULL, NULL, NULL, (GDestroyNotify)g_free); + +#ifdef HAVE_ALSA + control->priv->headset_card = -1; +#endif /* HAVE_ALSA */ + + control->priv->state = GVC_STATE_CLOSED; +} + +static void +gvc_mixer_control_finalize (GObject *object) +{ + GvcMixerControl *mixer_control; + + g_return_if_fail (object != NULL); + g_return_if_fail (GVC_IS_MIXER_CONTROL (object)); + + mixer_control = GVC_MIXER_CONTROL (object); + g_free (mixer_control->priv->name); + mixer_control->priv->name = NULL; + + g_return_if_fail (mixer_control->priv != NULL); + G_OBJECT_CLASS (gvc_mixer_control_parent_class)->finalize (object); +} + +GvcMixerControl * +gvc_mixer_control_new (const char *name) +{ + GObject *control; + control = g_object_new (GVC_TYPE_MIXER_CONTROL, + "name", name, + NULL); + return GVC_MIXER_CONTROL (control); +} + +gdouble +gvc_mixer_control_get_vol_max_norm (GvcMixerControl *control) +{ + g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), 0); + return (gdouble) PA_VOLUME_NORM; +} + +gdouble +gvc_mixer_control_get_vol_max_amplified (GvcMixerControl *control) +{ + g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), 0); + return (gdouble) PA_VOLUME_UI_MAX; +} diff --git a/subprojects/gvc/gvc-mixer-control.h b/subprojects/gvc/gvc-mixer-control.h new file mode 100644 index 0000000..8137849 --- /dev/null +++ b/subprojects/gvc/gvc-mixer-control.h @@ -0,0 +1,155 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2008 Red Hat, Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * + */ + +#ifndef __GVC_MIXER_CONTROL_H +#define __GVC_MIXER_CONTROL_H + +#include <glib-object.h> +#include "gvc-mixer-stream.h" +#include "gvc-mixer-card.h" +#include "gvc-mixer-ui-device.h" + +G_BEGIN_DECLS + +typedef enum +{ + GVC_STATE_CLOSED, + GVC_STATE_READY, + GVC_STATE_CONNECTING, + GVC_STATE_FAILED +} GvcMixerControlState; + +typedef enum +{ + GVC_HEADSET_PORT_CHOICE_NONE = 0, + GVC_HEADSET_PORT_CHOICE_HEADPHONES = 1 << 0, + GVC_HEADSET_PORT_CHOICE_HEADSET = 1 << 1, + GVC_HEADSET_PORT_CHOICE_MIC = 1 << 2 +} GvcHeadsetPortChoice; + +#define GVC_TYPE_MIXER_CONTROL (gvc_mixer_control_get_type ()) +#define GVC_MIXER_CONTROL(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), GVC_TYPE_MIXER_CONTROL, GvcMixerControl)) +#define GVC_MIXER_CONTROL_CLASS(k) (G_TYPE_CHECK_CLASS_CAST((k), GVC_TYPE_MIXER_CONTROL, GvcMixerControlClass)) +#define GVC_IS_MIXER_CONTROL(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), GVC_TYPE_MIXER_CONTROL)) +#define GVC_IS_MIXER_CONTROL_CLASS(k) (G_TYPE_CHECK_CLASS_TYPE ((k), GVC_TYPE_MIXER_CONTROL)) +#define GVC_MIXER_CONTROL_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), GVC_TYPE_MIXER_CONTROL, GvcMixerControlClass)) + +typedef struct GvcMixerControlPrivate GvcMixerControlPrivate; + +typedef struct +{ + GObject parent; + GvcMixerControlPrivate *priv; +} GvcMixerControl; + +typedef struct +{ + GObjectClass parent_class; + + void (*state_changed) (GvcMixerControl *control, + GvcMixerControlState new_state); + void (*stream_added) (GvcMixerControl *control, + guint id); + void (*stream_changed) (GvcMixerControl *control, + guint id); + void (*stream_removed) (GvcMixerControl *control, + guint id); + void (*card_added) (GvcMixerControl *control, + guint id); + void (*card_removed) (GvcMixerControl *control, + guint id); + void (*default_sink_changed) (GvcMixerControl *control, + guint id); + void (*default_source_changed) (GvcMixerControl *control, + guint id); + void (*active_output_update) (GvcMixerControl *control, + guint id); + void (*active_input_update) (GvcMixerControl *control, + guint id); + void (*output_added) (GvcMixerControl *control, + guint id); + void (*input_added) (GvcMixerControl *control, + guint id); + void (*output_removed) (GvcMixerControl *control, + guint id); + void (*input_removed) (GvcMixerControl *control, + guint id); + void (*audio_device_selection_needed) + (GvcMixerControl *control, + guint id, + gboolean show_dialog, + GvcHeadsetPortChoice choices); +} GvcMixerControlClass; + +GType gvc_mixer_control_get_type (void); + +GvcMixerControl * gvc_mixer_control_new (const char *name); + +gboolean gvc_mixer_control_open (GvcMixerControl *control); +gboolean gvc_mixer_control_close (GvcMixerControl *control); + +GSList * gvc_mixer_control_get_cards (GvcMixerControl *control); +GSList * gvc_mixer_control_get_streams (GvcMixerControl *control); +GSList * gvc_mixer_control_get_sinks (GvcMixerControl *control); +GSList * gvc_mixer_control_get_sources (GvcMixerControl *control); +GSList * gvc_mixer_control_get_sink_inputs (GvcMixerControl *control); +GSList * gvc_mixer_control_get_source_outputs (GvcMixerControl *control); + +GvcMixerStream * gvc_mixer_control_lookup_stream_id (GvcMixerControl *control, + guint id); +GvcMixerCard * gvc_mixer_control_lookup_card_id (GvcMixerControl *control, + guint id); +GvcMixerUIDevice * gvc_mixer_control_lookup_output_id (GvcMixerControl *control, + guint id); +GvcMixerUIDevice * gvc_mixer_control_lookup_input_id (GvcMixerControl *control, + guint id); +GvcMixerUIDevice * gvc_mixer_control_lookup_device_from_stream (GvcMixerControl *control, + GvcMixerStream *stream); + +GvcMixerStream * gvc_mixer_control_get_default_sink (GvcMixerControl *control); +GvcMixerStream * gvc_mixer_control_get_default_source (GvcMixerControl *control); +GvcMixerStream * gvc_mixer_control_get_event_sink_input (GvcMixerControl *control); + +gboolean gvc_mixer_control_set_default_sink (GvcMixerControl *control, + GvcMixerStream *stream); +gboolean gvc_mixer_control_set_default_source (GvcMixerControl *control, + GvcMixerStream *stream); + +gdouble gvc_mixer_control_get_vol_max_norm (GvcMixerControl *control); +gdouble gvc_mixer_control_get_vol_max_amplified (GvcMixerControl *control); +void gvc_mixer_control_change_output (GvcMixerControl *control, + GvcMixerUIDevice* output); +void gvc_mixer_control_change_input (GvcMixerControl *control, + GvcMixerUIDevice* input); +GvcMixerStream* gvc_mixer_control_get_stream_from_device (GvcMixerControl *control, + GvcMixerUIDevice *device); +gboolean gvc_mixer_control_change_profile_on_selected_device (GvcMixerControl *control, + GvcMixerUIDevice *device, + const gchar* profile); + +void gvc_mixer_control_set_headset_port (GvcMixerControl *control, + guint id, + GvcHeadsetPortChoice choices); + +GvcMixerControlState gvc_mixer_control_get_state (GvcMixerControl *control); + +G_END_DECLS + +#endif /* __GVC_MIXER_CONTROL_H */ diff --git a/subprojects/gvc/gvc-mixer-event-role.c b/subprojects/gvc/gvc-mixer-event-role.c new file mode 100644 index 0000000..272edb0 --- /dev/null +++ b/subprojects/gvc/gvc-mixer-event-role.c @@ -0,0 +1,229 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2008 William Jon McCann + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * + */ + +#include "config.h" + +#include <stdlib.h> +#include <stdio.h> +#include <unistd.h> + +#include <glib.h> +#include <glib/gi18n-lib.h> + +#include <pulse/pulseaudio.h> +#include <pulse/ext-stream-restore.h> + +#include "gvc-mixer-event-role.h" +#include "gvc-mixer-stream-private.h" +#include "gvc-channel-map-private.h" + +struct GvcMixerEventRolePrivate +{ + char *device; +}; + +enum +{ + PROP_0, + PROP_DEVICE, + N_PROPS +}; +static GParamSpec *obj_props[N_PROPS] = { NULL, }; + +static void gvc_mixer_event_role_finalize (GObject *object); + +G_DEFINE_TYPE_WITH_PRIVATE (GvcMixerEventRole, gvc_mixer_event_role, GVC_TYPE_MIXER_STREAM) + +static gboolean +update_settings (GvcMixerEventRole *role, + gboolean is_muted, + gpointer *op) +{ + pa_operation *o; + const GvcChannelMap *map; + pa_context *context; + pa_ext_stream_restore_info info; + + map = gvc_mixer_stream_get_channel_map (GVC_MIXER_STREAM(role)); + + info.volume = *gvc_channel_map_get_cvolume(map); + info.name = "sink-input-by-media-role:event"; + info.channel_map = *gvc_channel_map_get_pa_channel_map(map); + info.device = role->priv->device; + info.mute = is_muted; + + context = gvc_mixer_stream_get_pa_context (GVC_MIXER_STREAM (role)); + + o = pa_ext_stream_restore_write (context, + PA_UPDATE_REPLACE, + &info, + 1, + TRUE, + NULL, + NULL); + + if (o == NULL) { + g_warning ("pa_ext_stream_restore_write() failed"); + return FALSE; + } + + if (op != NULL) + *op = o; + + return TRUE; +} + +static gboolean +gvc_mixer_event_role_push_volume (GvcMixerStream *stream, gpointer *op) +{ + return update_settings (GVC_MIXER_EVENT_ROLE (stream), + gvc_mixer_stream_get_is_muted (stream), op); +} + +static gboolean +gvc_mixer_event_role_change_is_muted (GvcMixerStream *stream, + gboolean is_muted) +{ + /* Apply change straight away so that we don't get a race with + * gvc_mixer_event_role_push_volume(). + * See https://bugs.freedesktop.org/show_bug.cgi?id=51413 */ + gvc_mixer_stream_set_is_muted (stream, is_muted); + return update_settings (GVC_MIXER_EVENT_ROLE (stream), + is_muted, NULL); +} + +static gboolean +gvc_mixer_event_role_set_device (GvcMixerEventRole *role, + const char *device) +{ + g_return_val_if_fail (GVC_IS_MIXER_EVENT_ROLE (role), FALSE); + + g_free (role->priv->device); + role->priv->device = g_strdup (device); + g_object_notify_by_pspec (G_OBJECT (role), obj_props[PROP_DEVICE]); + + return TRUE; +} + +static void +gvc_mixer_event_role_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + GvcMixerEventRole *self = GVC_MIXER_EVENT_ROLE (object); + + switch (prop_id) { + case PROP_DEVICE: + gvc_mixer_event_role_set_device (self, g_value_get_string (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +gvc_mixer_event_role_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + GvcMixerEventRole *self = GVC_MIXER_EVENT_ROLE (object); + + switch (prop_id) { + case PROP_DEVICE: + g_value_set_string (value, self->priv->device); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +gvc_mixer_event_role_class_init (GvcMixerEventRoleClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GvcMixerStreamClass *stream_class = GVC_MIXER_STREAM_CLASS (klass); + + object_class->finalize = gvc_mixer_event_role_finalize; + object_class->set_property = gvc_mixer_event_role_set_property; + object_class->get_property = gvc_mixer_event_role_get_property; + + stream_class->push_volume = gvc_mixer_event_role_push_volume; + stream_class->change_is_muted = gvc_mixer_event_role_change_is_muted; + + obj_props[PROP_DEVICE] = g_param_spec_string ("device", + "Device", + "Device", + NULL, + G_PARAM_READWRITE|G_PARAM_CONSTRUCT|G_PARAM_STATIC_STRINGS); + g_object_class_install_properties (object_class, N_PROPS, obj_props); +} + +static void +gvc_mixer_event_role_init (GvcMixerEventRole *event_role) +{ + event_role->priv = gvc_mixer_event_role_get_instance_private (event_role); + +} + +static void +gvc_mixer_event_role_finalize (GObject *object) +{ + GvcMixerEventRole *mixer_event_role; + + g_return_if_fail (object != NULL); + g_return_if_fail (GVC_IS_MIXER_EVENT_ROLE (object)); + + mixer_event_role = GVC_MIXER_EVENT_ROLE (object); + + g_return_if_fail (mixer_event_role->priv != NULL); + + g_free (mixer_event_role->priv->device); + + G_OBJECT_CLASS (gvc_mixer_event_role_parent_class)->finalize (object); +} + +/** + * gvc_mixer_event_role_new: (skip) + * @context: + * @device: + * @channel_map: + * + * Returns: + */ +GvcMixerStream * +gvc_mixer_event_role_new (pa_context *context, + const char *device, + GvcChannelMap *channel_map) +{ + GObject *object; + + object = g_object_new (GVC_TYPE_MIXER_EVENT_ROLE, + "pa-context", context, + "index", 0, + "device", device, + "channel-map", channel_map, + NULL); + + return GVC_MIXER_STREAM (object); +} diff --git a/subprojects/gvc/gvc-mixer-event-role.h b/subprojects/gvc/gvc-mixer-event-role.h new file mode 100644 index 0000000..ab4c509 --- /dev/null +++ b/subprojects/gvc/gvc-mixer-event-role.h @@ -0,0 +1,57 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2008 Red Hat, Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * + */ + +#ifndef __GVC_MIXER_EVENT_ROLE_H +#define __GVC_MIXER_EVENT_ROLE_H + +#include <glib-object.h> +#include "gvc-mixer-stream.h" + +G_BEGIN_DECLS + +#define GVC_TYPE_MIXER_EVENT_ROLE (gvc_mixer_event_role_get_type ()) +#define GVC_MIXER_EVENT_ROLE(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), GVC_TYPE_MIXER_EVENT_ROLE, GvcMixerEventRole)) +#define GVC_MIXER_EVENT_ROLE_CLASS(k) (G_TYPE_CHECK_CLASS_CAST((k), GVC_TYPE_MIXER_EVENT_ROLE, GvcMixerEventRoleClass)) +#define GVC_IS_MIXER_EVENT_ROLE(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), GVC_TYPE_MIXER_EVENT_ROLE)) +#define GVC_IS_MIXER_EVENT_ROLE_CLASS(k) (G_TYPE_CHECK_CLASS_TYPE ((k), GVC_TYPE_MIXER_EVENT_ROLE)) +#define GVC_MIXER_EVENT_ROLE_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), GVC_TYPE_MIXER_EVENT_ROLE, GvcMixerEventRoleClass)) + +typedef struct GvcMixerEventRolePrivate GvcMixerEventRolePrivate; + +typedef struct +{ + GvcMixerStream parent; + GvcMixerEventRolePrivate *priv; +} GvcMixerEventRole; + +typedef struct +{ + GvcMixerStreamClass parent_class; +} GvcMixerEventRoleClass; + +GType gvc_mixer_event_role_get_type (void); + +GvcMixerStream * gvc_mixer_event_role_new (pa_context *context, + const char *device, + GvcChannelMap *channel_map); + +G_END_DECLS + +#endif /* __GVC_MIXER_EVENT_ROLE_H */ diff --git a/subprojects/gvc/gvc-mixer-sink-input.c b/subprojects/gvc/gvc-mixer-sink-input.c new file mode 100644 index 0000000..a359daf --- /dev/null +++ b/subprojects/gvc/gvc-mixer-sink-input.c @@ -0,0 +1,159 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2008 William Jon McCann + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * + */ + +#include "config.h" + +#include <stdlib.h> +#include <stdio.h> +#include <unistd.h> + +#include <glib.h> +#include <glib/gi18n-lib.h> + +#include <pulse/pulseaudio.h> + +#include "gvc-mixer-sink-input.h" +#include "gvc-mixer-stream-private.h" +#include "gvc-channel-map-private.h" + +struct GvcMixerSinkInputPrivate +{ + gpointer dummy; +}; + +static void gvc_mixer_sink_input_finalize (GObject *object); + +G_DEFINE_TYPE_WITH_PRIVATE (GvcMixerSinkInput, gvc_mixer_sink_input, GVC_TYPE_MIXER_STREAM) + +static gboolean +gvc_mixer_sink_input_push_volume (GvcMixerStream *stream, gpointer *op) +{ + pa_operation *o; + guint index; + const GvcChannelMap *map; + pa_context *context; + const pa_cvolume *cv; + + index = gvc_mixer_stream_get_index (stream); + + map = gvc_mixer_stream_get_channel_map (stream); + + cv = gvc_channel_map_get_cvolume(map); + + context = gvc_mixer_stream_get_pa_context (stream); + + o = pa_context_set_sink_input_volume (context, + index, + cv, + NULL, + NULL); + + if (o == NULL) { + g_warning ("pa_context_set_sink_input_volume() failed"); + return FALSE; + } + + *op = o; + + return TRUE; +} + +static gboolean +gvc_mixer_sink_input_change_is_muted (GvcMixerStream *stream, + gboolean is_muted) +{ + pa_operation *o; + guint index; + pa_context *context; + + index = gvc_mixer_stream_get_index (stream); + context = gvc_mixer_stream_get_pa_context (stream); + + o = pa_context_set_sink_input_mute (context, + index, + is_muted, + NULL, + NULL); + + if (o == NULL) { + g_warning ("pa_context_set_sink_input_mute_by_index() failed"); + return FALSE; + } + + pa_operation_unref(o); + + return TRUE; +} + +static void +gvc_mixer_sink_input_class_init (GvcMixerSinkInputClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GvcMixerStreamClass *stream_class = GVC_MIXER_STREAM_CLASS (klass); + + object_class->finalize = gvc_mixer_sink_input_finalize; + + stream_class->push_volume = gvc_mixer_sink_input_push_volume; + stream_class->change_is_muted = gvc_mixer_sink_input_change_is_muted; +} + +static void +gvc_mixer_sink_input_init (GvcMixerSinkInput *sink_input) +{ + sink_input->priv = gvc_mixer_sink_input_get_instance_private (sink_input); +} + +static void +gvc_mixer_sink_input_finalize (GObject *object) +{ + GvcMixerSinkInput *mixer_sink_input; + + g_return_if_fail (object != NULL); + g_return_if_fail (GVC_IS_MIXER_SINK_INPUT (object)); + + mixer_sink_input = GVC_MIXER_SINK_INPUT (object); + + g_return_if_fail (mixer_sink_input->priv != NULL); + G_OBJECT_CLASS (gvc_mixer_sink_input_parent_class)->finalize (object); +} + +/** + * gvc_mixer_sink_input_new: (skip) + * @context: + * @index: + * @channel_map: + * + * Returns: + */ +GvcMixerStream * +gvc_mixer_sink_input_new (pa_context *context, + guint index, + GvcChannelMap *channel_map) +{ + GObject *object; + + object = g_object_new (GVC_TYPE_MIXER_SINK_INPUT, + "pa-context", context, + "index", index, + "channel-map", channel_map, + NULL); + + return GVC_MIXER_STREAM (object); +} diff --git a/subprojects/gvc/gvc-mixer-sink-input.h b/subprojects/gvc/gvc-mixer-sink-input.h new file mode 100644 index 0000000..17bf127 --- /dev/null +++ b/subprojects/gvc/gvc-mixer-sink-input.h @@ -0,0 +1,57 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2008 Red Hat, Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * + */ + +#ifndef __GVC_MIXER_SINK_INPUT_H +#define __GVC_MIXER_SINK_INPUT_H + +#include <glib-object.h> +#include "gvc-mixer-stream.h" + +G_BEGIN_DECLS + +#define GVC_TYPE_MIXER_SINK_INPUT (gvc_mixer_sink_input_get_type ()) +#define GVC_MIXER_SINK_INPUT(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), GVC_TYPE_MIXER_SINK_INPUT, GvcMixerSinkInput)) +#define GVC_MIXER_SINK_INPUT_CLASS(k) (G_TYPE_CHECK_CLASS_CAST((k), GVC_TYPE_MIXER_SINK_INPUT, GvcMixerSinkInputClass)) +#define GVC_IS_MIXER_SINK_INPUT(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), GVC_TYPE_MIXER_SINK_INPUT)) +#define GVC_IS_MIXER_SINK_INPUT_CLASS(k) (G_TYPE_CHECK_CLASS_TYPE ((k), GVC_TYPE_MIXER_SINK_INPUT)) +#define GVC_MIXER_SINK_INPUT_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), GVC_TYPE_MIXER_SINK_INPUT, GvcMixerSinkInputClass)) + +typedef struct GvcMixerSinkInputPrivate GvcMixerSinkInputPrivate; + +typedef struct +{ + GvcMixerStream parent; + GvcMixerSinkInputPrivate *priv; +} GvcMixerSinkInput; + +typedef struct +{ + GvcMixerStreamClass parent_class; +} GvcMixerSinkInputClass; + +GType gvc_mixer_sink_input_get_type (void); + +GvcMixerStream * gvc_mixer_sink_input_new (pa_context *context, + guint index, + GvcChannelMap *channel_map); + +G_END_DECLS + +#endif /* __GVC_MIXER_SINK_INPUT_H */ diff --git a/subprojects/gvc/gvc-mixer-sink.c b/subprojects/gvc/gvc-mixer-sink.c new file mode 100644 index 0000000..a6115c6 --- /dev/null +++ b/subprojects/gvc/gvc-mixer-sink.c @@ -0,0 +1,189 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2008 William Jon McCann + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * + */ + +#include "config.h" + +#include <stdlib.h> +#include <stdio.h> +#include <unistd.h> + +#include <glib.h> +#include <glib/gi18n-lib.h> + +#include <pulse/pulseaudio.h> + +#include "gvc-mixer-sink.h" +#include "gvc-mixer-stream-private.h" +#include "gvc-channel-map-private.h" + +struct GvcMixerSinkPrivate +{ + gpointer dummy; +}; + +static void gvc_mixer_sink_finalize (GObject *object); + +G_DEFINE_TYPE_WITH_PRIVATE (GvcMixerSink, gvc_mixer_sink, GVC_TYPE_MIXER_STREAM) + +static gboolean +gvc_mixer_sink_push_volume (GvcMixerStream *stream, gpointer *op) +{ + pa_operation *o; + guint index; + const GvcChannelMap *map; + pa_context *context; + const pa_cvolume *cv; + + index = gvc_mixer_stream_get_index (stream); + + map = gvc_mixer_stream_get_channel_map (stream); + + /* set the volume */ + cv = gvc_channel_map_get_cvolume(map); + + context = gvc_mixer_stream_get_pa_context (stream); + + o = pa_context_set_sink_volume_by_index (context, + index, + cv, + NULL, + NULL); + + if (o == NULL) { + g_warning ("pa_context_set_sink_volume_by_index() failed: %s", pa_strerror(pa_context_errno(context))); + return FALSE; + } + + *op = o; + + return TRUE; +} + +static gboolean +gvc_mixer_sink_change_is_muted (GvcMixerStream *stream, + gboolean is_muted) +{ + pa_operation *o; + guint index; + pa_context *context; + + index = gvc_mixer_stream_get_index (stream); + context = gvc_mixer_stream_get_pa_context (stream); + + o = pa_context_set_sink_mute_by_index (context, + index, + is_muted, + NULL, + NULL); + + if (o == NULL) { + g_warning ("pa_context_set_sink_mute_by_index() failed: %s", pa_strerror(pa_context_errno(context))); + return FALSE; + } + + pa_operation_unref(o); + + return TRUE; +} + +static gboolean +gvc_mixer_sink_change_port (GvcMixerStream *stream, + const char *port) +{ + pa_operation *o; + guint index; + pa_context *context; + + index = gvc_mixer_stream_get_index (stream); + context = gvc_mixer_stream_get_pa_context (stream); + + o = pa_context_set_sink_port_by_index (context, + index, + port, + NULL, + NULL); + + if (o == NULL) { + g_warning ("pa_context_set_sink_port_by_index() failed: %s", pa_strerror(pa_context_errno(context))); + return FALSE; + } + + pa_operation_unref(o); + + return TRUE; +} + +static void +gvc_mixer_sink_class_init (GvcMixerSinkClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GvcMixerStreamClass *stream_class = GVC_MIXER_STREAM_CLASS (klass); + + object_class->finalize = gvc_mixer_sink_finalize; + + stream_class->push_volume = gvc_mixer_sink_push_volume; + stream_class->change_port = gvc_mixer_sink_change_port; + stream_class->change_is_muted = gvc_mixer_sink_change_is_muted; +} + +static void +gvc_mixer_sink_init (GvcMixerSink *sink) +{ + sink->priv = gvc_mixer_sink_get_instance_private (sink); +} + +static void +gvc_mixer_sink_finalize (GObject *object) +{ + GvcMixerSink *mixer_sink; + + g_return_if_fail (object != NULL); + g_return_if_fail (GVC_IS_MIXER_SINK (object)); + + mixer_sink = GVC_MIXER_SINK (object); + + g_return_if_fail (mixer_sink->priv != NULL); + G_OBJECT_CLASS (gvc_mixer_sink_parent_class)->finalize (object); +} + +/** + * gvc_mixer_sink_new: (skip) + * @context: + * @index: + * @channel_map: + * + * Returns: + */ +GvcMixerStream * +gvc_mixer_sink_new (pa_context *context, + guint index, + GvcChannelMap *channel_map) + +{ + GObject *object; + + object = g_object_new (GVC_TYPE_MIXER_SINK, + "pa-context", context, + "index", index, + "channel-map", channel_map, + NULL); + + return GVC_MIXER_STREAM (object); +} diff --git a/subprojects/gvc/gvc-mixer-sink.h b/subprojects/gvc/gvc-mixer-sink.h new file mode 100644 index 0000000..3fbe291 --- /dev/null +++ b/subprojects/gvc/gvc-mixer-sink.h @@ -0,0 +1,57 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2008 Red Hat, Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * + */ + +#ifndef __GVC_MIXER_SINK_H +#define __GVC_MIXER_SINK_H + +#include <glib-object.h> +#include "gvc-mixer-stream.h" + +G_BEGIN_DECLS + +#define GVC_TYPE_MIXER_SINK (gvc_mixer_sink_get_type ()) +#define GVC_MIXER_SINK(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), GVC_TYPE_MIXER_SINK, GvcMixerSink)) +#define GVC_MIXER_SINK_CLASS(k) (G_TYPE_CHECK_CLASS_CAST((k), GVC_TYPE_MIXER_SINK, GvcMixerSinkClass)) +#define GVC_IS_MIXER_SINK(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), GVC_TYPE_MIXER_SINK)) +#define GVC_IS_MIXER_SINK_CLASS(k) (G_TYPE_CHECK_CLASS_TYPE ((k), GVC_TYPE_MIXER_SINK)) +#define GVC_MIXER_SINK_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), GVC_TYPE_MIXER_SINK, GvcMixerSinkClass)) + +typedef struct GvcMixerSinkPrivate GvcMixerSinkPrivate; + +typedef struct +{ + GvcMixerStream parent; + GvcMixerSinkPrivate *priv; +} GvcMixerSink; + +typedef struct +{ + GvcMixerStreamClass parent_class; +} GvcMixerSinkClass; + +GType gvc_mixer_sink_get_type (void); + +GvcMixerStream * gvc_mixer_sink_new (pa_context *context, + guint index, + GvcChannelMap *channel_map); + +G_END_DECLS + +#endif /* __GVC_MIXER_SINK_H */ diff --git a/subprojects/gvc/gvc-mixer-source-output.c b/subprojects/gvc/gvc-mixer-source-output.c new file mode 100644 index 0000000..c4a275a --- /dev/null +++ b/subprojects/gvc/gvc-mixer-source-output.c @@ -0,0 +1,160 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2008 William Jon McCann + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * + */ + +#include "config.h" + +#include <stdlib.h> +#include <stdio.h> +#include <unistd.h> + +#include <glib.h> +#include <glib/gi18n-lib.h> + +#include <pulse/pulseaudio.h> + +#include "gvc-mixer-source-output.h" +#include "gvc-mixer-stream-private.h" +#include "gvc-channel-map-private.h" + +struct GvcMixerSourceOutputPrivate +{ + gpointer dummy; +}; + +static void gvc_mixer_source_output_finalize (GObject *object); + +G_DEFINE_TYPE_WITH_PRIVATE (GvcMixerSourceOutput, gvc_mixer_source_output, GVC_TYPE_MIXER_STREAM) + +static gboolean +gvc_mixer_source_output_push_volume (GvcMixerStream *stream, gpointer *op) +{ + pa_operation *o; + guint index; + const GvcChannelMap *map; + pa_context *context; + const pa_cvolume *cv; + + index = gvc_mixer_stream_get_index (stream); + + map = gvc_mixer_stream_get_channel_map (stream); + + cv = gvc_channel_map_get_cvolume(map); + + context = gvc_mixer_stream_get_pa_context (stream); + + o = pa_context_set_source_output_volume (context, + index, + cv, + NULL, + NULL); + + if (o == NULL) { + g_warning ("pa_context_set_source_output_volume() failed"); + return FALSE; + } + + *op = o; + + return TRUE; +} + +static gboolean +gvc_mixer_source_output_change_is_muted (GvcMixerStream *stream, + gboolean is_muted) +{ + pa_operation *o; + guint index; + pa_context *context; + + index = gvc_mixer_stream_get_index (stream); + context = gvc_mixer_stream_get_pa_context (stream); + + o = pa_context_set_source_output_mute (context, + index, + is_muted, + NULL, + NULL); + + if (o == NULL) { + g_warning ("pa_context_set_source_output_mute_by_index() failed"); + return FALSE; + } + + pa_operation_unref(o); + + return TRUE; +} + +static void +gvc_mixer_source_output_class_init (GvcMixerSourceOutputClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GvcMixerStreamClass *stream_class = GVC_MIXER_STREAM_CLASS (klass); + + object_class->finalize = gvc_mixer_source_output_finalize; + + stream_class->push_volume = gvc_mixer_source_output_push_volume; + stream_class->change_is_muted = gvc_mixer_source_output_change_is_muted; +} + +static void +gvc_mixer_source_output_init (GvcMixerSourceOutput *source_output) +{ + source_output->priv = gvc_mixer_source_output_get_instance_private (source_output); + +} + +static void +gvc_mixer_source_output_finalize (GObject *object) +{ + GvcMixerSourceOutput *mixer_source_output; + + g_return_if_fail (object != NULL); + g_return_if_fail (GVC_IS_MIXER_SOURCE_OUTPUT (object)); + + mixer_source_output = GVC_MIXER_SOURCE_OUTPUT (object); + + g_return_if_fail (mixer_source_output->priv != NULL); + G_OBJECT_CLASS (gvc_mixer_source_output_parent_class)->finalize (object); +} + +/** + * gvc_mixer_source_output_new: (skip) + * @context: + * @index: + * @channel_map: + * + * Returns: + */ +GvcMixerStream * +gvc_mixer_source_output_new (pa_context *context, + guint index, + GvcChannelMap *channel_map) +{ + GObject *object; + + object = g_object_new (GVC_TYPE_MIXER_SOURCE_OUTPUT, + "pa-context", context, + "index", index, + "channel-map", channel_map, + NULL); + + return GVC_MIXER_STREAM (object); +} diff --git a/subprojects/gvc/gvc-mixer-source-output.h b/subprojects/gvc/gvc-mixer-source-output.h new file mode 100644 index 0000000..4d9a6d6 --- /dev/null +++ b/subprojects/gvc/gvc-mixer-source-output.h @@ -0,0 +1,57 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2008 Red Hat, Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * + */ + +#ifndef __GVC_MIXER_SOURCE_OUTPUT_H +#define __GVC_MIXER_SOURCE_OUTPUT_H + +#include <glib-object.h> +#include "gvc-mixer-stream.h" + +G_BEGIN_DECLS + +#define GVC_TYPE_MIXER_SOURCE_OUTPUT (gvc_mixer_source_output_get_type ()) +#define GVC_MIXER_SOURCE_OUTPUT(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), GVC_TYPE_MIXER_SOURCE_OUTPUT, GvcMixerSourceOutput)) +#define GVC_MIXER_SOURCE_OUTPUT_CLASS(k) (G_TYPE_CHECK_CLASS_CAST((k), GVC_TYPE_MIXER_SOURCE_OUTPUT, GvcMixerSourceOutputClass)) +#define GVC_IS_MIXER_SOURCE_OUTPUT(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), GVC_TYPE_MIXER_SOURCE_OUTPUT)) +#define GVC_IS_MIXER_SOURCE_OUTPUT_CLASS(k) (G_TYPE_CHECK_CLASS_TYPE ((k), GVC_TYPE_MIXER_SOURCE_OUTPUT)) +#define GVC_MIXER_SOURCE_OUTPUT_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), GVC_TYPE_MIXER_SOURCE_OUTPUT, GvcMixerSourceOutputClass)) + +typedef struct GvcMixerSourceOutputPrivate GvcMixerSourceOutputPrivate; + +typedef struct +{ + GvcMixerStream parent; + GvcMixerSourceOutputPrivate *priv; +} GvcMixerSourceOutput; + +typedef struct +{ + GvcMixerStreamClass parent_class; +} GvcMixerSourceOutputClass; + +GType gvc_mixer_source_output_get_type (void); + +GvcMixerStream * gvc_mixer_source_output_new (pa_context *context, + guint index, + GvcChannelMap *channel_map); + +G_END_DECLS + +#endif /* __GVC_MIXER_SOURCE_OUTPUT_H */ diff --git a/subprojects/gvc/gvc-mixer-source.c b/subprojects/gvc/gvc-mixer-source.c new file mode 100644 index 0000000..434eec3 --- /dev/null +++ b/subprojects/gvc/gvc-mixer-source.c @@ -0,0 +1,189 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2008 William Jon McCann + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * + */ + +#include "config.h" + +#include <stdlib.h> +#include <stdio.h> +#include <unistd.h> + +#include <glib.h> +#include <glib/gi18n-lib.h> + +#include <pulse/pulseaudio.h> + +#include "gvc-mixer-source.h" +#include "gvc-mixer-stream-private.h" +#include "gvc-channel-map-private.h" + +struct GvcMixerSourcePrivate +{ + gpointer dummy; +}; + +static void gvc_mixer_source_finalize (GObject *object); + +G_DEFINE_TYPE_WITH_PRIVATE (GvcMixerSource, gvc_mixer_source, GVC_TYPE_MIXER_STREAM) + +static gboolean +gvc_mixer_source_push_volume (GvcMixerStream *stream, gpointer *op) +{ + pa_operation *o; + guint index; + const GvcChannelMap *map; + pa_context *context; + const pa_cvolume *cv; + + index = gvc_mixer_stream_get_index (stream); + + map = gvc_mixer_stream_get_channel_map (stream); + + /* set the volume */ + cv = gvc_channel_map_get_cvolume (map); + + context = gvc_mixer_stream_get_pa_context (stream); + + o = pa_context_set_source_volume_by_index (context, + index, + cv, + NULL, + NULL); + + if (o == NULL) { + g_warning ("pa_context_set_source_volume_by_index() failed: %s", pa_strerror(pa_context_errno(context))); + return FALSE; + } + + *op = o; + + return TRUE; +} + +static gboolean +gvc_mixer_source_change_is_muted (GvcMixerStream *stream, + gboolean is_muted) +{ + pa_operation *o; + guint index; + pa_context *context; + + index = gvc_mixer_stream_get_index (stream); + context = gvc_mixer_stream_get_pa_context (stream); + + o = pa_context_set_source_mute_by_index (context, + index, + is_muted, + NULL, + NULL); + + if (o == NULL) { + g_warning ("pa_context_set_source_mute_by_index() failed: %s", pa_strerror(pa_context_errno(context))); + return FALSE; + } + + pa_operation_unref(o); + + return TRUE; +} + +static gboolean +gvc_mixer_source_change_port (GvcMixerStream *stream, + const char *port) +{ + pa_operation *o; + guint index; + pa_context *context; + + index = gvc_mixer_stream_get_index (stream); + context = gvc_mixer_stream_get_pa_context (stream); + + o = pa_context_set_source_port_by_index (context, + index, + port, + NULL, + NULL); + + if (o == NULL) { + g_warning ("pa_context_set_source_port_by_index() failed: %s", pa_strerror(pa_context_errno(context))); + return FALSE; + } + + pa_operation_unref(o); + + return TRUE; +} + +static void +gvc_mixer_source_class_init (GvcMixerSourceClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GvcMixerStreamClass *stream_class = GVC_MIXER_STREAM_CLASS (klass); + + object_class->finalize = gvc_mixer_source_finalize; + + stream_class->push_volume = gvc_mixer_source_push_volume; + stream_class->change_is_muted = gvc_mixer_source_change_is_muted; + stream_class->change_port = gvc_mixer_source_change_port; +} + +static void +gvc_mixer_source_init (GvcMixerSource *source) +{ + source->priv = gvc_mixer_source_get_instance_private (source); +} + +static void +gvc_mixer_source_finalize (GObject *object) +{ + GvcMixerSource *mixer_source; + + g_return_if_fail (object != NULL); + g_return_if_fail (GVC_IS_MIXER_SOURCE (object)); + + mixer_source = GVC_MIXER_SOURCE (object); + + g_return_if_fail (mixer_source->priv != NULL); + G_OBJECT_CLASS (gvc_mixer_source_parent_class)->finalize (object); +} + +/** + * gvc_mixer_source_new: (skip) + * @context: + * @index: + * @channel_map: + * + * Returns: + */ +GvcMixerStream * +gvc_mixer_source_new (pa_context *context, + guint index, + GvcChannelMap *channel_map) + +{ + GObject *object; + + object = g_object_new (GVC_TYPE_MIXER_SOURCE, + "pa-context", context, + "index", index, + "channel-map", channel_map, + NULL); + + return GVC_MIXER_STREAM (object); +} diff --git a/subprojects/gvc/gvc-mixer-source.h b/subprojects/gvc/gvc-mixer-source.h new file mode 100644 index 0000000..bdffe8c --- /dev/null +++ b/subprojects/gvc/gvc-mixer-source.h @@ -0,0 +1,57 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2008 Red Hat, Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * + */ + +#ifndef __GVC_MIXER_SOURCE_H +#define __GVC_MIXER_SOURCE_H + +#include <glib-object.h> +#include "gvc-mixer-stream.h" + +G_BEGIN_DECLS + +#define GVC_TYPE_MIXER_SOURCE (gvc_mixer_source_get_type ()) +#define GVC_MIXER_SOURCE(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), GVC_TYPE_MIXER_SOURCE, GvcMixerSource)) +#define GVC_MIXER_SOURCE_CLASS(k) (G_TYPE_CHECK_CLASS_CAST((k), GVC_TYPE_MIXER_SOURCE, GvcMixerSourceClass)) +#define GVC_IS_MIXER_SOURCE(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), GVC_TYPE_MIXER_SOURCE)) +#define GVC_IS_MIXER_SOURCE_CLASS(k) (G_TYPE_CHECK_CLASS_TYPE ((k), GVC_TYPE_MIXER_SOURCE)) +#define GVC_MIXER_SOURCE_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), GVC_TYPE_MIXER_SOURCE, GvcMixerSourceClass)) + +typedef struct GvcMixerSourcePrivate GvcMixerSourcePrivate; + +typedef struct +{ + GvcMixerStream parent; + GvcMixerSourcePrivate *priv; +} GvcMixerSource; + +typedef struct +{ + GvcMixerStreamClass parent_class; +} GvcMixerSourceClass; + +GType gvc_mixer_source_get_type (void); + +GvcMixerStream * gvc_mixer_source_new (pa_context *context, + guint index, + GvcChannelMap *channel_map); + +G_END_DECLS + +#endif /* __GVC_MIXER_SOURCE_H */ diff --git a/subprojects/gvc/gvc-mixer-stream-private.h b/subprojects/gvc/gvc-mixer-stream-private.h new file mode 100644 index 0000000..b97ecf5 --- /dev/null +++ b/subprojects/gvc/gvc-mixer-stream-private.h @@ -0,0 +1,34 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2008 Red Hat, Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * + */ + +#ifndef __GVC_MIXER_STREAM_PRIVATE_H +#define __GVC_MIXER_STREAM_PRIVATE_H + +#include <glib-object.h> + +#include "gvc-channel-map.h" + +G_BEGIN_DECLS + +pa_context * gvc_mixer_stream_get_pa_context (GvcMixerStream *stream); + +G_END_DECLS + +#endif /* __GVC_MIXER_STREAM_PRIVATE_H */ diff --git a/subprojects/gvc/gvc-mixer-stream.c b/subprojects/gvc/gvc-mixer-stream.c new file mode 100644 index 0000000..f9bcc40 --- /dev/null +++ b/subprojects/gvc/gvc-mixer-stream.c @@ -0,0 +1,1055 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2008 William Jon McCann + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * + */ + +#include "config.h" + +#include <stdlib.h> +#include <stdio.h> +#include <unistd.h> + +#include <glib.h> +#include <glib/gi18n-lib.h> + +#include <pulse/pulseaudio.h> + +#include "gvc-mixer-stream.h" +#include "gvc-mixer-stream-private.h" +#include "gvc-channel-map-private.h" +#include "gvc-enum-types.h" + +static guint32 stream_serial = 1; + +struct GvcMixerStreamPrivate +{ + pa_context *pa_context; + guint id; + guint index; + guint card_index; + GvcChannelMap *channel_map; + char *name; + char *description; + char *application_id; + char *icon_name; + char *form_factor; + char *sysfs_path; + gboolean is_muted; + gboolean can_decibel; + gboolean is_event_stream; + gboolean is_virtual; + pa_volume_t base_volume; + pa_operation *change_volume_op; + char *port; + char *human_port; + GList *ports; + GvcMixerStreamState state; +}; + +enum +{ + PROP_0, + PROP_ID, + PROP_PA_CONTEXT, + PROP_CHANNEL_MAP, + PROP_INDEX, + PROP_NAME, + PROP_DESCRIPTION, + PROP_APPLICATION_ID, + PROP_ICON_NAME, + PROP_FORM_FACTOR, + PROP_SYSFS_PATH, + PROP_VOLUME, + PROP_DECIBEL, + PROP_IS_MUTED, + PROP_CAN_DECIBEL, + PROP_IS_EVENT_STREAM, + PROP_IS_VIRTUAL, + PROP_CARD_INDEX, + PROP_PORT, + PROP_STATE, + N_PROPS +}; +static GParamSpec *obj_props[N_PROPS] = { NULL, }; + +static void gvc_mixer_stream_finalize (GObject *object); + +G_DEFINE_ABSTRACT_TYPE_WITH_PRIVATE (GvcMixerStream, gvc_mixer_stream, G_TYPE_OBJECT) + +static void +free_port (GvcMixerStreamPort *p) +{ + g_free (p->port); + g_free (p->human_port); + g_slice_free (GvcMixerStreamPort, p); +} + +static GvcMixerStreamPort * +dup_port (GvcMixerStreamPort *p) +{ + GvcMixerStreamPort *m; + + m = g_slice_new (GvcMixerStreamPort); + + *m = *p; + m->port = g_strdup (p->port); + m->human_port = g_strdup (p->human_port); + + return m; +} + +G_DEFINE_BOXED_TYPE (GvcMixerStreamPort, gvc_mixer_stream_port, dup_port, free_port) + +static guint32 +get_next_stream_serial (void) +{ + guint32 serial; + + serial = stream_serial++; + + if ((gint32)stream_serial < 0) { + stream_serial = 1; + } + + return serial; +} + +pa_context * +gvc_mixer_stream_get_pa_context (GvcMixerStream *stream) +{ + g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), 0); + return stream->priv->pa_context; +} + +guint +gvc_mixer_stream_get_index (GvcMixerStream *stream) +{ + g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), 0); + return stream->priv->index; +} + +guint +gvc_mixer_stream_get_id (GvcMixerStream *stream) +{ + g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), 0); + return stream->priv->id; +} + +const GvcChannelMap * +gvc_mixer_stream_get_channel_map (GvcMixerStream *stream) +{ + g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), NULL); + return stream->priv->channel_map; +} + +/** + * gvc_mixer_stream_get_volume: + * @stream: + * + * Returns: (type guint32): + */ +pa_volume_t +gvc_mixer_stream_get_volume (GvcMixerStream *stream) +{ + g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), 0); + + return (pa_volume_t) gvc_channel_map_get_volume(stream->priv->channel_map)[VOLUME]; +} + +gdouble +gvc_mixer_stream_get_decibel (GvcMixerStream *stream) +{ + g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), 0); + + return pa_sw_volume_to_dB( + (pa_volume_t) gvc_channel_map_get_volume(stream->priv->channel_map)[VOLUME]); +} + +/** + * gvc_mixer_stream_set_volume: + * @stream: + * @volume: (type guint32): + * + * Returns: + */ +gboolean +gvc_mixer_stream_set_volume (GvcMixerStream *stream, + pa_volume_t volume) +{ + pa_cvolume cv; + + g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), FALSE); + + cv = *gvc_channel_map_get_cvolume(stream->priv->channel_map); + pa_cvolume_scale(&cv, volume); + + if (!pa_cvolume_equal(gvc_channel_map_get_cvolume(stream->priv->channel_map), &cv)) { + gvc_channel_map_volume_changed(stream->priv->channel_map, &cv, FALSE); + g_object_notify_by_pspec (G_OBJECT (stream), obj_props[PROP_VOLUME]); + return TRUE; + } + + return FALSE; +} + +gboolean +gvc_mixer_stream_set_decibel (GvcMixerStream *stream, + gdouble db) +{ + pa_cvolume cv; + + g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), FALSE); + + cv = *gvc_channel_map_get_cvolume(stream->priv->channel_map); + pa_cvolume_scale(&cv, pa_sw_volume_from_dB(db)); + + if (!pa_cvolume_equal(gvc_channel_map_get_cvolume(stream->priv->channel_map), &cv)) { + gvc_channel_map_volume_changed(stream->priv->channel_map, &cv, FALSE); + g_object_notify_by_pspec (G_OBJECT (stream), obj_props[PROP_VOLUME]); + } + + return TRUE; +} + +gboolean +gvc_mixer_stream_get_is_muted (GvcMixerStream *stream) +{ + g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), FALSE); + return stream->priv->is_muted; +} + +gboolean +gvc_mixer_stream_get_can_decibel (GvcMixerStream *stream) +{ + g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), FALSE); + return stream->priv->can_decibel; +} + +gboolean +gvc_mixer_stream_set_is_muted (GvcMixerStream *stream, + gboolean is_muted) +{ + g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), FALSE); + + if (is_muted != stream->priv->is_muted) { + stream->priv->is_muted = is_muted; + g_object_notify_by_pspec (G_OBJECT (stream), obj_props[PROP_IS_MUTED]); + } + + return TRUE; +} + +gboolean +gvc_mixer_stream_set_can_decibel (GvcMixerStream *stream, + gboolean can_decibel) +{ + g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), FALSE); + + if (can_decibel != stream->priv->can_decibel) { + stream->priv->can_decibel = can_decibel; + g_object_notify_by_pspec (G_OBJECT (stream), obj_props[PROP_CAN_DECIBEL]); + } + + return TRUE; +} + +const char * +gvc_mixer_stream_get_name (GvcMixerStream *stream) +{ + g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), NULL); + return stream->priv->name; +} + +const char * +gvc_mixer_stream_get_description (GvcMixerStream *stream) +{ + g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), NULL); + return stream->priv->description; +} + +gboolean +gvc_mixer_stream_set_name (GvcMixerStream *stream, + const char *name) +{ + g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), FALSE); + + g_free (stream->priv->name); + stream->priv->name = g_strdup (name); + g_object_notify_by_pspec (G_OBJECT (stream), obj_props[PROP_NAME]); + + return TRUE; +} + +gboolean +gvc_mixer_stream_set_description (GvcMixerStream *stream, + const char *description) +{ + g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), FALSE); + + g_free (stream->priv->description); + stream->priv->description = g_strdup (description); + g_object_notify_by_pspec (G_OBJECT (stream), obj_props[PROP_DESCRIPTION]); + + return TRUE; +} + +gboolean +gvc_mixer_stream_is_event_stream (GvcMixerStream *stream) +{ + g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), FALSE); + + return stream->priv->is_event_stream; +} + +gboolean +gvc_mixer_stream_set_is_event_stream (GvcMixerStream *stream, + gboolean is_event_stream) +{ + g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), FALSE); + + stream->priv->is_event_stream = is_event_stream; + g_object_notify_by_pspec (G_OBJECT (stream), obj_props[PROP_IS_EVENT_STREAM]); + + return TRUE; +} + +gboolean +gvc_mixer_stream_is_virtual (GvcMixerStream *stream) +{ + g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), FALSE); + + return stream->priv->is_virtual; +} + +gboolean +gvc_mixer_stream_set_is_virtual (GvcMixerStream *stream, + gboolean is_virtual) +{ + g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), FALSE); + + stream->priv->is_virtual = is_virtual; + g_object_notify_by_pspec (G_OBJECT (stream), obj_props[PROP_IS_VIRTUAL]); + + return TRUE; +} + +const char * +gvc_mixer_stream_get_application_id (GvcMixerStream *stream) +{ + g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), NULL); + return stream->priv->application_id; +} + +gboolean +gvc_mixer_stream_set_application_id (GvcMixerStream *stream, + const char *application_id) +{ + g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), FALSE); + + g_free (stream->priv->application_id); + stream->priv->application_id = g_strdup (application_id); + g_object_notify_by_pspec (G_OBJECT (stream), obj_props[PROP_APPLICATION_ID]); + + return TRUE; +} + +static void +on_channel_map_volume_changed (GvcChannelMap *channel_map, + gboolean set, + GvcMixerStream *stream) +{ + if (set == TRUE) + gvc_mixer_stream_push_volume (stream); + + g_object_notify_by_pspec (G_OBJECT (stream), obj_props[PROP_VOLUME]); +} + +static gboolean +gvc_mixer_stream_set_channel_map (GvcMixerStream *stream, + GvcChannelMap *channel_map) +{ + g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), FALSE); + + if (channel_map != NULL) { + g_object_ref (channel_map); + } + + if (stream->priv->channel_map != NULL) { + g_signal_handlers_disconnect_by_func (stream->priv->channel_map, + on_channel_map_volume_changed, + stream); + g_object_unref (stream->priv->channel_map); + } + + stream->priv->channel_map = channel_map; + + if (stream->priv->channel_map != NULL) { + g_signal_connect (stream->priv->channel_map, + "volume-changed", + G_CALLBACK (on_channel_map_volume_changed), + stream); + + g_object_notify_by_pspec (G_OBJECT (stream), obj_props[PROP_CHANNEL_MAP]); + } + + return TRUE; +} + +const char * +gvc_mixer_stream_get_icon_name (GvcMixerStream *stream) +{ + g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), NULL); + return stream->priv->icon_name; +} + +const char * +gvc_mixer_stream_get_form_factor (GvcMixerStream *stream) +{ + g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), NULL); + return stream->priv->form_factor; +} + +const char * +gvc_mixer_stream_get_sysfs_path (GvcMixerStream *stream) +{ + g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), NULL); + return stream->priv->sysfs_path; +} + +/** + * gvc_mixer_stream_get_gicon: + * @stream: a #GvcMixerStream + * + * Returns: (transfer full): a new #GIcon + */ +GIcon * +gvc_mixer_stream_get_gicon (GvcMixerStream *stream) +{ + g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), NULL); + if (stream->priv->icon_name == NULL) + return NULL; + return g_themed_icon_new_with_default_fallbacks (stream->priv->icon_name); +} + +gboolean +gvc_mixer_stream_set_icon_name (GvcMixerStream *stream, + const char *icon_name) +{ + g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), FALSE); + + g_free (stream->priv->icon_name); + stream->priv->icon_name = g_strdup (icon_name); + g_object_notify_by_pspec (G_OBJECT (stream), obj_props[PROP_ICON_NAME]); + + return TRUE; +} + +gboolean +gvc_mixer_stream_set_form_factor (GvcMixerStream *stream, + const char *form_factor) +{ + g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), FALSE); + + g_free (stream->priv->form_factor); + stream->priv->form_factor = g_strdup (form_factor); + g_object_notify_by_pspec (G_OBJECT (stream), obj_props[PROP_FORM_FACTOR]); + + return TRUE; +} + +gboolean +gvc_mixer_stream_set_sysfs_path (GvcMixerStream *stream, + const char *sysfs_path) +{ + g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), FALSE); + + g_free (stream->priv->sysfs_path); + stream->priv->sysfs_path = g_strdup (sysfs_path); + g_object_notify_by_pspec (G_OBJECT (stream), obj_props[PROP_SYSFS_PATH]); + + return TRUE; +} + +/** + * gvc_mixer_stream_get_base_volume: + * @stream: + * + * Returns: (type guint32): + */ +pa_volume_t +gvc_mixer_stream_get_base_volume (GvcMixerStream *stream) +{ + g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), 0); + + return stream->priv->base_volume; +} + +/** + * gvc_mixer_stream_set_base_volume: + * @stream: + * @base_volume: (type guint32): + * + * Returns: + */ +gboolean +gvc_mixer_stream_set_base_volume (GvcMixerStream *stream, + pa_volume_t base_volume) +{ + g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), FALSE); + + stream->priv->base_volume = base_volume; + + return TRUE; +} + +const GvcMixerStreamPort * +gvc_mixer_stream_get_port (GvcMixerStream *stream) +{ + GList *l; + + g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), NULL); + g_return_val_if_fail (stream->priv->ports != NULL, NULL); + + for (l = stream->priv->ports; l != NULL; l = l->next) { + GvcMixerStreamPort *p = l->data; + if (g_strcmp0 (stream->priv->port, p->port) == 0) { + return p; + } + } + + g_assert_not_reached (); + + return NULL; +} + +gboolean +gvc_mixer_stream_set_port (GvcMixerStream *stream, + const char *port) +{ + GList *l; + + g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), FALSE); + g_return_val_if_fail (stream->priv->ports != NULL, FALSE); + + g_free (stream->priv->port); + stream->priv->port = g_strdup (port); + + g_free (stream->priv->human_port); + stream->priv->human_port = NULL; + + for (l = stream->priv->ports; l != NULL; l = l->next) { + GvcMixerStreamPort *p = l->data; + if (g_str_equal (stream->priv->port, p->port)) { + stream->priv->human_port = g_strdup (p->human_port); + break; + } + } + + g_object_notify_by_pspec (G_OBJECT (stream), obj_props[PROP_PORT]); + + return TRUE; +} + +gboolean +gvc_mixer_stream_change_port (GvcMixerStream *stream, + const char *port) +{ + g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), FALSE); + return GVC_MIXER_STREAM_GET_CLASS (stream)->change_port (stream, port); +} + +/** + * gvc_mixer_stream_get_ports: + * + * Return value: (transfer none) (element-type GvcMixerStreamPort): + */ +const GList * +gvc_mixer_stream_get_ports (GvcMixerStream *stream) +{ + g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), NULL); + return stream->priv->ports; +} + +gboolean +gvc_mixer_stream_set_state (GvcMixerStream *stream, + GvcMixerStreamState state) +{ + g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), FALSE); + + if (stream->priv->state != state) { + stream->priv->state = state; + g_object_notify_by_pspec (G_OBJECT (stream), obj_props[PROP_STATE]); + } + + return TRUE; +} + +GvcMixerStreamState +gvc_mixer_stream_get_state (GvcMixerStream *stream) +{ + g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), GVC_STREAM_STATE_INVALID); + return stream->priv->state; +} + +static int +sort_ports (GvcMixerStreamPort *a, + GvcMixerStreamPort *b) +{ + if (a->priority == b->priority) + return 0; + if (a->priority > b->priority) + return 1; + return -1; +} + +/** + * gvc_mixer_stream_set_ports: + * @ports: (transfer full) (element-type GvcMixerStreamPort): + */ +gboolean +gvc_mixer_stream_set_ports (GvcMixerStream *stream, + GList *ports) +{ + g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), FALSE); + g_return_val_if_fail (stream->priv->ports == NULL, FALSE); + + stream->priv->ports = g_list_sort (ports, (GCompareFunc) sort_ports); + + return TRUE; +} + +guint +gvc_mixer_stream_get_card_index (GvcMixerStream *stream) +{ + g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), PA_INVALID_INDEX); + return stream->priv->card_index; +} + +gboolean +gvc_mixer_stream_set_card_index (GvcMixerStream *stream, + guint card_index) +{ + g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), FALSE); + + stream->priv->card_index = card_index; + g_object_notify_by_pspec (G_OBJECT (stream), obj_props[PROP_CARD_INDEX]); + + return TRUE; +} + +static void +gvc_mixer_stream_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + GvcMixerStream *self = GVC_MIXER_STREAM (object); + + switch (prop_id) { + case PROP_PA_CONTEXT: + self->priv->pa_context = g_value_get_pointer (value); + break; + case PROP_INDEX: + self->priv->index = g_value_get_ulong (value); + break; + case PROP_ID: + self->priv->id = g_value_get_ulong (value); + break; + case PROP_CHANNEL_MAP: + gvc_mixer_stream_set_channel_map (self, g_value_get_object (value)); + break; + case PROP_NAME: + gvc_mixer_stream_set_name (self, g_value_get_string (value)); + break; + case PROP_DESCRIPTION: + gvc_mixer_stream_set_description (self, g_value_get_string (value)); + break; + case PROP_APPLICATION_ID: + gvc_mixer_stream_set_application_id (self, g_value_get_string (value)); + break; + case PROP_ICON_NAME: + gvc_mixer_stream_set_icon_name (self, g_value_get_string (value)); + break; + case PROP_FORM_FACTOR: + gvc_mixer_stream_set_form_factor (self, g_value_get_string (value)); + break; + case PROP_SYSFS_PATH: + gvc_mixer_stream_set_sysfs_path (self, g_value_get_string (value)); + break; + case PROP_VOLUME: + gvc_mixer_stream_set_volume (self, g_value_get_ulong (value)); + break; + case PROP_DECIBEL: + gvc_mixer_stream_set_decibel (self, g_value_get_double (value)); + break; + case PROP_IS_MUTED: + gvc_mixer_stream_set_is_muted (self, g_value_get_boolean (value)); + break; + case PROP_IS_EVENT_STREAM: + gvc_mixer_stream_set_is_event_stream (self, g_value_get_boolean (value)); + break; + case PROP_IS_VIRTUAL: + gvc_mixer_stream_set_is_virtual (self, g_value_get_boolean (value)); + break; + case PROP_CAN_DECIBEL: + gvc_mixer_stream_set_can_decibel (self, g_value_get_boolean (value)); + break; + case PROP_PORT: + gvc_mixer_stream_set_port (self, g_value_get_string (value)); + break; + case PROP_STATE: + gvc_mixer_stream_set_state (self, g_value_get_enum (value)); + break; + case PROP_CARD_INDEX: + self->priv->card_index = g_value_get_long (value); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +gvc_mixer_stream_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + GvcMixerStream *self = GVC_MIXER_STREAM (object); + + switch (prop_id) { + case PROP_PA_CONTEXT: + g_value_set_pointer (value, self->priv->pa_context); + break; + case PROP_INDEX: + g_value_set_ulong (value, self->priv->index); + break; + case PROP_ID: + g_value_set_ulong (value, self->priv->id); + break; + case PROP_CHANNEL_MAP: + g_value_set_object (value, self->priv->channel_map); + break; + case PROP_NAME: + g_value_set_string (value, self->priv->name); + break; + case PROP_DESCRIPTION: + g_value_set_string (value, self->priv->description); + break; + case PROP_APPLICATION_ID: + g_value_set_string (value, self->priv->application_id); + break; + case PROP_ICON_NAME: + g_value_set_string (value, self->priv->icon_name); + break; + case PROP_FORM_FACTOR: + g_value_set_string (value, self->priv->form_factor); + break; + case PROP_SYSFS_PATH: + g_value_set_string (value, self->priv->sysfs_path); + break; + case PROP_VOLUME: + g_value_set_ulong (value, + pa_cvolume_max(gvc_channel_map_get_cvolume(self->priv->channel_map))); + break; + case PROP_DECIBEL: + g_value_set_double (value, + pa_sw_volume_to_dB(pa_cvolume_max(gvc_channel_map_get_cvolume(self->priv->channel_map)))); + break; + case PROP_IS_MUTED: + g_value_set_boolean (value, self->priv->is_muted); + break; + case PROP_IS_EVENT_STREAM: + g_value_set_boolean (value, self->priv->is_event_stream); + break; + case PROP_IS_VIRTUAL: + g_value_set_boolean (value, self->priv->is_virtual); + break; + case PROP_CAN_DECIBEL: + g_value_set_boolean (value, self->priv->can_decibel); + break; + case PROP_PORT: + g_value_set_string (value, self->priv->port); + break; + case PROP_STATE: + g_value_set_enum (value, self->priv->state); + break; + case PROP_CARD_INDEX: + g_value_set_long (value, self->priv->card_index); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static GObject * +gvc_mixer_stream_constructor (GType type, + guint n_construct_properties, + GObjectConstructParam *construct_params) +{ + GObject *object; + GvcMixerStream *self; + + object = G_OBJECT_CLASS (gvc_mixer_stream_parent_class)->constructor (type, n_construct_properties, construct_params); + + self = GVC_MIXER_STREAM (object); + + self->priv->id = get_next_stream_serial (); + + return object; +} + +static gboolean +gvc_mixer_stream_real_change_port (GvcMixerStream *stream, + const char *port) +{ + return FALSE; +} + +static gboolean +gvc_mixer_stream_real_push_volume (GvcMixerStream *stream, gpointer *op) +{ + return FALSE; +} + +static gboolean +gvc_mixer_stream_real_change_is_muted (GvcMixerStream *stream, + gboolean is_muted) +{ + return FALSE; +} + +gboolean +gvc_mixer_stream_push_volume (GvcMixerStream *stream) +{ + pa_operation *op; + gboolean ret; + + g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), FALSE); + + if (stream->priv->is_event_stream != FALSE) + return TRUE; + + g_debug ("Pushing new volume to stream '%s' (%s)", + stream->priv->description, stream->priv->name); + + ret = GVC_MIXER_STREAM_GET_CLASS (stream)->push_volume (stream, (gpointer *) &op); + if (ret) { + if (stream->priv->change_volume_op != NULL) + pa_operation_unref (stream->priv->change_volume_op); + stream->priv->change_volume_op = op; + } + return ret; +} + +gboolean +gvc_mixer_stream_change_is_muted (GvcMixerStream *stream, + gboolean is_muted) +{ + gboolean ret; + g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), FALSE); + ret = GVC_MIXER_STREAM_GET_CLASS (stream)->change_is_muted (stream, is_muted); + return ret; +} + +gboolean +gvc_mixer_stream_is_running (GvcMixerStream *stream) +{ + g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), FALSE); + + if (stream->priv->change_volume_op == NULL) + return FALSE; + + if ((pa_operation_get_state(stream->priv->change_volume_op) == PA_OPERATION_RUNNING)) + return TRUE; + + pa_operation_unref(stream->priv->change_volume_op); + stream->priv->change_volume_op = NULL; + + return FALSE; +} + +static void +gvc_mixer_stream_class_init (GvcMixerStreamClass *klass) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS (klass); + + gobject_class->constructor = gvc_mixer_stream_constructor; + gobject_class->finalize = gvc_mixer_stream_finalize; + gobject_class->set_property = gvc_mixer_stream_set_property; + gobject_class->get_property = gvc_mixer_stream_get_property; + + klass->push_volume = gvc_mixer_stream_real_push_volume; + klass->change_port = gvc_mixer_stream_real_change_port; + klass->change_is_muted = gvc_mixer_stream_real_change_is_muted; + + obj_props[PROP_INDEX] = g_param_spec_ulong ("index", + "Index", + "The index for this stream", + 0, G_MAXULONG, 0, + G_PARAM_READWRITE|G_PARAM_CONSTRUCT_ONLY|G_PARAM_STATIC_STRINGS); + obj_props[PROP_ID] = g_param_spec_ulong ("id", + "id", + "The id for this stream", + 0, G_MAXULONG, 0, + G_PARAM_READWRITE|G_PARAM_CONSTRUCT_ONLY|G_PARAM_STATIC_STRINGS); + obj_props[PROP_CHANNEL_MAP] = g_param_spec_object ("channel-map", + "channel map", + "The channel map for this stream", + GVC_TYPE_CHANNEL_MAP, + G_PARAM_READWRITE|G_PARAM_CONSTRUCT|G_PARAM_STATIC_STRINGS); + obj_props[PROP_PA_CONTEXT] = g_param_spec_pointer ("pa-context", + "PulseAudio context", + "The PulseAudio context for this stream", + G_PARAM_READWRITE|G_PARAM_CONSTRUCT_ONLY|G_PARAM_STATIC_STRINGS); + obj_props[PROP_VOLUME] = g_param_spec_ulong ("volume", + "Volume", + "The volume for this stream", + 0, G_MAXULONG, 0, + G_PARAM_READWRITE|G_PARAM_STATIC_STRINGS); + obj_props[PROP_DECIBEL] = g_param_spec_double ("decibel", + "Decibel", + "The decibel level for this stream", + -G_MAXDOUBLE, G_MAXDOUBLE, 0, + G_PARAM_READWRITE|G_PARAM_CONSTRUCT|G_PARAM_STATIC_STRINGS); + obj_props[PROP_NAME] = g_param_spec_string ("name", + "Name", + "Name to display for this stream", + NULL, + G_PARAM_READWRITE|G_PARAM_CONSTRUCT|G_PARAM_STATIC_STRINGS); + obj_props[PROP_DESCRIPTION] = g_param_spec_string ("description", + "Description", + "Description to display for this stream", + NULL, + G_PARAM_READWRITE|G_PARAM_CONSTRUCT|G_PARAM_STATIC_STRINGS); + obj_props[PROP_APPLICATION_ID] = g_param_spec_string ("application-id", + "Application identifier", + "Application identifier for this stream", + NULL, + G_PARAM_READWRITE|G_PARAM_CONSTRUCT|G_PARAM_STATIC_STRINGS); + obj_props[PROP_ICON_NAME] = g_param_spec_string ("icon-name", + "Icon Name", + "Name of icon to display for this stream", + NULL, + G_PARAM_READWRITE|G_PARAM_CONSTRUCT|G_PARAM_STATIC_STRINGS); + obj_props[PROP_FORM_FACTOR] = g_param_spec_string ("form-factor", + "Form Factor", + "Device form factor for this stream, as reported by PulseAudio", + NULL, + G_PARAM_READWRITE|G_PARAM_CONSTRUCT|G_PARAM_STATIC_STRINGS); + obj_props[PROP_SYSFS_PATH] = g_param_spec_string ("sysfs-path", + "Sysfs path", + "Sysfs path for the device associated with this stream", + NULL, + G_PARAM_READWRITE|G_PARAM_CONSTRUCT|G_PARAM_STATIC_STRINGS); + obj_props[PROP_IS_MUTED] = g_param_spec_boolean ("is-muted", + "is muted", + "Whether stream is muted", + FALSE, + G_PARAM_READWRITE|G_PARAM_CONSTRUCT|G_PARAM_STATIC_STRINGS); + obj_props[PROP_CAN_DECIBEL] = g_param_spec_boolean ("can-decibel", + "can decibel", + "Whether stream volume can be converted to decibel units", + FALSE, + G_PARAM_READWRITE|G_PARAM_CONSTRUCT|G_PARAM_STATIC_STRINGS); + obj_props[PROP_IS_EVENT_STREAM] = g_param_spec_boolean ("is-event-stream", + "is event stream", + "Whether stream's role is to play an event", + FALSE, + G_PARAM_READWRITE|G_PARAM_CONSTRUCT|G_PARAM_STATIC_STRINGS); + obj_props[PROP_IS_VIRTUAL] = g_param_spec_boolean ("is-virtual", + "is virtual stream", + "Whether the stream is virtual", + FALSE, + G_PARAM_READWRITE|G_PARAM_CONSTRUCT|G_PARAM_STATIC_STRINGS); + obj_props[PROP_PORT] = g_param_spec_string ("port", + "Port", + "The name of the current port for this stream", + NULL, + G_PARAM_READWRITE|G_PARAM_STATIC_STRINGS); + obj_props[PROP_STATE] = g_param_spec_enum ("state", + "State", + "The current state of this stream", + GVC_TYPE_MIXER_STREAM_STATE, + GVC_STREAM_STATE_INVALID, + G_PARAM_READWRITE|G_PARAM_STATIC_STRINGS); + obj_props[PROP_CARD_INDEX] = g_param_spec_long ("card-index", + "Card index", + "The index of the card for this stream", + PA_INVALID_INDEX, G_MAXLONG, PA_INVALID_INDEX, + G_PARAM_READWRITE|G_PARAM_CONSTRUCT|G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (gobject_class, N_PROPS, obj_props); +} + +static void +gvc_mixer_stream_init (GvcMixerStream *stream) +{ + stream->priv = gvc_mixer_stream_get_instance_private (stream); +} + +static void +gvc_mixer_stream_finalize (GObject *object) +{ + GvcMixerStream *mixer_stream; + + g_return_if_fail (object != NULL); + g_return_if_fail (GVC_IS_MIXER_STREAM (object)); + + mixer_stream = GVC_MIXER_STREAM (object); + + g_return_if_fail (mixer_stream->priv != NULL); + + g_object_unref (mixer_stream->priv->channel_map); + mixer_stream->priv->channel_map = NULL; + + g_free (mixer_stream->priv->name); + mixer_stream->priv->name = NULL; + + g_free (mixer_stream->priv->description); + mixer_stream->priv->description = NULL; + + g_free (mixer_stream->priv->application_id); + mixer_stream->priv->application_id = NULL; + + g_free (mixer_stream->priv->icon_name); + mixer_stream->priv->icon_name = NULL; + + g_free (mixer_stream->priv->form_factor); + mixer_stream->priv->form_factor = NULL; + + g_free (mixer_stream->priv->sysfs_path); + mixer_stream->priv->sysfs_path = NULL; + + g_free (mixer_stream->priv->port); + mixer_stream->priv->port = NULL; + + g_free (mixer_stream->priv->human_port); + mixer_stream->priv->human_port = NULL; + + g_list_free_full (mixer_stream->priv->ports, (GDestroyNotify) free_port); + mixer_stream->priv->ports = NULL; + + if (mixer_stream->priv->change_volume_op) { + pa_operation_unref(mixer_stream->priv->change_volume_op); + mixer_stream->priv->change_volume_op = NULL; + } + + G_OBJECT_CLASS (gvc_mixer_stream_parent_class)->finalize (object); +} diff --git a/subprojects/gvc/gvc-mixer-stream.h b/subprojects/gvc/gvc-mixer-stream.h new file mode 100644 index 0000000..586ec75 --- /dev/null +++ b/subprojects/gvc/gvc-mixer-stream.h @@ -0,0 +1,146 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2008 Red Hat, Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * + */ + +#ifndef __GVC_MIXER_STREAM_H +#define __GVC_MIXER_STREAM_H + +#include <glib-object.h> +#include "gvc-pulseaudio-fake.h" +#include "gvc-channel-map.h" +#include <gio/gio.h> + +G_BEGIN_DECLS + +#define GVC_TYPE_MIXER_STREAM (gvc_mixer_stream_get_type ()) +#define GVC_MIXER_STREAM(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), GVC_TYPE_MIXER_STREAM, GvcMixerStream)) +#define GVC_MIXER_STREAM_CLASS(k) (G_TYPE_CHECK_CLASS_CAST((k), GVC_TYPE_MIXER_STREAM, GvcMixerStreamClass)) +#define GVC_IS_MIXER_STREAM(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), GVC_TYPE_MIXER_STREAM)) +#define GVC_IS_MIXER_STREAM_CLASS(k) (G_TYPE_CHECK_CLASS_TYPE ((k), GVC_TYPE_MIXER_STREAM)) +#define GVC_MIXER_STREAM_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), GVC_TYPE_MIXER_STREAM, GvcMixerStreamClass)) + +typedef struct GvcMixerStreamPrivate GvcMixerStreamPrivate; + +typedef struct +{ + GObject parent; + GvcMixerStreamPrivate *priv; +} GvcMixerStream; + +typedef struct +{ + GObjectClass parent_class; + + /* vtable */ + gboolean (*push_volume) (GvcMixerStream *stream, + gpointer *operation); + gboolean (*change_is_muted) (GvcMixerStream *stream, + gboolean is_muted); + gboolean (*change_port) (GvcMixerStream *stream, + const char *port); +} GvcMixerStreamClass; + +typedef struct +{ + char *port; + char *human_port; + guint priority; + gboolean available; +} GvcMixerStreamPort; + +typedef enum +{ + GVC_STREAM_STATE_INVALID, + GVC_STREAM_STATE_RUNNING, + GVC_STREAM_STATE_IDLE, + GVC_STREAM_STATE_SUSPENDED +} GvcMixerStreamState; + +GType gvc_mixer_stream_port_get_type (void) G_GNUC_CONST; +GType gvc_mixer_stream_get_type (void) G_GNUC_CONST; + +guint gvc_mixer_stream_get_index (GvcMixerStream *stream); +guint gvc_mixer_stream_get_id (GvcMixerStream *stream); +const GvcChannelMap *gvc_mixer_stream_get_channel_map(GvcMixerStream *stream); +const GvcMixerStreamPort *gvc_mixer_stream_get_port (GvcMixerStream *stream); +const GList * gvc_mixer_stream_get_ports (GvcMixerStream *stream); +gboolean gvc_mixer_stream_change_port (GvcMixerStream *stream, + const char *port); + +pa_volume_t gvc_mixer_stream_get_volume (GvcMixerStream *stream); +gdouble gvc_mixer_stream_get_decibel (GvcMixerStream *stream); +gboolean gvc_mixer_stream_push_volume (GvcMixerStream *stream); +pa_volume_t gvc_mixer_stream_get_base_volume (GvcMixerStream *stream); + +gboolean gvc_mixer_stream_get_is_muted (GvcMixerStream *stream); +gboolean gvc_mixer_stream_get_can_decibel (GvcMixerStream *stream); +gboolean gvc_mixer_stream_change_is_muted (GvcMixerStream *stream, + gboolean is_muted); +gboolean gvc_mixer_stream_is_running (GvcMixerStream *stream); +const char * gvc_mixer_stream_get_name (GvcMixerStream *stream); +const char * gvc_mixer_stream_get_icon_name (GvcMixerStream *stream); +const char * gvc_mixer_stream_get_form_factor (GvcMixerStream *stream); +const char * gvc_mixer_stream_get_sysfs_path (GvcMixerStream *stream); +GIcon * gvc_mixer_stream_get_gicon (GvcMixerStream *stream); +const char * gvc_mixer_stream_get_description (GvcMixerStream *stream); +const char * gvc_mixer_stream_get_application_id (GvcMixerStream *stream); +gboolean gvc_mixer_stream_is_event_stream (GvcMixerStream *stream); +gboolean gvc_mixer_stream_is_virtual (GvcMixerStream *stream); +guint gvc_mixer_stream_get_card_index (GvcMixerStream *stream); +GvcMixerStreamState gvc_mixer_stream_get_state (GvcMixerStream *stream); + +/* private */ +gboolean gvc_mixer_stream_set_volume (GvcMixerStream *stream, + pa_volume_t volume); +gboolean gvc_mixer_stream_set_decibel (GvcMixerStream *stream, + gdouble db); +gboolean gvc_mixer_stream_set_is_muted (GvcMixerStream *stream, + gboolean is_muted); +gboolean gvc_mixer_stream_set_can_decibel (GvcMixerStream *stream, + gboolean can_decibel); +gboolean gvc_mixer_stream_set_name (GvcMixerStream *stream, + const char *name); +gboolean gvc_mixer_stream_set_description (GvcMixerStream *stream, + const char *description); +gboolean gvc_mixer_stream_set_icon_name (GvcMixerStream *stream, + const char *name); +gboolean gvc_mixer_stream_set_form_factor (GvcMixerStream *stream, + const char *form_factor); +gboolean gvc_mixer_stream_set_sysfs_path (GvcMixerStream *stream, + const char *sysfs_path); +gboolean gvc_mixer_stream_set_is_event_stream (GvcMixerStream *stream, + gboolean is_event_stream); +gboolean gvc_mixer_stream_set_is_virtual (GvcMixerStream *stream, + gboolean is_event_stream); +gboolean gvc_mixer_stream_set_application_id (GvcMixerStream *stream, + const char *application_id); +gboolean gvc_mixer_stream_set_base_volume (GvcMixerStream *stream, + pa_volume_t base_volume); +gboolean gvc_mixer_stream_set_port (GvcMixerStream *stream, + const char *port); +gboolean gvc_mixer_stream_set_ports (GvcMixerStream *stream, + GList *ports); +gboolean gvc_mixer_stream_set_card_index (GvcMixerStream *stream, + guint card_index); +gboolean gvc_mixer_stream_set_state (GvcMixerStream *stream, + GvcMixerStreamState state); + +G_END_DECLS + +#endif /* __GVC_MIXER_STREAM_H */ diff --git a/subprojects/gvc/gvc-mixer-ui-device.c b/subprojects/gvc/gvc-mixer-ui-device.c new file mode 100644 index 0000000..db1a694 --- /dev/null +++ b/subprojects/gvc/gvc-mixer-ui-device.c @@ -0,0 +1,744 @@ +/* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 4; tab-width: 4 -*- */ +/* + * gvc-mixer-ui-device.c + * Copyright (C) Conor Curran 2011 <conor.curran@canonical.com> + * Copyright (C) 2012 David Henningsson, Canonical Ltd. <david.henningsson@canonical.com> + * + * gvc-mixer-ui-device.c 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. + * + * gvc-mixer-ui-device.c is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#include <string.h> + +#include "gvc-mixer-ui-device.h" +#include "gvc-mixer-card.h" + +struct GvcMixerUIDevicePrivate +{ + gchar *first_line_desc; + gchar *second_line_desc; + + GvcMixerCard *card; + gchar *port_name; + char *icon_name; + guint stream_id; + guint id; + gboolean port_available; + + /* These two lists contain pointers to GvcMixerCardProfile objects. Those objects are owned by GvcMixerCard. * + * TODO: Do we want to add a weak reference to the GvcMixerCard for this reason? */ + GList *supported_profiles; /* all profiles supported by this port.*/ + GList *profiles; /* profiles to be added to combobox, subset of supported_profiles. */ + GvcMixerUIDeviceDirection type; + gboolean disable_profile_swapping; + gchar *user_preferred_profile; +}; + +enum +{ + PROP_0, + PROP_DESC_LINE_1, + PROP_DESC_LINE_2, + PROP_CARD, + PROP_PORT_NAME, + PROP_STREAM_ID, + PROP_UI_DEVICE_TYPE, + PROP_PORT_AVAILABLE, + PROP_ICON_NAME, + N_PROPS +}; +static GParamSpec *obj_props[N_PROPS] = { NULL, }; + +static void gvc_mixer_ui_device_finalize (GObject *object); + +static void gvc_mixer_ui_device_set_icon_name (GvcMixerUIDevice *device, + const char *icon_name); + +G_DEFINE_TYPE_WITH_PRIVATE (GvcMixerUIDevice, gvc_mixer_ui_device, G_TYPE_OBJECT) + +static guint32 +get_next_output_serial (void) +{ + static guint32 output_serial = 1; + guint32 serial; + + serial = output_serial++; + + if ((gint32)output_serial < 0) + output_serial = 1; + + return serial; +} + +static void +gvc_mixer_ui_device_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + GvcMixerUIDevice *self = GVC_MIXER_UI_DEVICE (object); + + switch (property_id) { + case PROP_DESC_LINE_1: + g_value_set_string (value, self->priv->first_line_desc); + break; + case PROP_DESC_LINE_2: + g_value_set_string (value, self->priv->second_line_desc); + break; + case PROP_CARD: + g_value_set_pointer (value, self->priv->card); + break; + case PROP_PORT_NAME: + g_value_set_string (value, self->priv->port_name); + break; + case PROP_STREAM_ID: + g_value_set_uint (value, self->priv->stream_id); + break; + case PROP_UI_DEVICE_TYPE: + g_value_set_uint (value, (guint)self->priv->type); + break; + case PROP_PORT_AVAILABLE: + g_value_set_boolean (value, self->priv->port_available); + break; + case PROP_ICON_NAME: + g_value_set_string (value, gvc_mixer_ui_device_get_icon_name (self)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + +static void +gvc_mixer_ui_device_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + GvcMixerUIDevice *self = GVC_MIXER_UI_DEVICE (object); + + switch (property_id) { + case PROP_DESC_LINE_1: + g_free (self->priv->first_line_desc); + self->priv->first_line_desc = g_value_dup_string (value); + g_debug ("gvc-mixer-output-set-property - 1st line: %s", + self->priv->first_line_desc); + break; + case PROP_DESC_LINE_2: + g_free (self->priv->second_line_desc); + self->priv->second_line_desc = g_value_dup_string (value); + g_debug ("gvc-mixer-output-set-property - 2nd line: %s", + self->priv->second_line_desc); + break; + case PROP_CARD: + self->priv->card = g_value_get_pointer (value); + g_debug ("gvc-mixer-output-set-property - card: %p", + self->priv->card); + break; + case PROP_PORT_NAME: + g_free (self->priv->port_name); + self->priv->port_name = g_value_dup_string (value); + g_debug ("gvc-mixer-output-set-property - card port name: %s", + self->priv->port_name); + break; + case PROP_STREAM_ID: + self->priv->stream_id = g_value_get_uint (value); + g_debug ("gvc-mixer-output-set-property - sink/source id: %i", + self->priv->stream_id); + break; + case PROP_UI_DEVICE_TYPE: + self->priv->type = (GvcMixerUIDeviceDirection) g_value_get_uint (value); + break; + case PROP_PORT_AVAILABLE: + self->priv->port_available = g_value_get_boolean (value); + g_debug ("gvc-mixer-output-set-property - port available %i, value passed in %i", + self->priv->port_available, g_value_get_boolean (value)); + break; + case PROP_ICON_NAME: + gvc_mixer_ui_device_set_icon_name (self, g_value_get_string (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + +static GObject * +gvc_mixer_ui_device_constructor (GType type, + guint n_construct_properties, + GObjectConstructParam *construct_params) +{ + GObject *object; + GvcMixerUIDevice *self; + + object = G_OBJECT_CLASS (gvc_mixer_ui_device_parent_class)->constructor (type, n_construct_properties, construct_params); + + self = GVC_MIXER_UI_DEVICE (object); + self->priv->id = get_next_output_serial (); + self->priv->stream_id = GVC_MIXER_UI_DEVICE_INVALID; + return object; +} + +static void +gvc_mixer_ui_device_init (GvcMixerUIDevice *device) +{ + device->priv = gvc_mixer_ui_device_get_instance_private (device); +} + +static void +gvc_mixer_ui_device_dispose (GObject *object) +{ + GvcMixerUIDevice *device; + + g_return_if_fail (object != NULL); + g_return_if_fail (GVC_MIXER_UI_DEVICE (object)); + + device = GVC_MIXER_UI_DEVICE (object); + + g_clear_pointer (&device->priv->port_name, g_free); + g_clear_pointer (&device->priv->icon_name, g_free); + g_clear_pointer (&device->priv->first_line_desc, g_free); + g_clear_pointer (&device->priv->second_line_desc, g_free); + g_clear_pointer (&device->priv->profiles, g_list_free); + g_clear_pointer (&device->priv->supported_profiles, g_list_free); + g_clear_pointer (&device->priv->user_preferred_profile, g_free); + + G_OBJECT_CLASS (gvc_mixer_ui_device_parent_class)->dispose (object); +} + +static void +gvc_mixer_ui_device_finalize (GObject *object) +{ + G_OBJECT_CLASS (gvc_mixer_ui_device_parent_class)->finalize (object); +} + +static void +gvc_mixer_ui_device_class_init (GvcMixerUIDeviceClass *klass) +{ + GObjectClass* object_class = G_OBJECT_CLASS (klass); + + object_class->constructor = gvc_mixer_ui_device_constructor; + object_class->dispose = gvc_mixer_ui_device_dispose; + object_class->finalize = gvc_mixer_ui_device_finalize; + object_class->set_property = gvc_mixer_ui_device_set_property; + object_class->get_property = gvc_mixer_ui_device_get_property; + + obj_props[PROP_DESC_LINE_1] = + g_param_spec_string ("description", + "Description construct prop", + "Set first line description", + "no-name-set", + G_PARAM_READWRITE|G_PARAM_STATIC_STRINGS); + + obj_props[PROP_DESC_LINE_2] = + g_param_spec_string ("origin", + "origin construct prop", + "Set second line description name", + "no-name-set", + G_PARAM_READWRITE|G_PARAM_STATIC_STRINGS); + + obj_props[PROP_CARD] = + g_param_spec_pointer ("card", + "Card from pulse", + "Set/Get card", + G_PARAM_READWRITE|G_PARAM_STATIC_STRINGS); + + obj_props[PROP_PORT_NAME] = + g_param_spec_string ("port-name", + "port-name construct prop", + "Set port-name", + NULL, + G_PARAM_READWRITE|G_PARAM_STATIC_STRINGS); + + obj_props[PROP_STREAM_ID] = + g_param_spec_uint ("stream-id", + "stream id assigned by gvc-stream", + "Set/Get stream id", + 0, + G_MAXUINT, + GVC_MIXER_UI_DEVICE_INVALID, + G_PARAM_READWRITE|G_PARAM_STATIC_STRINGS); + + obj_props[PROP_UI_DEVICE_TYPE] = + g_param_spec_uint ("type", + "ui-device type", + "determine whether its an input and output", + 0, 1, 0, + G_PARAM_READWRITE|G_PARAM_STATIC_STRINGS); + + obj_props[PROP_PORT_AVAILABLE] = + g_param_spec_boolean ("port-available", + "available", + "determine whether this port is available", + FALSE, + G_PARAM_READWRITE|G_PARAM_STATIC_STRINGS); + + obj_props[PROP_ICON_NAME] = + g_param_spec_string ("icon-name", + "Icon Name", + "Name of icon to display for this card", + NULL, + G_PARAM_READWRITE|G_PARAM_CONSTRUCT|G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (object_class, N_PROPS, obj_props); +} + +/* Removes the part of the string that starts with skip_prefix + * ie. corresponding to the other direction. + * Normally either "input:" or "output:" + * + * Example: if given the input string "output:hdmi-stereo+input:analog-stereo" and + * skip_prefix "input:", the resulting string is "output:hdmi-stereo". + * + * The returned string must be freed with g_free(). + */ +static gchar * +get_profile_canonical_name (const gchar *profile_name, const gchar *skip_prefix) +{ + gchar *result = NULL; + gchar **s; + guint i; + + /* optimisation for the simple case. */ + if (strstr (profile_name, skip_prefix) == NULL) + return g_strdup (profile_name); + + s = g_strsplit (profile_name, "+", 0); + for (i = 0; i < g_strv_length (s); i++) { + if (g_str_has_prefix (s[i], skip_prefix)) + continue; + if (result == NULL) + result = g_strdup (s[i]); + else { + gchar *c = g_strdup_printf("%s+%s", result, s[i]); + g_free(result); + result = c; + } + } + + g_strfreev(s); + + if (!result) + return g_strdup("off"); + + return result; +} + +const gchar * +gvc_mixer_ui_device_get_matching_profile (GvcMixerUIDevice *device, const gchar *profile) +{ + const gchar *skip_prefix = device->priv->type == UIDeviceInput ? "output:" : "input:"; + gchar *target_cname = get_profile_canonical_name (profile, skip_prefix); + GList *l; + gchar *result = NULL; + + g_return_val_if_fail (GVC_IS_MIXER_UI_DEVICE (device), NULL); + g_return_val_if_fail (profile != NULL, NULL); + + for (l = device->priv->profiles; l != NULL; l = l->next) { + gchar *canonical_name; + GvcMixerCardProfile* p = l->data; + canonical_name = get_profile_canonical_name (p->profile, skip_prefix); + if (strcmp (canonical_name, target_cname) == 0) + result = p->profile; + g_free (canonical_name); + } + + g_free (target_cname); + g_debug ("Matching profile for '%s' is '%s'", profile, result ? result : "(null)"); + return result; +} + + +static void +add_canonical_names_of_profiles (GvcMixerUIDevice *device, + const GList *in_profiles, + GHashTable *added_profiles, + const gchar *skip_prefix, + gboolean only_canonical) +{ + const GList *l; + + for (l = in_profiles; l != NULL; l = l->next) { + gchar *canonical_name; + GvcMixerCardProfile* p = l->data; + + canonical_name = get_profile_canonical_name (p->profile, skip_prefix); + g_debug ("The canonical name for '%s' is '%s'", p->profile, canonical_name); + + /* Have we already added the canonical version of this profile? */ + if (g_hash_table_contains (added_profiles, canonical_name)) { + g_free (canonical_name); + continue; + } + + if (only_canonical && strcmp (p->profile, canonical_name) != 0) { + g_free (canonical_name); + continue; + } + + g_free (canonical_name); + + /* https://bugzilla.gnome.org/show_bug.cgi?id=693654 + * Don't add a profile that will make the UI device completely disappear */ + if (p->n_sinks == 0 && p->n_sources == 0) + continue; + + g_debug ("Adding profile to combobox: '%s' - '%s'", p->profile, p->human_profile); + g_hash_table_insert (added_profiles, g_strdup (p->profile), p); + device->priv->profiles = g_list_append (device->priv->profiles, p); + } +} + +/** + * gvc_mixer_ui_device_set_profiles: + * @in_profiles: (element-type Gvc.MixerCardProfile): a list of GvcMixerCardProfile + * + * Assigns value to + * - device->priv->profiles (profiles to be added to combobox) + * - device->priv->supported_profiles (all profiles of this port) + * - device->priv->disable_profile_swapping (whether to show the combobox) + * + * This method attempts to reduce the list of profiles visible to the user by figuring out + * from the context of that device (whether it's an input or an output) what profiles + * actually provide an alternative. + * + * It does this by the following. + * - It ignores off profiles. + * - It takes the canonical name of the profile. That name is what you get when you + * ignore the other direction. + * - In the first iteration, it only adds the names of canonical profiles - i e + * when the other side is turned off. + * - Normally the first iteration covers all cases, but sometimes (e g bluetooth) + * it doesn't, so add other profiles whose canonical name isn't already added + * in a second iteration. + */ +void +gvc_mixer_ui_device_set_profiles (GvcMixerUIDevice *device, + const GList *in_profiles) +{ + GHashTable *added_profiles; + const gchar *skip_prefix = device->priv->type == UIDeviceInput ? "output:" : "input:"; + + g_return_if_fail (GVC_IS_MIXER_UI_DEVICE (device)); + + g_debug ("Set profiles for '%s'", gvc_mixer_ui_device_get_description(device)); + + if (in_profiles == NULL) + return; + + device->priv->supported_profiles = g_list_copy ((GList*) in_profiles); + + added_profiles = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL); + + /* Run two iterations: First, add profiles which are canonical themselves, + * Second, add profiles for which the canonical name is not added already. */ + + add_canonical_names_of_profiles(device, in_profiles, added_profiles, skip_prefix, TRUE); + add_canonical_names_of_profiles(device, in_profiles, added_profiles, skip_prefix, FALSE); + + /* TODO: Consider adding the "Off" profile here */ + + device->priv->disable_profile_swapping = g_hash_table_size (added_profiles) <= 1; + g_hash_table_destroy (added_profiles); +} + +/** + * gvc_mixer_ui_device_get_best_profile: + * @selected: (allow-none): The selected profile or its canonical name or %NULL for any profile + * @current: The currently selected profile + * + * Returns: (transfer none): a profile name, valid as long as the UI device profiles are. + */ +const gchar * +gvc_mixer_ui_device_get_best_profile (GvcMixerUIDevice *device, + const gchar *selected, + const gchar *current) +{ + GList *candidates, *l; + const gchar *result; + const gchar *skip_prefix; + gchar *canonical_name_selected; + + g_return_val_if_fail (GVC_IS_MIXER_UI_DEVICE (device), NULL); + g_return_val_if_fail (current != NULL, NULL); + + if (device->priv->type == UIDeviceInput) + skip_prefix = "output:"; + else + skip_prefix = "input:"; + + /* First make a list of profiles acceptable to switch to */ + canonical_name_selected = NULL; + if (selected) + canonical_name_selected = get_profile_canonical_name (selected, skip_prefix); + + candidates = NULL; + for (l = device->priv->supported_profiles; l != NULL; l = l->next) { + gchar *canonical_name; + GvcMixerCardProfile* p = l->data; + canonical_name = get_profile_canonical_name (p->profile, skip_prefix); + if (!canonical_name_selected || strcmp (canonical_name, canonical_name_selected) == 0) { + candidates = g_list_append (candidates, p); + g_debug ("Candidate for profile switching: '%s'", p->profile); + } + g_free (canonical_name); + } + + if (!candidates) { + g_warning ("No suitable profile candidates for '%s'", selected ? selected : "(null)"); + g_free (canonical_name_selected); + return current; + } + + /* 1) Maybe we can skip profile switching altogether? */ + result = NULL; + for (l = candidates; (result == NULL) && (l != NULL); l = l->next) { + GvcMixerCardProfile* p = l->data; + if (strcmp (current, p->profile) == 0) + result = p->profile; + } + + /* 2) Try to keep the other side unchanged if possible */ + if (result == NULL) { + guint prio = 0; + const gchar *skip_prefix_reverse = device->priv->type == UIDeviceInput ? "input:" : "output:"; + gchar *current_reverse = get_profile_canonical_name (current, skip_prefix_reverse); + for (l = candidates; l != NULL; l = l->next) { + gchar *p_reverse; + GvcMixerCardProfile* p = l->data; + p_reverse = get_profile_canonical_name (p->profile, skip_prefix_reverse); + g_debug ("Comparing '%s' (from '%s') with '%s', prio %d", p_reverse, p->profile, current_reverse, p->priority); + if (strcmp (p_reverse, current_reverse) == 0 && (!result || p->priority > prio)) { + result = p->profile; + prio = p->priority; + } + g_free (p_reverse); + } + g_free (current_reverse); + } + + /* 3) All right, let's just pick the profile with highest priority. + * TODO: We could consider asking a GUI question if this stops streams + * in the other direction */ + if (result == NULL) { + guint prio = 0; + for (l = candidates; l != NULL; l = l->next) { + GvcMixerCardProfile* p = l->data; + if ((p->priority > prio) || !result) { + result = p->profile; + prio = p->priority; + } + } + } + + g_list_free (candidates); + g_free (canonical_name_selected); + return result; +} + +const gchar * +gvc_mixer_ui_device_get_active_profile (GvcMixerUIDevice* device) +{ + GvcMixerCardProfile *profile; + + g_return_val_if_fail (GVC_IS_MIXER_UI_DEVICE (device), NULL); + + if (device->priv->card == NULL) { + g_warning ("Device did not have an appropriate card"); + return NULL; + } + + profile = gvc_mixer_card_get_profile (device->priv->card); + return gvc_mixer_ui_device_get_matching_profile (device, profile->profile); +} + +gboolean +gvc_mixer_ui_device_should_profiles_be_hidden (GvcMixerUIDevice *device) +{ + g_return_val_if_fail (GVC_IS_MIXER_UI_DEVICE (device), FALSE); + + return device->priv->disable_profile_swapping; +} + +/** + * gvc_mixer_ui_device_get_profiles: + * @device: + * + * Returns: (transfer none) (element-type Gvc.MixerCardProfile): + */ +GList* +gvc_mixer_ui_device_get_profiles (GvcMixerUIDevice *device) +{ + g_return_val_if_fail (GVC_IS_MIXER_UI_DEVICE (device), NULL); + + return device->priv->profiles; +} + +/** + * gvc_mixer_ui_device_get_supported_profiles: + * @device: + * + * Returns: (transfer none) (element-type Gvc.MixerCardProfile): + */ +GList* +gvc_mixer_ui_device_get_supported_profiles (GvcMixerUIDevice *device) +{ + g_return_val_if_fail (GVC_IS_MIXER_UI_DEVICE (device), NULL); + + return device->priv->supported_profiles; +} + +guint +gvc_mixer_ui_device_get_id (GvcMixerUIDevice *device) +{ + g_return_val_if_fail (GVC_IS_MIXER_UI_DEVICE (device), 0); + + return device->priv->id; +} + +guint +gvc_mixer_ui_device_get_stream_id (GvcMixerUIDevice *device) +{ + g_return_val_if_fail (GVC_IS_MIXER_UI_DEVICE (device), 0); + + return device->priv->stream_id; +} + +void +gvc_mixer_ui_device_invalidate_stream (GvcMixerUIDevice *self) +{ + g_return_if_fail (GVC_IS_MIXER_UI_DEVICE (self)); + + self->priv->stream_id = GVC_MIXER_UI_DEVICE_INVALID; +} + +const gchar * +gvc_mixer_ui_device_get_description (GvcMixerUIDevice *device) +{ + g_return_val_if_fail (GVC_IS_MIXER_UI_DEVICE (device), NULL); + + return device->priv->first_line_desc; +} + +const char * +gvc_mixer_ui_device_get_icon_name (GvcMixerUIDevice *device) +{ + g_return_val_if_fail (GVC_IS_MIXER_UI_DEVICE (device), NULL); + + if (device->priv->icon_name) + return device->priv->icon_name; + + if (device->priv->card) + return gvc_mixer_card_get_icon_name (device->priv->card); + + return NULL; +} + +static void +gvc_mixer_ui_device_set_icon_name (GvcMixerUIDevice *device, + const char *icon_name) +{ + g_return_if_fail (GVC_IS_MIXER_UI_DEVICE (device)); + + g_free (device->priv->icon_name); + device->priv->icon_name = g_strdup (icon_name); + g_object_notify_by_pspec (G_OBJECT (device), obj_props[PROP_ICON_NAME]); +} + + +/** + * gvc_mixer_ui_device_get_gicon: + * @device: + * + * Returns: (transfer full): + */ +GIcon * +gvc_mixer_ui_device_get_gicon (GvcMixerUIDevice *device) +{ + const char *icon_name; + + g_return_val_if_fail (GVC_IS_MIXER_UI_DEVICE (device), NULL); + + icon_name = gvc_mixer_ui_device_get_icon_name (device); + + if (icon_name != NULL) + return g_themed_icon_new_with_default_fallbacks (icon_name); + else + return NULL; +} + +const gchar * +gvc_mixer_ui_device_get_origin (GvcMixerUIDevice *device) +{ + g_return_val_if_fail (GVC_IS_MIXER_UI_DEVICE (device), NULL); + + return device->priv->second_line_desc; +} + +const gchar* +gvc_mixer_ui_device_get_user_preferred_profile (GvcMixerUIDevice *device) +{ + g_return_val_if_fail (GVC_IS_MIXER_UI_DEVICE (device), NULL); + + return device->priv->user_preferred_profile; +} + +const gchar * +gvc_mixer_ui_device_get_top_priority_profile (GvcMixerUIDevice *device) +{ + GList *last; + GvcMixerCardProfile *profile; + + g_return_val_if_fail (GVC_IS_MIXER_UI_DEVICE (device), NULL); + + last = g_list_last (device->priv->supported_profiles); + profile = last->data; + + return profile->profile; +} + +void +gvc_mixer_ui_device_set_user_preferred_profile (GvcMixerUIDevice *device, + const gchar *profile) +{ + g_return_if_fail (GVC_IS_MIXER_UI_DEVICE (device)); + g_return_if_fail (profile != NULL); + + g_free (device->priv->user_preferred_profile); + device->priv->user_preferred_profile = g_strdup (profile); +} + +const gchar * +gvc_mixer_ui_device_get_port (GvcMixerUIDevice *device) +{ + g_return_val_if_fail (GVC_IS_MIXER_UI_DEVICE (device), NULL); + + return device->priv->port_name; +} + +gboolean +gvc_mixer_ui_device_has_ports (GvcMixerUIDevice *device) +{ + g_return_val_if_fail (GVC_IS_MIXER_UI_DEVICE (device), FALSE); + + return (device->priv->port_name != NULL); +} + +gboolean +gvc_mixer_ui_device_is_output (GvcMixerUIDevice *device) +{ + g_return_val_if_fail (GVC_IS_MIXER_UI_DEVICE (device), FALSE); + + return (device->priv->type == UIDeviceOutput); +} diff --git a/subprojects/gvc/gvc-mixer-ui-device.h b/subprojects/gvc/gvc-mixer-ui-device.h new file mode 100644 index 0000000..69095cb --- /dev/null +++ b/subprojects/gvc/gvc-mixer-ui-device.h @@ -0,0 +1,85 @@ +/* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 4; tab-width: 4 -*- */ +/* + * Copyright (C) Conor Curran 2011 <conor.curran@canonical.com> + * + * This 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. + * + * gvc-mixer-ui-device.h is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#ifndef _GVC_MIXER_UI_DEVICE_H_ +#define _GVC_MIXER_UI_DEVICE_H_ + +#include <glib-object.h> +#include <gio/gio.h> + +G_BEGIN_DECLS + +#define GVC_TYPE_MIXER_UI_DEVICE (gvc_mixer_ui_device_get_type ()) +#define GVC_MIXER_UI_DEVICE(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), GVC_TYPE_MIXER_UI_DEVICE, GvcMixerUIDevice)) +#define GVC_MIXER_UI_DEVICE_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), GVC_TYPE_MIXER_UI_DEVICE, GvcMixerUIDeviceClass)) +#define GVC_IS_MIXER_UI_DEVICE(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GVC_TYPE_MIXER_UI_DEVICE)) +#define GVC_IS_MIXER_UI_DEVICE_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), GVC_TYPE_MIXER_UI_DEVICE)) +#define GVC_MIXER_UI_DEVICE_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), GVC_TYPE_MIXER_UI_DEVICE, GvcMixerUIDeviceClass)) + +#define GVC_MIXER_UI_DEVICE_INVALID 0 + +typedef struct GvcMixerUIDevicePrivate GvcMixerUIDevicePrivate; + +typedef struct +{ + GObjectClass parent_class; +} GvcMixerUIDeviceClass; + +typedef struct +{ + GObject parent_instance; + GvcMixerUIDevicePrivate *priv; +} GvcMixerUIDevice; + +typedef enum +{ + UIDeviceInput, + UIDeviceOutput, +} GvcMixerUIDeviceDirection; + +GType gvc_mixer_ui_device_get_type (void) G_GNUC_CONST; + +guint gvc_mixer_ui_device_get_id (GvcMixerUIDevice *device); +guint gvc_mixer_ui_device_get_stream_id (GvcMixerUIDevice *device); +const gchar * gvc_mixer_ui_device_get_description (GvcMixerUIDevice *device); +const gchar * gvc_mixer_ui_device_get_icon_name (GvcMixerUIDevice *device); +GIcon * gvc_mixer_ui_device_get_gicon (GvcMixerUIDevice *device); +const gchar * gvc_mixer_ui_device_get_origin (GvcMixerUIDevice *device); +const gchar * gvc_mixer_ui_device_get_port (GvcMixerUIDevice *device); +const gchar * gvc_mixer_ui_device_get_best_profile (GvcMixerUIDevice *device, + const gchar *selected, + const gchar *current); +const gchar * gvc_mixer_ui_device_get_active_profile (GvcMixerUIDevice* device); +const gchar * gvc_mixer_ui_device_get_matching_profile (GvcMixerUIDevice *device, + const gchar *profile); +const gchar * gvc_mixer_ui_device_get_user_preferred_profile (GvcMixerUIDevice *device); +const gchar * gvc_mixer_ui_device_get_top_priority_profile (GvcMixerUIDevice *device); +GList * gvc_mixer_ui_device_get_profiles (GvcMixerUIDevice *device); +GList * gvc_mixer_ui_device_get_supported_profiles (GvcMixerUIDevice *device); +gboolean gvc_mixer_ui_device_should_profiles_be_hidden (GvcMixerUIDevice *device); +void gvc_mixer_ui_device_set_profiles (GvcMixerUIDevice *device, + const GList *in_profiles); +void gvc_mixer_ui_device_set_user_preferred_profile (GvcMixerUIDevice *device, + const gchar *profile); +void gvc_mixer_ui_device_invalidate_stream (GvcMixerUIDevice *device); +gboolean gvc_mixer_ui_device_has_ports (GvcMixerUIDevice *device); +gboolean gvc_mixer_ui_device_is_output (GvcMixerUIDevice *device); + +G_END_DECLS + +#endif /* _GVC_MIXER_UI_DEVICE_H_ */ diff --git a/subprojects/gvc/gvc-pulseaudio-fake.h b/subprojects/gvc/gvc-pulseaudio-fake.h new file mode 100644 index 0000000..92a41b6 --- /dev/null +++ b/subprojects/gvc/gvc-pulseaudio-fake.h @@ -0,0 +1,30 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2008 Red Hat, Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * + */ + +#ifndef __GVC_PULSEAUDIO_FAKE_H +#define __GVC_PULSEAUDIO_FAKE_H + +#ifndef PA_API_VERSION +#define pa_channel_position_t int +#define pa_volume_t guint32 +#define pa_context gpointer +#endif /* PA_API_VERSION */ + +#endif /* __GVC_PULSEAUDIO_FAKE_H */ diff --git a/subprojects/gvc/libgnome-volume-control.doap b/subprojects/gvc/libgnome-volume-control.doap new file mode 100644 index 0000000..2fcc8e1 --- /dev/null +++ b/subprojects/gvc/libgnome-volume-control.doap @@ -0,0 +1,32 @@ +<Project xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:rdfs="http://www.w3.org/2000/01/rdf-schema#" + xmlns:foaf="http://xmlns.com/foaf/0.1/" + xmlns:gnome="http://api.gnome.org/doap-extensions#" + xmlns="http://usefulinc.com/ns/doap#"> + + <name xml:lang="en">libgnome-volume-control</name> + <shortdesc xml:lang="en">GObject layer for PulseAudio</shortdesc> + <description> + This library contains code to access PulseAudio using a GObject + based library, shared between gnome-control-center, gnome-settings-daemon + and gnome-shell. It is not API stable, and it is meant to be used + as a submodule. + </description> + + <!-- <category rdf:resource="http://api.gnome.org/doap-extensions#desktop" /> --> + + <maintainer> + <foaf:Person> + <foaf:name>Giovanni Campagna</foaf:name> + <foaf:mbox rdf:resource="mailto:scampa.giovanni@gmail.com" /> + <gnome:userid>gcampagna</gnome:userid> + </foaf:Person> + </maintainer> + <maintainer> + <foaf:Person> + <foaf:name>Bastien Nocera</foaf:name> + <foaf:mbox rdf:resource="mailto:hadess@hadess.net" /> + <gnome:userid>hadess</gnome:userid> + </foaf:Person> + </maintainer> +</Project> diff --git a/subprojects/gvc/meson.build b/subprojects/gvc/meson.build new file mode 100644 index 0000000..a1a2af5 --- /dev/null +++ b/subprojects/gvc/meson.build @@ -0,0 +1,137 @@ +project('gvc', 'c', + meson_version: '>= 0.42.0', + default_options: ['static=true'] +) + +assert(meson.is_subproject(), 'This project is only intended to be used as a subproject!') + +gnome = import('gnome') + +pkglibdir = get_option('pkglibdir') +pkgdatadir = get_option('pkgdatadir') + +cdata = configuration_data() +cdata.set_quoted('GETTEXT_PACKAGE', get_option('package_name')) +cdata.set_quoted('PACKAGE_VERSION', get_option('package_version')) + +libgvc_gir_headers = [ + 'gvc-channel-map.h', + 'gvc-mixer-card.h', + 'gvc-mixer-control.h', + 'gvc-mixer-event-role.h', + 'gvc-mixer-sink.h', + 'gvc-mixer-sink-input.h', + 'gvc-mixer-source.h', + 'gvc-mixer-source-output.h', + 'gvc-mixer-stream.h', + 'gvc-mixer-ui-device.h' +] + +libgvc_enums = gnome.mkenums_simple('gvc-enum-types', + sources: libgvc_gir_headers +) + +libgvc_gir_sources = [ + 'gvc-channel-map.c', + 'gvc-mixer-card.c', + 'gvc-mixer-control.c', + 'gvc-mixer-event-role.c', + 'gvc-mixer-sink.c', + 'gvc-mixer-sink-input.c', + 'gvc-mixer-source.c', + 'gvc-mixer-source-output.c', + 'gvc-mixer-stream.c', + 'gvc-mixer-ui-device.c' +] + +libgvc_no_gir_sources = [ + 'gvc-mixer-card-private.h', + 'gvc-mixer-stream-private.h', + 'gvc-channel-map-private.h', + 'gvc-mixer-control-private.h', + 'gvc-pulseaudio-fake.h' +] + +libgvc_deps = [ + dependency('gio-2.0'), + dependency('gobject-2.0'), + dependency('libpulse', version: '>= 12.99.3'), + dependency('libpulse-mainloop-glib') +] + +enable_alsa = get_option('alsa') +if enable_alsa + libgvc_deps += dependency('alsa') +endif +cdata.set('HAVE_ALSA', enable_alsa) + +enable_static = get_option('static') +enable_introspection = get_option('introspection') + +assert(not enable_static or not enable_introspection, 'Currently meson requires a shared library for building girs.') +assert(enable_static or pkglibdir != '', 'Installing shared library, but pkglibdir is unset!') + +c_args = ['-DG_LOG_DOMAIN="Gvc"'] + +if enable_introspection + c_args += '-DWITH_INTROSPECTION' +endif + +if enable_static + libgvc_static = static_library('gvc', + sources: libgvc_gir_sources + libgvc_no_gir_sources + libgvc_enums, + dependencies: libgvc_deps, + c_args: c_args + ) + + libgvc = libgvc_static +else + if pkglibdir == '' + error('Installing shared library, but pkglibdir is unset!') + endif + + libgvc_shared = shared_library('gvc', + sources: libgvc_gir_sources + libgvc_no_gir_sources + libgvc_enums, + dependencies: libgvc_deps, + c_args: c_args, + install_dir: pkglibdir, + install: true + ) + + libgvc = libgvc_shared +endif + +if enable_introspection + assert(pkgdatadir != '', 'Installing introspection, but pkgdatadir is unset!') + + libgvc_gir = gnome.generate_gir(libgvc, + sources: libgvc_gir_sources + libgvc_gir_headers + libgvc_enums, + nsversion: '1.0', + namespace: 'Gvc', + includes: ['Gio-2.0', 'GObject-2.0'], + extra_args: ['-DWITH_INTROSPECTION', '--quiet'], + install_dir_gir: pkgdatadir, + install_dir_typelib: pkglibdir, + install: true + ) +endif + +if enable_alsa + executable('test-audio-device-selection', + sources: 'test-audio-device-selection.c', + link_with: libgvc, + dependencies: libgvc_deps, + c_args: c_args + ) +endif + +libgvc_dep = declare_dependency( + link_with: libgvc, + include_directories: include_directories('.'), + dependencies: libgvc_deps +) + +configure_file( + output: 'config.h', + configuration: cdata +) diff --git a/subprojects/gvc/meson_options.txt b/subprojects/gvc/meson_options.txt new file mode 100644 index 0000000..38513e3 --- /dev/null +++ b/subprojects/gvc/meson_options.txt @@ -0,0 +1,41 @@ +option('package_name', + type: 'string', + value: '', + description: 'The value for the GETTEXT_PACKAGE define.' +) + +option('package_version', + type: 'string', + value: '', + description: 'The value for the PACKAGE_VERSION define.' +) + +option('pkglibdir', + type: 'string', + value: '', + description: 'The private directory the shared library/typelib will be installed into.' +) + +option('pkgdatadir', + type: 'string', + value: '', + description: 'The private directory the gir file will be installed into.' +) + +option('alsa', + type: 'boolean', + value: true, + description: 'Build ALSA support.' +) + +option('static', + type: 'boolean', + value: false, + description: 'Build as a static library.' +) + +option('introspection', + type: 'boolean', + value: false, + description: 'Build gobject-introspection support' +) diff --git a/subprojects/gvc/test-audio-device-selection.c b/subprojects/gvc/test-audio-device-selection.c new file mode 100644 index 0000000..8195f9d --- /dev/null +++ b/subprojects/gvc/test-audio-device-selection.c @@ -0,0 +1,84 @@ + +#include <stdio.h> +#include <locale.h> +#include <pulse/pulseaudio.h> +#include "gvc-mixer-control.h" + +#define MAX_ATTEMPTS 3 + +typedef struct { + GvcHeadsetPortChoice choice; + const char *name; +} AudioSelectionChoice; + +static AudioSelectionChoice audio_selection_choices[] = { + { GVC_HEADSET_PORT_CHOICE_HEADPHONES, "headphones" }, + { GVC_HEADSET_PORT_CHOICE_HEADSET, "headset" }, + { GVC_HEADSET_PORT_CHOICE_MIC, "microphone" }, +}; + +static void +audio_selection_needed (GvcMixerControl *volume, + guint id, + gboolean show_dialog, + GvcHeadsetPortChoice choices, + gpointer user_data) +{ + const char *args[G_N_ELEMENTS (audio_selection_choices) + 1]; + guint i, n; + int response = -1; + + if (!show_dialog) { + g_print ("--- Audio selection not needed anymore for id %d\n", id); + return; + } + + n = 0; + for (i = 0; i < G_N_ELEMENTS (audio_selection_choices); ++i) { + if (choices & audio_selection_choices[i].choice) + args[n++] = audio_selection_choices[i].name; + } + args[n] = NULL; + + g_print ("+++ Audio selection needed for id %d\n", id); + g_print (" Choices are:\n"); + for (i = 0; args[i] != NULL; i++) + g_print (" %d. %s\n", i + 1, args[i]); + + for (i = 0; response < 0 && i < MAX_ATTEMPTS; i++) { + int res; + + g_print ("What is your choice?\n"); + if (scanf ("%d", &res) == 1 && + res > 0 && + res < (int) g_strv_length ((char **) args)) { + response = res; + break; + } + } + + gvc_mixer_control_set_headset_port (volume, + id, + audio_selection_choices[response - 1].choice); +} + +int main (int argc, char **argv) +{ + GMainLoop *loop; + GvcMixerControl *volume; + + setlocale (LC_ALL, ""); + + loop = g_main_loop_new (NULL, FALSE); + + volume = gvc_mixer_control_new ("GNOME Volume Control test"); + g_signal_connect (volume, + "audio-device-selection-needed", + G_CALLBACK (audio_selection_needed), + NULL); + gvc_mixer_control_open (volume); + + g_main_loop_run (loop); + + return 0; +} diff --git a/subprojects/shew/COPYING b/subprojects/shew/COPYING new file mode 100644 index 0000000..4362b49 --- /dev/null +++ b/subprojects/shew/COPYING @@ -0,0 +1,502 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 2.1, February 1999 + + Copyright (C) 1991, 1999 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. + +[This is the first released version of the Lesser GPL. It also counts + as the successor of the GNU Library Public License, version 2, hence + the version number 2.1.] + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +Licenses are intended to guarantee your freedom to share and change +free software--to make sure the software is free for all its users. + + This license, the Lesser General Public License, applies to some +specially designated software packages--typically libraries--of the +Free Software Foundation and other authors who decide to use it. You +can use it too, but we suggest you first think carefully about whether +this license or the ordinary General Public License is the better +strategy to use in any particular case, based on the explanations below. + + When we speak of free software, we are referring to freedom of use, +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 and use pieces of +it in new free programs; and that you are informed that you can do +these things. + + To protect your rights, we need to make restrictions that forbid +distributors to deny you these rights or to ask you to surrender these +rights. These restrictions translate to certain responsibilities for +you if you distribute copies of the library or if you modify it. + + For example, if you distribute copies of the library, whether gratis +or for a fee, you must give the recipients all the rights that we gave +you. You must make sure that they, too, receive or can get the source +code. If you link other code with the library, you must provide +complete object files to the recipients, so that they can relink them +with the library after making changes to the library and recompiling +it. And you must show them these terms so they know their rights. + + We protect your rights with a two-step method: (1) we copyright the +library, and (2) we offer you this license, which gives you legal +permission to copy, distribute and/or modify the library. + + To protect each distributor, we want to make it very clear that +there is no warranty for the free library. Also, if the library is +modified by someone else and passed on, the recipients should know +that what they have is not the original version, so that the original +author's reputation will not be affected by problems that might be +introduced by others. + + Finally, software patents pose a constant threat to the existence of +any free program. We wish to make sure that a company cannot +effectively restrict the users of a free program by obtaining a +restrictive license from a patent holder. Therefore, we insist that +any patent license obtained for a version of the library must be +consistent with the full freedom of use specified in this license. + + Most GNU software, including some libraries, is covered by the +ordinary GNU General Public License. This license, the GNU Lesser +General Public License, applies to certain designated libraries, and +is quite different from the ordinary General Public License. We use +this license for certain libraries in order to permit linking those +libraries into non-free programs. + + When a program is linked with a library, whether statically or using +a shared library, the combination of the two is legally speaking a +combined work, a derivative of the original library. The ordinary +General Public License therefore permits such linking only if the +entire combination fits its criteria of freedom. The Lesser General +Public License permits more lax criteria for linking other code with +the library. + + We call this license the "Lesser" General Public License because it +does Less to protect the user's freedom than the ordinary General +Public License. It also provides other free software developers Less +of an advantage over competing non-free programs. These disadvantages +are the reason we use the ordinary General Public License for many +libraries. However, the Lesser license provides advantages in certain +special circumstances. + + For example, on rare occasions, there may be a special need to +encourage the widest possible use of a certain library, so that it becomes +a de-facto standard. To achieve this, non-free programs must be +allowed to use the library. A more frequent case is that a free +library does the same job as widely used non-free libraries. In this +case, there is little to gain by limiting the free library to free +software only, so we use the Lesser General Public License. + + In other cases, permission to use a particular library in non-free +programs enables a greater number of people to use a large body of +free software. For example, permission to use the GNU C Library in +non-free programs enables many more people to use the whole GNU +operating system, as well as its variant, the GNU/Linux operating +system. + + Although the Lesser General Public License is Less protective of the +users' freedom, it does ensure that the user of a program that is +linked with the Library has the freedom and the wherewithal to run +that program using a modified version of the Library. + + The precise terms and conditions for copying, distribution and +modification follow. Pay close attention to the difference between a +"work based on the library" and a "work that uses the library". The +former contains code derived from the library, whereas the latter must +be combined with the library in order to run. + + GNU LESSER GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License Agreement applies to any software library or other +program which contains a notice placed by the copyright holder or +other authorized party saying it may be distributed under the terms of +this Lesser General Public License (also called "this License"). +Each licensee is addressed as "you". + + A "library" means a collection of software functions and/or data +prepared so as to be conveniently linked with application programs +(which use some of those functions and data) to form executables. + + The "Library", below, refers to any such software library or work +which has been distributed under these terms. A "work based on the +Library" means either the Library or any derivative work under +copyright law: that is to say, a work containing the Library or a +portion of it, either verbatim or with modifications and/or translated +straightforwardly into another language. (Hereinafter, translation is +included without limitation in the term "modification".) + + "Source code" for a work means the preferred form of the work for +making modifications to it. For a library, 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 library. + + Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running a program using the Library is not restricted, and output from +such a program is covered only if its contents constitute a work based +on the Library (independent of the use of the Library in a tool for +writing it). Whether that is true depends on what the Library does +and what the program that uses the Library does. + + 1. You may copy and distribute verbatim copies of the Library's +complete 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 distribute a copy of this License along with the +Library. + + 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 Library or any portion +of it, thus forming a work based on the Library, 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) The modified work must itself be a software library. + + b) You must cause the files modified to carry prominent notices + stating that you changed the files and the date of any change. + + c) You must cause the whole of the work to be licensed at no + charge to all third parties under the terms of this License. + + d) If a facility in the modified Library refers to a function or a + table of data to be supplied by an application program that uses + the facility, other than as an argument passed when the facility + is invoked, then you must make a good faith effort to ensure that, + in the event an application does not supply such function or + table, the facility still operates, and performs whatever part of + its purpose remains meaningful. + + (For example, a function in a library to compute square roots has + a purpose that is entirely well-defined independent of the + application. Therefore, Subsection 2d requires that any + application-supplied function or table used by this function must + be optional: if the application does not supply it, the square + root function must still compute square roots.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Library, +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 Library, 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 Library. + +In addition, mere aggregation of another work not based on the Library +with the Library (or with a work based on the Library) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may opt to apply the terms of the ordinary GNU General Public +License instead of this License to a given copy of the Library. To do +this, you must alter all the notices that refer to this License, so +that they refer to the ordinary GNU General Public License, version 2, +instead of to this License. (If a newer version than version 2 of the +ordinary GNU General Public License has appeared, then you can specify +that version instead if you wish.) Do not make any other change in +these notices. + + Once this change is made in a given copy, it is irreversible for +that copy, so the ordinary GNU General Public License applies to all +subsequent copies and derivative works made from that copy. + + This option is useful when you wish to copy part of the code of +the Library into a program that is not a library. + + 4. You may copy and distribute the Library (or a portion or +derivative of it, under Section 2) in object code or executable form +under the terms of Sections 1 and 2 above provided that you 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. + + If distribution of 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 satisfies the requirement to +distribute the source code, even though third parties are not +compelled to copy the source along with the object code. + + 5. A program that contains no derivative of any portion of the +Library, but is designed to work with the Library by being compiled or +linked with it, is called a "work that uses the Library". Such a +work, in isolation, is not a derivative work of the Library, and +therefore falls outside the scope of this License. + + However, linking a "work that uses the Library" with the Library +creates an executable that is a derivative of the Library (because it +contains portions of the Library), rather than a "work that uses the +library". The executable is therefore covered by this License. +Section 6 states terms for distribution of such executables. + + When a "work that uses the Library" uses material from a header file +that is part of the Library, the object code for the work may be a +derivative work of the Library even though the source code is not. +Whether this is true is especially significant if the work can be +linked without the Library, or if the work is itself a library. The +threshold for this to be true is not precisely defined by law. + + If such an object file uses only numerical parameters, data +structure layouts and accessors, and small macros and small inline +functions (ten lines or less in length), then the use of the object +file is unrestricted, regardless of whether it is legally a derivative +work. (Executables containing this object code plus portions of the +Library will still fall under Section 6.) + + Otherwise, if the work is a derivative of the Library, you may +distribute the object code for the work under the terms of Section 6. +Any executables containing that work also fall under Section 6, +whether or not they are linked directly with the Library itself. + + 6. As an exception to the Sections above, you may also combine or +link a "work that uses the Library" with the Library to produce a +work containing portions of the Library, and distribute that work +under terms of your choice, provided that the terms permit +modification of the work for the customer's own use and reverse +engineering for debugging such modifications. + + You must give prominent notice with each copy of the work that the +Library is used in it and that the Library and its use are covered by +this License. You must supply a copy of this License. If the work +during execution displays copyright notices, you must include the +copyright notice for the Library among them, as well as a reference +directing the user to the copy of this License. Also, you must do one +of these things: + + a) Accompany the work with the complete corresponding + machine-readable source code for the Library including whatever + changes were used in the work (which must be distributed under + Sections 1 and 2 above); and, if the work is an executable linked + with the Library, with the complete machine-readable "work that + uses the Library", as object code and/or source code, so that the + user can modify the Library and then relink to produce a modified + executable containing the modified Library. (It is understood + that the user who changes the contents of definitions files in the + Library will not necessarily be able to recompile the application + to use the modified definitions.) + + b) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (1) uses at run time a + copy of the library already present on the user's computer system, + rather than copying library functions into the executable, and (2) + will operate properly with a modified version of the library, if + the user installs one, as long as the modified version is + interface-compatible with the version that the work was made with. + + c) Accompany the work with a written offer, valid for at + least three years, to give the same user the materials + specified in Subsection 6a, above, for a charge no more + than the cost of performing this distribution. + + d) If distribution of the work is made by offering access to copy + from a designated place, offer equivalent access to copy the above + specified materials from the same place. + + e) Verify that the user has already received a copy of these + materials or that you have already sent this user a copy. + + For an executable, the required form of the "work that uses the +Library" must include any data and utility programs needed for +reproducing the executable from it. However, as a special exception, +the materials to be 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. + + It may happen that this requirement contradicts the license +restrictions of other proprietary libraries that do not normally +accompany the operating system. Such a contradiction means you cannot +use both them and the Library together in an executable that you +distribute. + + 7. You may place library facilities that are a work based on the +Library side-by-side in a single library together with other library +facilities not covered by this License, and distribute such a combined +library, provided that the separate distribution of the work based on +the Library and of the other library facilities is otherwise +permitted, and provided that you do these two things: + + a) Accompany the combined library with a copy of the same work + based on the Library, uncombined with any other library + facilities. This must be distributed under the terms of the + Sections above. + + b) Give prominent notice with the combined library of the fact + that part of it is a work based on the Library, and explaining + where to find the accompanying uncombined form of the same work. + + 8. You may not copy, modify, sublicense, link with, or distribute +the Library except as expressly provided under this License. Any +attempt otherwise to copy, modify, sublicense, link with, or +distribute the Library 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. + + 9. 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 Library or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Library (or any work based on the +Library), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Library or works based on it. + + 10. Each time you redistribute the Library (or any work based on the +Library), the recipient automatically receives a license from the +original licensor to copy, distribute, link with or modify the Library +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 with +this License. + + 11. 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 Library at all. For example, if a patent +license would not permit royalty-free redistribution of the Library 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 Library. + +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. + + 12. If the distribution and/or use of the Library is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Library 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. + + 13. The Free Software Foundation may publish revised and/or new +versions of the Lesser 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 Library +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 Library does not specify a +license version number, you may choose any version ever published by +the Free Software Foundation. + + 14. If you wish to incorporate parts of the Library into other free +programs whose distribution conditions are incompatible with these, +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 + + 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO +WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. +EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR +OTHER PARTIES PROVIDE THE LIBRARY "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 +LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. 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 LIBRARY 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 +LIBRARY (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 LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), 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 Libraries + + If you develop a new library, and you want it to be of the greatest +possible use to the public, we recommend making it free software that +everyone can redistribute and change. You can do so by permitting +redistribution under these terms (or, alternatively, under the terms of the +ordinary General Public License). + + To apply these terms, attach the following notices to the library. 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. + + <one line to give the library's name and a brief idea of what it does.> + Copyright (C) <year> <name of author> + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; 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. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the library, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the + library `Frob' (a library for tweaking knobs) written by James Random Hacker. + + <signature of Ty Coon>, 1 April 1990 + Ty Coon, President of Vice + +That's all there is to it! diff --git a/subprojects/shew/README.md b/subprojects/shew/README.md new file mode 100644 index 0000000..de15991 --- /dev/null +++ b/subprojects/shew/README.md @@ -0,0 +1,24 @@ +# Shell External Windows + +Shew is a small support library for dealing with external windows. + +The code for creating external windows from a handle that are suitable for +setting a transient parent was copied from [xdg-desktop-portal-gtk]. + +The code for exporting a handle for a GtkWindow is losely based on code +from GTK/[libportal]. + +## Stability +This is an unstable library with no API or ABI guarantees, and no +soname versioning. + +It is not recommended to use it outside the gnome-shell project, and +may only be used as a subproject. + +## License +shew is distributed under the terms of the GNU Lesser General Public License +version 2.1 or later. See the [COPYING][license] file for details. + +[xdg-desktop-portal-gtk]: https://github.com/flatpak/xdg-desktop-portal-gtk +[libportal]: https://github.com/flatpak/libportal +[license]: COPYING diff --git a/subprojects/shew/meson.build b/subprojects/shew/meson.build new file mode 100644 index 0000000..58a2499 --- /dev/null +++ b/subprojects/shew/meson.build @@ -0,0 +1,28 @@ +project('shew', 'c', + version: '43.9', + meson_version: '>= 0.58.0', + license: 'LGPLv2+', +) + +assert(meson.is_subproject(), 'This project is only intended to be used as a subproject!') + +gnome = import('gnome') +pkg = import('pkgconfig') + +api_version = '0' +full_name = '@0@-@1@'.format(meson.project_name(), api_version) + +package_version = meson.project_version() +package_name = get_option('package_name') +assert(package_name != '', 'package_name must be specified') + +pkgdatadir = join_paths(get_option('datadir'), package_name) +pkglibdir = join_paths(get_option('libdir'), package_name) + +girdir = join_paths(pkgdatadir, 'gir-1.0') +typelibdir = join_paths(pkglibdir, 'girepository-1.0') + +gtk_dep = dependency('gtk4') +x11_dep = dependency('x11', required: false) + +subdir('src') diff --git a/subprojects/shew/meson_options.txt b/subprojects/shew/meson_options.txt new file mode 100644 index 0000000..b5c0f5c --- /dev/null +++ b/subprojects/shew/meson_options.txt @@ -0,0 +1,4 @@ +option('package_name', + type: 'string', + description: 'Parent package the library is built into' +) diff --git a/subprojects/shew/src/meson.build b/subprojects/shew/src/meson.build new file mode 100644 index 0000000..e590a46 --- /dev/null +++ b/subprojects/shew/src/meson.build @@ -0,0 +1,29 @@ +shew_public_headers = files( + 'shew-external-window.h', + 'shew-window-exporter.h', +) + +shew_sources = [ + 'shew-external-window-wayland.c', + 'shew-external-window-x11.c', + 'shew-external-window.c', + 'shew-window-exporter.c', +] + +libshew = library(full_name, + sources: shew_sources, + dependencies: [gtk_dep, x11_dep], + install_dir: pkglibdir, + install: true, +) + +libshew_gir = gnome.generate_gir(libshew, + sources: shew_sources + shew_public_headers, + nsversion: api_version, + namespace: 'Shew', + includes: ['Gdk-4.0', 'Gtk-4.0'], + extra_args: ['--quiet'], + install_dir_gir: girdir, + install_dir_typelib: typelibdir, + install: true, +) diff --git a/subprojects/shew/src/shew-external-window-wayland.c b/subprojects/shew/src/shew-external-window-wayland.c new file mode 100644 index 0000000..3d51aa8 --- /dev/null +++ b/subprojects/shew/src/shew-external-window-wayland.c @@ -0,0 +1,117 @@ +/* + * Copyright © 2016 Red Hat, Inc + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see <http://www.gnu.org/licenses/>. + * + * Authors: + * Jonas Ådahl <jadahl@redhat.com> + */ + +#include <gdk/gdk.h> + +#ifdef GDK_WINDOWING_WAYLAND +#include <gdk/wayland/gdkwayland.h> +#endif + +#include "shew-external-window-wayland.h" + +static GdkDisplay *wayland_display; + +struct _ShewExternalWindowWayland +{ + ShewExternalWindow parent; + + char *handle_str; +}; + +G_DEFINE_TYPE (ShewExternalWindowWayland, shew_external_window_wayland, + SHEW_TYPE_EXTERNAL_WINDOW) + +static GdkDisplay * +get_wayland_display (void) +{ + if (wayland_display) + return wayland_display; + + gdk_set_allowed_backends ("wayland"); + wayland_display = gdk_display_open (NULL); + gdk_set_allowed_backends (NULL); + + if (!wayland_display) + g_warning ("Failed to open Wayland display"); + + return wayland_display; +} + +ShewExternalWindowWayland * +shew_external_window_wayland_new (const char *handle_str) +{ + ShewExternalWindowWayland *external_window_wayland; + GdkDisplay *display; + + display = get_wayland_display (); + if (!display) + { + g_warning ("No Wayland display connection, ignoring Wayland parent"); + return NULL; + } + + external_window_wayland = g_object_new (SHEW_TYPE_EXTERNAL_WINDOW_WAYLAND, + "display", display, + NULL); + external_window_wayland->handle_str = g_strdup (handle_str); + + return external_window_wayland; +} + +static void +shew_external_window_wayland_set_parent_of (ShewExternalWindow *external_window, + GdkSurface *child_surface) +{ + ShewExternalWindowWayland *external_window_wayland = + SHEW_EXTERNAL_WINDOW_WAYLAND (external_window); + char *handle_str = external_window_wayland->handle_str; + +#ifdef GDK_WINDOWING_WAYLAND + if (!gdk_wayland_toplevel_set_transient_for_exported (GDK_WAYLAND_TOPLEVEL (child_surface), handle_str)) + g_warning ("Failed to set portal window transient for external parent"); +#endif +} + +static void +shew_external_window_wayland_dispose (GObject *object) +{ + ShewExternalWindowWayland *external_window_wayland = + SHEW_EXTERNAL_WINDOW_WAYLAND (object); + + g_free (external_window_wayland->handle_str); + + G_OBJECT_CLASS (shew_external_window_wayland_parent_class)->dispose (object); +} + +static void +shew_external_window_wayland_init (ShewExternalWindowWayland *external_window_wayland) +{ +} + +static void +shew_external_window_wayland_class_init (ShewExternalWindowWaylandClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + ShewExternalWindowClass *external_window_class = SHEW_EXTERNAL_WINDOW_CLASS (klass); + + object_class->dispose = shew_external_window_wayland_dispose; + + external_window_class->set_parent_of = shew_external_window_wayland_set_parent_of; +} diff --git a/subprojects/shew/src/shew-external-window-wayland.h b/subprojects/shew/src/shew-external-window-wayland.h new file mode 100644 index 0000000..c9e9fd7 --- /dev/null +++ b/subprojects/shew/src/shew-external-window-wayland.h @@ -0,0 +1,30 @@ +/* + * Copyright © 2016 Red Hat, Inc + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see <http://www.gnu.org/licenses/>. + * + * Authors: + * Jonas Ådahl <jadahl@redhat.com> + */ + +#pragma once + +#include <glib-object.h> + +#include "shew-external-window.h" + +#define SHEW_TYPE_EXTERNAL_WINDOW_WAYLAND (shew_external_window_wayland_get_type ()) +G_DECLARE_FINAL_TYPE (ShewExternalWindowWayland, shew_external_window_wayland, SHEW, EXTERNAL_WINDOW_WAYLAND, ShewExternalWindow) + +ShewExternalWindowWayland *shew_external_window_wayland_new (const char *handle_str); diff --git a/subprojects/shew/src/shew-external-window-x11.c b/subprojects/shew/src/shew-external-window-x11.c new file mode 100644 index 0000000..7f08665 --- /dev/null +++ b/subprojects/shew/src/shew-external-window-x11.c @@ -0,0 +1,136 @@ +/* + * Copyright © 2016 Red Hat, Inc + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see <http://www.gnu.org/licenses/>. + * + * Authors: + * Jonas Ådahl <jadahl@redhat.com> + */ + +#include <errno.h> +#include <gdk/gdk.h> +#ifdef GDK_WINDOWING_X11 +#include <gdk/x11/gdkx.h> +#include <X11/Xlib.h> +#endif +#include <stdlib.h> + +#include "shew-external-window-x11.h" + +static GdkDisplay *x11_display; + +struct _ShewExternalWindowX11 +{ + ShewExternalWindow parent; + + int foreign_xid; +}; + +G_DEFINE_TYPE (ShewExternalWindowX11, shew_external_window_x11, + SHEW_TYPE_EXTERNAL_WINDOW) + +static GdkDisplay * +get_x11_display (void) +{ + if (x11_display) + return x11_display; + + gdk_set_allowed_backends ("x11"); + x11_display = gdk_display_open (NULL); + gdk_set_allowed_backends (NULL); + if (!x11_display) + g_warning ("Failed to open X11 display"); + + return x11_display; +} + +static gboolean +check_foreign_xid (GdkDisplay *display, + int xid) +{ + gboolean result = FALSE; +#ifdef GDK_WINDOWING_X11 + XWindowAttributes attrs; + + gdk_x11_display_error_trap_push (display); + result = XGetWindowAttributes (GDK_DISPLAY_XDISPLAY (display), xid, &attrs); + if (gdk_x11_display_error_trap_pop (display)) + return FALSE; + +#endif + return result; +} + +ShewExternalWindowX11 * +shew_external_window_x11_new (const char *handle_str) +{ + ShewExternalWindowX11 *external_window_x11; + GdkDisplay *display; + int xid; + + display = get_x11_display (); + if (!display) + { + g_warning ("No X display connection, ignoring X11 parent"); + return NULL; + } + + errno = 0; + xid = strtol (handle_str, NULL, 16); + if (errno != 0) + { + g_warning ("Failed to reference external X11 window, invalid XID %s", handle_str); + return NULL; + } + + if (!check_foreign_xid (display, xid)) + { + g_warning ("Failed to find foreign window for XID %d", xid); + return NULL; + } + + external_window_x11 = g_object_new (SHEW_TYPE_EXTERNAL_WINDOW_X11, + "display", display, + NULL); + external_window_x11->foreign_xid = xid; + + return external_window_x11; +} + +static void +shew_external_window_x11_set_parent_of (ShewExternalWindow *external_window, + GdkSurface *child_surface) +{ + ShewExternalWindowX11 *external_window_x11 = + SHEW_EXTERNAL_WINDOW_X11 (external_window); + +#ifdef GDK_WINDOWING_X11 + XSetTransientForHint (GDK_SURFACE_XDISPLAY (child_surface), + GDK_SURFACE_XID (child_surface), + external_window_x11->foreign_xid); +#endif +} + +static void +shew_external_window_x11_init (ShewExternalWindowX11 *external_window_x11) +{ +} + +static void +shew_external_window_x11_class_init (ShewExternalWindowX11Class *klass) +{ + ShewExternalWindowClass *external_window_class = SHEW_EXTERNAL_WINDOW_CLASS (klass); + + external_window_class->set_parent_of = shew_external_window_x11_set_parent_of; +} diff --git a/subprojects/shew/src/shew-external-window-x11.h b/subprojects/shew/src/shew-external-window-x11.h new file mode 100644 index 0000000..ed0bab4 --- /dev/null +++ b/subprojects/shew/src/shew-external-window-x11.h @@ -0,0 +1,30 @@ +/* + * Copyright © 2016 Red Hat, Inc + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see <http://www.gnu.org/licenses/>. + * + * Authors: + * Jonas Ådahl <jadahl@redhat.com> + */ + +#pragma once + +#include <glib-object.h> + +#include "shew-external-window.h" + +#define SHEW_TYPE_EXTERNAL_WINDOW_X11 (shew_external_window_x11_get_type ()) +G_DECLARE_FINAL_TYPE (ShewExternalWindowX11, shew_external_window_x11, SHEW, EXTERNAL_WINDOW_X11, ShewExternalWindow) + +ShewExternalWindowX11 *shew_external_window_x11_new (const char *handle_str); diff --git a/subprojects/shew/src/shew-external-window.c b/subprojects/shew/src/shew-external-window.c new file mode 100644 index 0000000..118d93f --- /dev/null +++ b/subprojects/shew/src/shew-external-window.c @@ -0,0 +1,161 @@ +/* + * Copyright © 2016 Red Hat, Inc + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see <http://www.gnu.org/licenses/>. + * + * Authors: + * Jonas Ådahl <jadahl@redhat.com> + */ + +#include <string.h> + +#include "shew-external-window.h" +#include "shew-external-window-x11.h" +#include "shew-external-window-wayland.h" + +enum +{ + PROP_0, + + PROP_DISPLAY, +}; + +typedef struct _ShewExternalWindowPrivate +{ + GdkDisplay *display; +} ShewExternalWindowPrivate; + +G_DEFINE_TYPE_WITH_PRIVATE (ShewExternalWindow, shew_external_window, G_TYPE_OBJECT) + +ShewExternalWindow * +shew_external_window_new_from_handle (const char *handle_str) +{ +#ifdef GDK_WINDOWING_X11 + { + const char x11_prefix[] = "x11:"; + if (g_str_has_prefix (handle_str, x11_prefix)) + { + ShewExternalWindowX11 *external_window_x11; + const char *x11_handle_str = handle_str + strlen (x11_prefix); + + external_window_x11 = shew_external_window_x11_new (x11_handle_str); + return SHEW_EXTERNAL_WINDOW (external_window_x11); + } + } +#endif +#ifdef GDK_WINDOWING_WAYLAND + { + const char wayland_prefix[] = "wayland:"; + if (g_str_has_prefix (handle_str, wayland_prefix)) + { + ShewExternalWindowWayland *external_window_wayland; + const char *wayland_handle_str = handle_str + strlen (wayland_prefix); + + external_window_wayland = + shew_external_window_wayland_new (wayland_handle_str); + return SHEW_EXTERNAL_WINDOW (external_window_wayland); + } + } +#endif + + g_warning ("Unhandled parent window type %s\n", handle_str); + return NULL; +} + +void +shew_external_window_set_parent_of (ShewExternalWindow *external_window, + GdkSurface *child_surface) +{ + SHEW_EXTERNAL_WINDOW_GET_CLASS (external_window)->set_parent_of (external_window, + child_surface); +} + +/** + * shew_external_window_get_display: + * Returns: (transfer none) + */ +GdkDisplay * +shew_external_window_get_display (ShewExternalWindow *external_window) +{ + ShewExternalWindowPrivate *priv = + shew_external_window_get_instance_private (external_window); + + return priv->display; +} + +static void +shew_external_window_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + ShewExternalWindow *external_window = SHEW_EXTERNAL_WINDOW (object); + ShewExternalWindowPrivate *priv = + shew_external_window_get_instance_private (external_window); + + switch (prop_id) + { + case PROP_DISPLAY: + g_set_object (&priv->display, g_value_get_object (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +shew_external_window_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + ShewExternalWindow *external_window = SHEW_EXTERNAL_WINDOW (object); + ShewExternalWindowPrivate *priv = + shew_external_window_get_instance_private (external_window); + + switch (prop_id) + { + case PROP_DISPLAY: + g_value_set_object (value, priv->display); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +shew_external_window_init (ShewExternalWindow *external_window) +{ +} + +static void +shew_external_window_class_init (ShewExternalWindowClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->get_property = shew_external_window_get_property; + object_class->set_property = shew_external_window_set_property; + + g_object_class_install_property (object_class, + PROP_DISPLAY, + g_param_spec_object ("display", + "GdkDisplay", + "The GdkDisplay instance", + GDK_TYPE_DISPLAY, + G_PARAM_READWRITE | + G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS)); +} diff --git a/subprojects/shew/src/shew-external-window.h b/subprojects/shew/src/shew-external-window.h new file mode 100644 index 0000000..ca6671b --- /dev/null +++ b/subprojects/shew/src/shew-external-window.h @@ -0,0 +1,43 @@ +/* + * Copyright © 2016 Red Hat, Inc + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see <http://www.gnu.org/licenses/>. + * + * Authors: + * Jonas Ådahl <jadahl@redhat.com> + */ + +#pragma once + +#include <glib-object.h> +#include <gtk/gtk.h> + + +#define SHEW_TYPE_EXTERNAL_WINDOW (shew_external_window_get_type ()) +G_DECLARE_DERIVABLE_TYPE (ShewExternalWindow, shew_external_window, SHEW, EXTERNAL_WINDOW, GObject) + +struct _ShewExternalWindowClass +{ + GObjectClass parent_class; + + void (*set_parent_of) (ShewExternalWindow *external_window, + GdkSurface *child_surface); +}; + +ShewExternalWindow *shew_external_window_new_from_handle (const char *handle_str); + +void shew_external_window_set_parent_of (ShewExternalWindow *external_window, + GdkSurface *child_surface); + +GdkDisplay *shew_external_window_get_display (ShewExternalWindow *external_window); diff --git a/subprojects/shew/src/shew-window-exporter.c b/subprojects/shew/src/shew-window-exporter.c new file mode 100644 index 0000000..ab84bf8 --- /dev/null +++ b/subprojects/shew/src/shew-window-exporter.c @@ -0,0 +1,217 @@ +/* + * Copyright © 2020 Red Hat, Inc + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see <http://www.gnu.org/licenses/>. + * + * Authors: + * Florian Müllner <fmuellner@gnome.org> + */ + +#include "shew-window-exporter.h" + +#ifdef GDK_WINDOWING_X11 +#include <gdk/x11/gdkx.h> +#endif +#ifdef GDK_WINDOWING_WAYLAND +#include <gdk/wayland/gdkwayland.h> +#endif + +struct _ShewWindowExporter +{ + GObject parent; + + GtkWindow *window; +}; + +G_DEFINE_TYPE (ShewWindowExporter, shew_window_exporter, G_TYPE_OBJECT) + +enum +{ + PROP_0, + + PROP_WINDOW, +}; + +ShewWindowExporter * +shew_window_exporter_new (GtkWindow *window) +{ + return g_object_new (SHEW_TYPE_WINDOW_EXPORTER, + "window", window, + NULL); +} + +#ifdef GDK_WINDOWING_WAYLAND +static void +wayland_window_exported (GdkToplevel *toplevel, + const char *handle, + gpointer user_data) +{ + g_autoptr (GTask) task = user_data; + + g_task_return_pointer (task, g_strdup_printf ("wayland:%s", handle), g_free); +} +#endif + +void +shew_window_exporter_export (ShewWindowExporter *exporter, + GAsyncReadyCallback callback, + gpointer user_data) +{ + g_autoptr (GTask) task = NULL; + GtkWidget *widget; + + g_return_if_fail (SHEW_IS_WINDOW_EXPORTER (exporter)); + + if (exporter->window == NULL) + { + g_task_report_new_error (exporter, callback, user_data, + shew_window_exporter_export, + G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED, + "No window to export"); + return; + } + + task = g_task_new (exporter, NULL, callback, user_data); + g_task_set_source_tag (task, shew_window_exporter_export); + + widget = GTK_WIDGET (exporter->window); + +#ifdef GDK_WINDOWING_X11 + if (GDK_IS_X11_DISPLAY (gtk_widget_get_display (widget))) + { + GdkSurface *s = gtk_native_get_surface (GTK_NATIVE (widget)); + guint32 xid = (guint32) gdk_x11_surface_get_xid (s); + + g_task_return_pointer (task, g_strdup_printf ("x11:%x", xid), g_free); + } +#endif + +#ifdef GDK_WINDOWING_WAYLAND + if (GDK_IS_WAYLAND_DISPLAY (gtk_widget_get_display (widget))) + { + GdkSurface *s = gtk_native_get_surface (GTK_NATIVE (widget)); + gdk_wayland_toplevel_export_handle (GDK_WAYLAND_TOPLEVEL (s), + wayland_window_exported, + g_steal_pointer (&task), NULL); + } +#endif + + if (task != NULL && !g_task_get_completed (task)) + { + g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED, + "Unsupported windowing system"); + } +} + +char * +shew_window_exporter_export_finish (ShewWindowExporter *exporter, + GAsyncResult *result, + GError **error) +{ + g_return_val_if_fail (SHEW_IS_WINDOW_EXPORTER (exporter), NULL); + g_return_val_if_fail (g_async_result_is_tagged (result, shew_window_exporter_export), NULL); + + return g_task_propagate_pointer (G_TASK (result), error); +} + +void +shew_window_exporter_unexport (ShewWindowExporter *exporter) +{ + GtkWidget *widget; + + g_return_if_fail (SHEW_IS_WINDOW_EXPORTER (exporter)); + + widget = GTK_WIDGET (exporter->window); + +#ifdef GDK_WINDOWING_WAYLAND + if (GDK_IS_WAYLAND_DISPLAY (gtk_widget_get_display (widget))) + { + GdkSurface *s = gtk_native_get_surface (GTK_NATIVE (widget)); + gdk_wayland_toplevel_unexport_handle (GDK_WAYLAND_TOPLEVEL (s)); + } +#endif +} + +static void +shew_window_exporter_dispose (GObject *object) +{ + ShewWindowExporter *exporter = SHEW_WINDOW_EXPORTER (object); + + g_clear_object (&exporter->window); + + G_OBJECT_CLASS (shew_window_exporter_parent_class)->dispose (object); +} + +static void +shew_window_exporter_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + ShewWindowExporter *exporter = SHEW_WINDOW_EXPORTER (object); + + switch (prop_id) + { + case PROP_WINDOW: + g_set_object (&exporter->window, g_value_get_object (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +shew_window_exporter_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + ShewWindowExporter *exporter = SHEW_WINDOW_EXPORTER (object); + + switch (prop_id) + { + case PROP_WINDOW: + g_value_set_object (value, exporter->window); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +shew_window_exporter_init (ShewWindowExporter *exporter) +{ +} + +static void +shew_window_exporter_class_init (ShewWindowExporterClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->get_property = shew_window_exporter_get_property; + object_class->set_property = shew_window_exporter_set_property; + object_class->dispose = shew_window_exporter_dispose; + + g_object_class_install_property (object_class, + PROP_WINDOW, + g_param_spec_object ("window", + "GtkWindow", + "The GtkWindow to export", + GTK_TYPE_WINDOW, + G_PARAM_READWRITE | + G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS)); +} diff --git a/subprojects/shew/src/shew-window-exporter.h b/subprojects/shew/src/shew-window-exporter.h new file mode 100644 index 0000000..224fff5 --- /dev/null +++ b/subprojects/shew/src/shew-window-exporter.h @@ -0,0 +1,38 @@ +/* + * Copyright © 2020 Red Hat, Inc + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see <http://www.gnu.org/licenses/>. + * + * Authors: + * Florian Müllner <fmuellner@gnome.org> + */ + +#pragma once + +#include <gtk/gtk.h> + +#define SHEW_TYPE_WINDOW_EXPORTER (shew_window_exporter_get_type ()) +G_DECLARE_FINAL_TYPE (ShewWindowExporter, shew_window_exporter, SHEW, WINDOW_EXPORTER, GObject) + +ShewWindowExporter *shew_window_exporter_new (GtkWindow *window); + +void shew_window_exporter_export (ShewWindowExporter *exporter, + GAsyncReadyCallback callback, + gpointer user_data); + +char *shew_window_exporter_export_finish (ShewWindowExporter *exporter, + GAsyncResult *result, + GError **error); + +void shew_window_exporter_unexport (ShewWindowExporter *exporter); |