summaryrefslogtreecommitdiffstats
path: root/subprojects
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--subprojects/extensions-app/COPYING339
-rw-r--r--subprojects/extensions-app/README.md32
-rw-r--r--subprojects/extensions-app/build-aux/flatpak/org.gnome.Extensions.json39
l---------subprojects/extensions-app/build-aux/meson/check-version.py1
-rw-r--r--subprojects/extensions-app/data/css/style.css21
l---------subprojects/extensions-app/data/dbus-interfaces/org.gnome.Shell.Extensions.xml1
-rw-r--r--subprojects/extensions-app/data/icons/hicolor/scalable/apps/org.gnome.Extensions.Devel.svg1
-rw-r--r--subprojects/extensions-app/data/icons/hicolor/scalable/apps/org.gnome.Extensions.svg1
-rw-r--r--subprojects/extensions-app/data/icons/hicolor/symbolic/apps/org.gnome.Extensions-symbolic.svg1
-rw-r--r--subprojects/extensions-app/data/icons/meson.build1
-rw-r--r--subprojects/extensions-app/data/meson.build50
-rw-r--r--subprojects/extensions-app/data/metainfo/extensions-main.pngbin0 -> 44240 bytes
-rw-r--r--subprojects/extensions-app/data/metainfo/extensions-remove.pngbin0 -> 42278 bytes
-rw-r--r--subprojects/extensions-app/data/metainfo/extensions-update.pngbin0 -> 53240 bytes
-rw-r--r--subprojects/extensions-app/data/metainfo/meson.build16
-rw-r--r--subprojects/extensions-app/data/metainfo/org.gnome.Extensions.metainfo.xml.in67
-rw-r--r--subprojects/extensions-app/data/org.gnome.Extensions.data.gresource.xml.in13
-rw-r--r--subprojects/extensions-app/data/org.gnome.Extensions.desktop.in.in10
-rw-r--r--subprojects/extensions-app/data/org.gnome.Extensions.service.in3
-rw-r--r--subprojects/extensions-app/data/ui/extension-row.ui143
-rw-r--r--subprojects/extensions-app/data/ui/extensions-window.ui204
-rwxr-xr-xsubprojects/extensions-app/generate-translations.sh19
-rw-r--r--subprojects/extensions-app/js/gnome-extensions-app.in2
-rw-r--r--subprojects/extensions-app/js/main.js548
-rw-r--r--subprojects/extensions-app/js/meson.build40
-rw-r--r--subprojects/extensions-app/js/misc/config.js1
-rw-r--r--subprojects/extensions-app/js/org.gnome.Extensions.in6
-rw-r--r--subprojects/extensions-app/js/org.gnome.Extensions.src.gresource.xml.in9
-rw-r--r--subprojects/extensions-app/logo.pngbin0 -> 2365 bytes
-rw-r--r--subprojects/extensions-app/meson.build90
-rw-r--r--subprojects/extensions-app/meson_options.txt12
-rw-r--r--subprojects/extensions-app/po/.gitignore3
l---------subprojects/extensions-app/po/LINGUAS1
-rw-r--r--subprojects/extensions-app/po/meson.build1
l---------subprojects/extensions-app/subprojects/shew1
-rw-r--r--subprojects/extensions-tool/COPYING675
-rw-r--r--subprojects/extensions-tool/README.md23
-rw-r--r--subprojects/extensions-tool/completion/bash/gnome-extensions91
-rwxr-xr-xsubprojects/extensions-tool/generate-translations.sh19
-rw-r--r--subprojects/extensions-tool/man/gnome-extensions.1297
-rw-r--r--subprojects/extensions-tool/man/gnome-extensions.txt211
-rw-r--r--subprojects/extensions-tool/man/meson.build7
-rw-r--r--subprojects/extensions-tool/man/stylesheet.xsl27
-rw-r--r--subprojects/extensions-tool/meson.build84
-rw-r--r--subprojects/extensions-tool/meson_options.txt17
-rw-r--r--subprojects/extensions-tool/po/.gitignore3
l---------subprojects/extensions-tool/po/LINGUAS1
-rw-r--r--subprojects/extensions-tool/po/meson.build1
-rw-r--r--subprojects/extensions-tool/src/command-create.c506
-rw-r--r--subprojects/extensions-tool/src/command-disable.c126
-rw-r--r--subprojects/extensions-tool/src/command-enable.c126
-rw-r--r--subprojects/extensions-tool/src/command-info.c113
-rw-r--r--subprojects/extensions-tool/src/command-install.c213
-rw-r--r--subprojects/extensions-tool/src/command-list.c196
-rw-r--r--subprojects/extensions-tool/src/command-pack.c516
-rw-r--r--subprojects/extensions-tool/src/command-prefs.c115
-rw-r--r--subprojects/extensions-tool/src/command-reset.c86
-rw-r--r--subprojects/extensions-tool/src/command-uninstall.c114
-rw-r--r--subprojects/extensions-tool/src/commands.h38
-rw-r--r--subprojects/extensions-tool/src/common.h73
-rw-r--r--subprojects/extensions-tool/src/gnome-extensions-tool.gresource.xml11
-rw-r--r--subprojects/extensions-tool/src/main.c412
-rw-r--r--subprojects/extensions-tool/src/meson.build37
-rw-r--r--subprojects/extensions-tool/src/templates/00-plain.desktop.in5
-rw-r--r--subprojects/extensions-tool/src/templates/indicator.desktop.in5
-rw-r--r--subprojects/extensions-tool/src/templates/indicator/extension.js70
-rw-r--r--subprojects/extensions-tool/src/templates/indicator/stylesheet.css1
-rw-r--r--subprojects/extensions-tool/src/templates/meson.build13
-rw-r--r--subprojects/extensions-tool/src/templates/plain/extension.js34
-rw-r--r--subprojects/extensions-tool/src/templates/plain/stylesheet.css1
-rw-r--r--subprojects/gvc/.gitignore11
-rw-r--r--subprojects/gvc/.gitlab-ci.yml16
-rw-r--r--subprojects/gvc/.gitlab-ci/meson.build23
l---------subprojects/gvc/.gitlab-ci/subprojects/gvc1
-rw-r--r--subprojects/gvc/README.md12
-rw-r--r--subprojects/gvc/gvc-channel-map-private.h39
-rw-r--r--subprojects/gvc/gvc-channel-map.c246
-rw-r--r--subprojects/gvc/gvc-channel-map.h73
-rw-r--r--subprojects/gvc/gvc-mixer-card-private.h35
-rw-r--r--subprojects/gvc/gvc-mixer-card.c574
-rw-r--r--subprojects/gvc/gvc-mixer-card.h102
-rw-r--r--subprojects/gvc/gvc-mixer-control-private.h35
-rw-r--r--subprojects/gvc/gvc-mixer-control.c3881
-rw-r--r--subprojects/gvc/gvc-mixer-control.h155
-rw-r--r--subprojects/gvc/gvc-mixer-event-role.c229
-rw-r--r--subprojects/gvc/gvc-mixer-event-role.h57
-rw-r--r--subprojects/gvc/gvc-mixer-sink-input.c159
-rw-r--r--subprojects/gvc/gvc-mixer-sink-input.h57
-rw-r--r--subprojects/gvc/gvc-mixer-sink.c189
-rw-r--r--subprojects/gvc/gvc-mixer-sink.h57
-rw-r--r--subprojects/gvc/gvc-mixer-source-output.c160
-rw-r--r--subprojects/gvc/gvc-mixer-source-output.h57
-rw-r--r--subprojects/gvc/gvc-mixer-source.c189
-rw-r--r--subprojects/gvc/gvc-mixer-source.h57
-rw-r--r--subprojects/gvc/gvc-mixer-stream-private.h34
-rw-r--r--subprojects/gvc/gvc-mixer-stream.c1055
-rw-r--r--subprojects/gvc/gvc-mixer-stream.h146
-rw-r--r--subprojects/gvc/gvc-mixer-ui-device.c744
-rw-r--r--subprojects/gvc/gvc-mixer-ui-device.h85
-rw-r--r--subprojects/gvc/gvc-pulseaudio-fake.h30
-rw-r--r--subprojects/gvc/libgnome-volume-control.doap32
-rw-r--r--subprojects/gvc/meson.build137
-rw-r--r--subprojects/gvc/meson_options.txt41
-rw-r--r--subprojects/gvc/test-audio-device-selection.c84
-rw-r--r--subprojects/shew/COPYING502
-rw-r--r--subprojects/shew/README.md24
-rw-r--r--subprojects/shew/meson.build28
-rw-r--r--subprojects/shew/meson_options.txt4
-rw-r--r--subprojects/shew/src/meson.build29
-rw-r--r--subprojects/shew/src/shew-external-window-wayland.c117
-rw-r--r--subprojects/shew/src/shew-external-window-wayland.h30
-rw-r--r--subprojects/shew/src/shew-external-window-x11.c136
-rw-r--r--subprojects/shew/src/shew-external-window-x11.h30
-rw-r--r--subprojects/shew/src/shew-external-window.c161
-rw-r--r--subprojects/shew/src/shew-external-window.h43
-rw-r--r--subprojects/shew/src/shew-window-exporter.c217
-rw-r--r--subprojects/shew/src/shew-window-exporter.h38
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
new file mode 100644
index 0000000..7d3de2b
--- /dev/null
+++ b/subprojects/extensions-app/data/metainfo/extensions-main.png
Binary files differ
diff --git a/subprojects/extensions-app/data/metainfo/extensions-remove.png b/subprojects/extensions-app/data/metainfo/extensions-remove.png
new file mode 100644
index 0000000..d54e201
--- /dev/null
+++ b/subprojects/extensions-app/data/metainfo/extensions-remove.png
Binary files differ
diff --git a/subprojects/extensions-app/data/metainfo/extensions-update.png b/subprojects/extensions-app/data/metainfo/extensions-update.png
new file mode 100644
index 0000000..4b91cd8
--- /dev/null
+++ b/subprojects/extensions-app/data/metainfo/extensions-update.png
Binary files differ
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'>&lt;Control&gt;f</property>
+ <property name='action'>activate</property>
+ </object>
+ </child>
+ <child>
+ <object class='GtkShortcut'>
+ <property name='trigger'>&lt;Control&gt;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 &lt;a href="https://extensions.gnome.org"&gt;extensions.gnome.org&lt;/a&gt;.</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 &lt;a href="https://extensions.gnome.org"&gt;extensions.gnome.org&lt;/a&gt;.</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
new file mode 100644
index 0000000..8872925
--- /dev/null
+++ b/subprojects/extensions-app/logo.png
Binary files differ
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&#10;</xsl:text>
+ <xsl:call-template name="bold">
+ <xsl:with-param name="node" select="."/>
+ <xsl:with-param name="context" select=".."/>
+ </xsl:call-template>
+ <xsl:text>&#10;</xsl:text>
+</xsl:template>
+
+<xsl:template match="varlistentry[preceding-sibling::title]">
+ <xsl:if test="not(preceding-sibling::varlistentry)">
+ <xsl:text>.RS 4&#10;</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&#10;</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);