@@ -0,0 +1,4 @@ | |||
target | |||
Cargo.lock | |||
*.bk | |||
*~ |
@@ -0,0 +1,4 @@ | |||
[submodule "3rdparty/libnotify"] | |||
path = 3rdparty/libnotify | |||
url = https://github.com/hasufell/rust-libnotify.git | |||
branch = git-deps |
@@ -0,0 +1 @@ | |||
Subproject commit 5b26d19aa92316a9555b0b22d2d7c3a2721a33dc |
@@ -0,0 +1,674 @@ | |||
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>. |
@@ -0,0 +1,53 @@ | |||
[package] | |||
name = "pnmixer-rs" | |||
version = "0.1.0" | |||
authors = ["Julian Ospald <hasufell@posteo.de>"] | |||
[build-dependencies] | |||
gcc = "^0.3.0" | |||
[dependencies] | |||
alsa = "^0.1.8" | |||
alsa-sys = "^0.1.1" | |||
error-chain = { git = "https://github.com/hasufell/error-chain.git", branch = "PR-from-error" } | |||
ffi = "^0.0.2" | |||
flexi_logger = "^0.5.1" | |||
gdk-pixbuf = { git = "https://github.com/gtk-rs/gdk-pixbuf.git" } | |||
gdk-pixbuf-sys = { git = "https://github.com/gtk-rs/sys" } | |||
gdk-sys = { git = "https://github.com/gtk-rs/sys" } | |||
gio = { git = "https://github.com/gtk-rs/gio.git" } | |||
glib = { git = "https://github.com/gtk-rs/glib.git" } | |||
glib-sys = { git = "https://github.com/gtk-rs/sys" } | |||
gobject-sys = { git = "https://github.com/gtk-rs/sys" } | |||
gtk-sys = { git = "https://github.com/gtk-rs/sys" } | |||
libc = "^0.2.23" | |||
log = "^0.3.8" | |||
serde = "^1.0.9" | |||
serde_derive = "^1.0.9" | |||
toml = "^0.4.2" | |||
which = "*" | |||
xdg = "*" | |||
libnotify = { path = "3rdparty/libnotify", optional = true } | |||
[dependencies.gtk] | |||
git = "https://github.com/gtk-rs/gtk.git" | |||
features = [ "v3_10", "v3_12", "v3_22" ] | |||
[dependencies.gdk] | |||
git = "https://github.com/gtk-rs/gdk.git" | |||
features = [ "v3_10", "v3_12", "v3_22" ] | |||
[profile.dev] | |||
opt-level = 0 # controls the `--opt-level` the compiler builds with | |||
debug = true # controls whether the compiler passes `-C debuginfo` | |||
# a value of `true` is equivalent to `2` | |||
rpath = false # controls whether the compiler passes `-C rpath` | |||
lto = false # controls `-C lto` for binaries and staticlibs | |||
debug-assertions = false # controls whether debug assertions are enabled | |||
codegen-units = 1 # controls whether the compiler passes `-C codegen-units` | |||
# `codegen-units` is ignored when `lto = true` | |||
panic = 'unwind' # panic strategy (`-C panic=...`), can also be 'abort' | |||
[features] | |||
notify = ["libnotify"] |
@@ -0,0 +1,58 @@ | |||
INSTALL = install | |||
INSTALL_DIR = $(INSTALL) -d | |||
INSTALL_BIN = $(INSTALL) -m 755 | |||
INSTALL_DATA = $(INSTALL) -m 644 | |||
PREFIX=/usr/local | |||
BINDIR=$(PREFIX)/bin | |||
SHAREDIR=$(PREFIX)/share | |||
DATADIR=$(SHAREDIR)/pnmixer | |||
PIXMAPSDIR=$(DATADIR)/pixmaps | |||
ICONSDIR=$(SHAREDIR)/icons/hicolor/128x128/apps | |||
DESKTOPDIR=$(SHAREDIR)/applications | |||
CARGO ?= cargo | |||
CARGO_ARGS ?= | |||
CARGO_BUILD_ARGS ?= --release | |||
CARGO_BUILD ?= $(CARGO) $(CARGO_ARGS) build $(CARGO_BUILD_ARGS) | |||
CARGO_INSTALL_ARGS ?= --root="$(DESTDIR)/$(PREFIX)" | |||
CARGO_INSTALL ?= $(CARGO) $(CARGO_ARGS) install $(CARGO_INSTALL_ARGS) | |||
pnmixer-rs: Cargo.toml | |||
PIXMAPSDIR=$(PIXMAPSDIR) $(CARGO_BUILD) | |||
install: install-data | |||
$(INSTALL_DIR) "$(DESTDIR)/$(BINDIR)" | |||
$(INSTALL_BIN) target/release/pnmixer "$(DESTDIR)/$(BINDIR)/pnmixer" | |||
install-data: install-pixmaps install-icons install-desktop | |||
install-pixmaps: | |||
$(INSTALL_DIR) "$(DESTDIR)/$(PIXMAPSDIR)" | |||
$(INSTALL_DATA) data/pixmaps/pnmixer-about.png "$(DESTDIR)/$(PIXMAPSDIR)/pnmixer-about.png" | |||
$(INSTALL_DATA) data/pixmaps/pnmixer-high.png "$(DESTDIR)/$(PIXMAPSDIR)/pnmixer-high.png" | |||
$(INSTALL_DATA) data/pixmaps/pnmixer-low.png "$(DESTDIR)/$(PIXMAPSDIR)/pnmixer-low.png" | |||
$(INSTALL_DATA) data/pixmaps/pnmixer-medium.png "$(DESTDIR)/$(PIXMAPSDIR)/pnmixer-medium.png" | |||
$(INSTALL_DATA) data/pixmaps/pnmixer-muted.png "$(DESTDIR)/$(PIXMAPSDIR)/pnmixer-muted.png" | |||
$(INSTALL_DATA) data/pixmaps/pnmixer-off.png "$(DESTDIR)/$(PIXMAPSDIR)/pnmixer-off.png" | |||
install-icons: | |||
$(INSTALL_DIR) "$(DESTDIR)/$(ICONSDIR)" | |||
$(INSTALL_DATA) data/icons/pnmixer.png "$(DESTDIR)/$(ICONSDIR)/pnmixer.png" | |||
install-desktop: | |||
$(INSTALL_DIR) "$(DESTDIR)/$(DESKTOPDIR)" | |||
$(INSTALL_DATA) data/desktop/pnmixer.desktop "$(DESTDIR)/$(DESKTOPDIR)/pnmixer.desktop" | |||
.PHONY: pnmixer-rs install install-data install-pixmaps install-icons install-desktop |
@@ -0,0 +1,53 @@ | |||
PNMixer-rs | |||
========== | |||
About | |||
----- | |||
Rewrite of [nicklan/pnmixer](https://github.com/nicklan/pnmixer) in | |||
[Rust](https://www.rust-lang.org). | |||
This is meant as a drop-in replacement, but may diverge in feature set | |||
in the future. | |||
Installation | |||
------------ | |||
The Rust ecosystem uses [Cargo](https://crates.io/), as such, you need | |||
both the rust compiler and the cargo crate | |||
(usually part of the compiler toolchain), then issue from within | |||
the cloned repository: | |||
```sh | |||
cargo install | |||
``` | |||
Features | |||
-------- | |||
Additonal features compared to [nicklan/pnmixer](https://github.com/nicklan/pnmixer): | |||
* decide whether to unmute or not on explicit volume change | |||
* updates tray icon on icon theme change | |||
Removed features: | |||
* normalize volume | |||
* slider orientation of volume popup | |||
* settings for displaying text volume on volume popup | |||
TODO: | |||
* hotkey support | |||
Behavior | |||
-------- | |||
Pretty much the same. Differences are: | |||
* volume slider is shown even when volume is muted | |||
Documentation | |||
------------- | |||
TODO |
@@ -0,0 +1,5 @@ | |||
extern crate gcc; | |||
fn main() { | |||
gcc::compile_library("libxpm.a", &["src/xpm.c"]); | |||
} |
@@ -0,0 +1,10 @@ | |||
[Desktop Entry] | |||
Name=PNMixer-rs | |||
_GenericName=System Tray Mixer | |||
_Comment=An audio mixer for the system tray | |||
Exec=pnmixer | |||
TryExec=pnmixer | |||
Icon=pnmixer | |||
Terminal=false | |||
Type=Application | |||
Categories=AudioVideo; |
@@ -0,0 +1,188 @@ | |||
<?xml version="1.0" encoding="UTF-8"?> | |||
<!-- Generated with glade 3.18.3 --> | |||
<interface> | |||
<requires lib="gtk+" version="3.12"/> | |||
<object class="GtkDialog" id="hotkey_dialog"> | |||
<property name="width_request">300</property> | |||
<property name="height_request">200</property> | |||
<property name="can_focus">False</property> | |||
<property name="border_width">5</property> | |||
<property name="title">Set xxx HotKey</property> | |||
<property name="modal">True</property> | |||
<property name="destroy_with_parent">True</property> | |||
<property name="icon_name">input-keyboard</property> | |||
<property name="type_hint">dialog</property> | |||
<child internal-child="vbox"> | |||
<object class="GtkBox" id="dialog-vbox1"> | |||
<property name="visible">True</property> | |||
<property name="can_focus">False</property> | |||
<property name="spacing">2</property> | |||
<child internal-child="action_area"> | |||
<object class="GtkButtonBox" id="dialog-action_area1"> | |||
<property name="visible">True</property> | |||
<property name="can_focus">False</property> | |||
<property name="layout_style">end</property> | |||
<child> | |||
<object class="GtkButton" id="button1"> | |||
<property name="use_action_appearance">False</property> | |||
<property name="visible">True</property> | |||
<property name="can_focus">True</property> | |||
<property name="receives_default">True</property> | |||
<child> | |||
<object class="GtkBox" id="box4"> | |||
<property name="visible">True</property> | |||
<property name="can_focus">False</property> | |||
<property name="halign">center</property> | |||
<child> | |||
<object class="GtkImage" id="image4"> | |||
<property name="visible">True</property> | |||
<property name="can_focus">False</property> | |||
<property name="icon_name">gtk-cancel</property> | |||
</object> | |||
<packing> | |||
<property name="expand">False</property> | |||
<property name="fill">True</property> | |||
<property name="position">0</property> | |||
</packing> | |||
</child> | |||
<child> | |||
<object class="GtkLabel" id="label2"> | |||
<property name="visible">True</property> | |||
<property name="can_focus">False</property> | |||
<property name="margin_start">5</property> | |||
<property name="label" translatable="yes">Cancel</property> | |||
</object> | |||
<packing> | |||
<property name="expand">False</property> | |||
<property name="fill">True</property> | |||
<property name="position">1</property> | |||
</packing> | |||
</child> | |||
</object> | |||
</child> | |||
</object> | |||
<packing> | |||
<property name="expand">False</property> | |||
<property name="fill">False</property> | |||
<property name="position">0</property> | |||
</packing> | |||
</child> | |||
</object> | |||
<packing> | |||
<property name="expand">False</property> | |||
<property name="fill">True</property> | |||
<property name="pack_type">end</property> | |||
<property name="position">3</property> | |||
</packing> | |||
</child> | |||
<child> | |||
<object class="GtkBox" id="hbox1"> | |||
<property name="visible">True</property> | |||
<property name="can_focus">False</property> | |||
<child> | |||
<object class="GtkImage" id="image1"> | |||
<property name="visible">True</property> | |||
<property name="can_focus">False</property> | |||
<property name="margin_start">10</property> | |||
<property name="margin_end">10</property> | |||
<property name="pixel_size">57</property> | |||
<property name="icon_name">input-keyboard</property> | |||
<property name="icon_size">3</property> | |||
</object> | |||
<packing> | |||
<property name="expand">False</property> | |||
<property name="fill">False</property> | |||
<property name="position">0</property> | |||
</packing> | |||
</child> | |||
<child> | |||
<object class="GtkBox" id="vbox1"> | |||
<property name="visible">True</property> | |||
<property name="can_focus">False</property> | |||
<property name="orientation">vertical</property> | |||
<child> | |||
<object class="GtkLabel" id="label1"> | |||
<property name="visible">True</property> | |||
<property name="can_focus">False</property> | |||
<property name="label" translatable="yes">Defining HotKey</property> | |||
<attributes> | |||
<attribute name="weight" value="bold"/> | |||
</attributes> | |||
</object> | |||
<packing> | |||
<property name="expand">True</property> | |||
<property name="fill">True</property> | |||
<property name="position">0</property> | |||
</packing> | |||
</child> | |||
<child> | |||
<object class="GtkLabel" id="hotkey_reset_label"> | |||
<property name="visible">True</property> | |||
<property name="can_focus">False</property> | |||
<property name="label" translatable="yes">(press <Ctrl>C to reset)</property> | |||
</object> | |||
<packing> | |||
<property name="expand">True</property> | |||
<property name="fill">True</property> | |||
<property name="position">1</property> | |||
</packing> | |||
</child> | |||
</object> | |||
<packing> | |||
<property name="expand">True</property> | |||
<property name="fill">True</property> | |||
<property name="position">1</property> | |||
</packing> | |||
</child> | |||
</object> | |||
<packing> | |||
<property name="expand">False</property> | |||
<property name="fill">True</property> | |||
<property name="position">0</property> | |||
</packing> | |||
</child> | |||
<child> | |||
<object class="GtkHSeparator" id="hseparator1"> | |||
<property name="visible">True</property> | |||
<property name="can_focus">False</property> | |||
</object> | |||
<packing> | |||
<property name="expand">False</property> | |||
<property name="fill">True</property> | |||
<property name="position">1</property> | |||
</packing> | |||
</child> | |||
<child> | |||
<object class="GtkLabel" id="instruction_label"> | |||
<property name="visible">True</property> | |||
<property name="can_focus">False</property> | |||
<property name="label">Press new HotKey for xxx</property> | |||
<property name="use_markup">True</property> | |||
</object> | |||
<packing> | |||
<property name="expand">True</property> | |||
<property name="fill">True</property> | |||
<property name="position">2</property> | |||
</packing> | |||
</child> | |||
<child> | |||
<object class="GtkLabel" id="key_pressed_label"> | |||
<property name="visible">True</property> | |||
<property name="can_focus">False</property> | |||
<attributes> | |||
<attribute name="weight" value="bold"/> | |||
</attributes> | |||
</object> | |||
<packing> | |||
<property name="expand">True</property> | |||
<property name="fill">True</property> | |||
<property name="position">4</property> | |||
</packing> | |||
</child> | |||
</object> | |||
</child> | |||
<action-widgets> | |||
<action-widget response="0">button1</action-widget> | |||
</action-widgets> | |||
</object> | |||
</interface> |
@@ -0,0 +1,265 @@ | |||
<?xml version="1.0" encoding="UTF-8"?> | |||
<!-- Generated with glade 3.18.3 --> | |||
<interface> | |||
<requires lib="gtk+" version="3.12"/> | |||
<object class="GtkWindow" id="menu_window"> | |||
<property name="can_focus">False</property> | |||
<property name="type">popup</property> | |||
<property name="resizable">False</property> | |||
<property name="window_position">mouse</property> | |||
<property name="type_hint">utility</property> | |||
<property name="skip_taskbar_hint">True</property> | |||
<property name="skip_pager_hint">True</property> | |||
<property name="decorated">False</property> | |||
<child> | |||
<object class="GtkMenuBar" id="menubar"> | |||
<property name="can_focus">False</property> | |||
<child> | |||
<object class="GtkMenuItem" id="menuitem"> | |||
<property name="use_action_appearance">False</property> | |||
<property name="visible">True</property> | |||
<property name="can_focus">False</property> | |||
<property name="label">_Popup</property> | |||
<property name="use_underline">True</property> | |||
<child type="submenu"> | |||
<object class="GtkMenu" id="menu"> | |||
<property name="visible">True</property> | |||
<property name="can_focus">False</property> | |||
<child> | |||
<object class="GtkMenuItem" id="mute_item"> | |||
<property name="use_action_appearance">False</property> | |||
<property name="visible">True</property> | |||
<property name="can_focus">False</property> | |||
<property name="tooltip_text" translatable="yes">Mute/Unmute Volume</property> | |||
<child> | |||
<object class="GtkBox" id="box0"> | |||
<property name="visible">True</property> | |||
<property name="can_focus">False</property> | |||
<child> | |||
<object class="GtkCheckButton" id="mute_check"> | |||
<property name="use_action_appearance">False</property> | |||
<property name="visible">True</property> | |||
<property name="can_focus">True</property> | |||
<property name="receives_default">False</property> | |||
<property name="halign">center</property> | |||
<property name="draw_indicator">True</property> | |||
</object> | |||
<packing> | |||
<property name="position">0</property> | |||
</packing> | |||
</child> | |||
<child> | |||
<object class="GtkAccelLabel" id="mute_accellabel"> | |||
<property name="visible">True</property> | |||
<property name="can_focus">False</property> | |||
<property name="margin_start">4</property> | |||
<property name="margin_end">8</property> | |||
<property name="label" translatable="yes">Mute</property> | |||
</object> | |||
<packing> | |||
<property name="position">1</property> | |||
</packing> | |||
</child> | |||
</object> | |||
</child> | |||
</object> | |||
</child> | |||
<child> | |||
<object class="GtkMenuItem" id="mixer_item"> | |||
<property name="use_action_appearance">False</property> | |||
<property name="visible">True</property> | |||
<property name="can_focus">False</property> | |||
<property name="tooltip_text" translatable="yes">Open Volume Control</property> | |||
<child> | |||
<object class="GtkBox" id="box1"> | |||
<property name="visible">True</property> | |||
<property name="can_focus">False</property> | |||
<child> | |||
<object class="GtkImage" id="mixer_image"> | |||
<property name="visible">True</property> | |||
<property name="can_focus">False</property> | |||
<property name="icon_name">system-run</property> | |||
</object> | |||
<packing> | |||
<property name="position">0</property> | |||
</packing> | |||
</child> | |||
<child> | |||
<object class="GtkAccelLabel" id="mixer_accellabel"> | |||
<property name="visible">True</property> | |||
<property name="can_focus">False</property> | |||
<property name="margin_start">4</property> | |||
<property name="margin_end">8</property> | |||
<property name="label" translatable="yes">Volume Control</property> | |||
</object> | |||
<packing> | |||
<property name="position">1</property> | |||
</packing> | |||
</child> | |||
</object> | |||
</child> | |||
</object> | |||
</child> | |||
<child> | |||
<object class="GtkMenuItem" id="prefs_item"> | |||
<property name="use_action_appearance">False</property> | |||
<property name="visible">True</property> | |||
<property name="can_focus">False</property> | |||
<property name="tooltip_text" translatable="yes">Preferences</property> | |||
<child> | |||
<object class="GtkBox" id="box2"> | |||
<property name="visible">True</property> | |||
<property name="can_focus">False</property> | |||
<child> | |||
<object class="GtkImage" id="prefs_image"> | |||
<property name="visible">True</property> | |||
<property name="can_focus">False</property> | |||
<property name="icon_name">preferences-desktop</property> | |||
<property name="icon_size">1</property> | |||
</object> | |||
<packing> | |||
<property name="position">0</property> | |||
</packing> | |||
</child> | |||
<child> | |||
<object class="GtkAccelLabel" id="prefs_accellabel"> | |||
<property name="visible">True</property> | |||
<property name="can_focus">False</property> | |||
<property name="margin_start">4</property> | |||
<property name="margin_end">8</property> | |||
<property name="label" translatable="yes">Preferences</property> | |||
</object> | |||
<packing> | |||
<property name="position">1</property> | |||
</packing> | |||
</child> | |||
</object> | |||
</child> | |||
</object> | |||
</child> | |||
<child> | |||
<object class="GtkMenuItem" id="reload_item"> | |||
<property name="use_action_appearance">False</property> | |||
<property name="visible">True</property> | |||
<property name="can_focus">False</property> | |||
<property name="tooltip_text" translatable="yes">Reload Sound</property> | |||
<child> | |||
<object class="GtkBox" id="box3"> | |||
<property name="visible">True</property> | |||
<property name="can_focus">False</property> | |||
<child> | |||
<object class="GtkImage" id="reload_image"> | |||
<property name="visible">True</property> | |||
<property name="can_focus">False</property> | |||
<property name="icon_name">gtk-refresh</property> | |||
<property name="icon_size">1</property> | |||
</object> | |||
<packing> | |||
<property name="position">0</property> | |||
</packing> | |||
</child> | |||
<child> | |||
<object class="GtkAccelLabel" id="reload_accellabel"> | |||
<property name="visible">True</property> | |||
<property name="can_focus">False</property> | |||
<property name="margin_start">4</property> | |||
<property name="margin_end">8</property> | |||
<property name="label" translatable="yes">Reload Sound</property> | |||
</object> | |||
<packing> | |||
<property name="position">1</property> | |||
</packing> | |||
</child> | |||
</object> | |||
</child> | |||
</object> | |||
</child> | |||
<child> | |||
<object class="GtkMenuItem" id="about_item"> | |||
<property name="use_action_appearance">False</property> | |||
<property name="visible">True</property> | |||
<property name="can_focus">False</property> | |||
<property name="tooltip_text" translatable="yes">About</property> | |||
<child> | |||
<object class="GtkBox" id="box4"> | |||
<property name="visible">True</property> | |||
<property name="can_focus">False</property> | |||
<child> | |||
<object class="GtkImage" id="about_image"> | |||
<property name="visible">True</property> | |||
<property name="can_focus">False</property> | |||
<property name="icon_name">help-about</property> | |||
<property name="icon_size">1</property> | |||
</object> | |||
<packing> | |||
<property name="position">0</property> | |||
</packing> | |||
</child> | |||
<child> | |||
<object class="GtkAccelLabel" id="about_accellabel"> | |||
<property name="visible">True</property> | |||
<property name="can_focus">False</property> | |||
<property name="margin_start">4</property> | |||
<property name="margin_end">8</property> | |||
<property name="label" translatable="yes">About</property> | |||
</object> | |||
<packing> | |||
<property name="position">1</property> | |||
</packing> | |||
</child> | |||
</object> | |||
</child> | |||
</object> | |||
</child> | |||
<child> | |||
<object class="GtkSeparatorMenuItem" id="separatormenuitem2"> | |||
<property name="use_action_appearance">False</property> | |||
<property name="visible">True</property> | |||
<property name="can_focus">False</property> | |||
</object> | |||
</child> | |||
<child> | |||
<object class="GtkMenuItem" id="quit_item"> | |||
<property name="use_action_appearance">False</property> | |||
<property name="visible">True</property> | |||
<property name="can_focus">False</property> | |||
<property name="tooltip_text" translatable="yes">Quit</property> | |||
<child> | |||
<object class="GtkBox" id="box5"> | |||
<property name="visible">True</property> | |||
<property name="can_focus">False</property> | |||
<child> | |||
<object class="GtkImage" id="quit_image"> | |||
<property name="visible">True</property> | |||
<property name="can_focus">False</property> | |||
<property name="icon_name">gtk-quit</property> | |||
<property name="icon_size">1</property> | |||
</object> | |||
<packing> | |||
<property name="position">0</property> | |||
</packing> | |||
</child> | |||
<child> | |||
<object class="GtkAccelLabel" id="quit_accellabel"> | |||
<property name="visible">True</property> | |||
<property name="can_focus">False</property> | |||
<property name="margin_start">4</property> | |||
<property name="margin_end">8</property> | |||
<property name="label" translatable="yes">Quit</property> | |||
</object> | |||
<packing> | |||
<property name="position">1</property> | |||
</packing> | |||
</child> | |||
</object> | |||
</child> | |||
</object> | |||
</child> | |||
</object> | |||
</child> | |||
</object> | |||
</child> | |||
</object> | |||
</child> | |||
</object> | |||
</interface> |
@@ -0,0 +1,88 @@ | |||
<?xml version="1.0" encoding="UTF-8"?> | |||
<!-- Generated with glade 3.18.3 --> | |||
<interface> | |||
<requires lib="gtk+" version="3.12"/> | |||
<object class="GtkAdjustment" id="vol_scale_adj"> | |||
<property name="upper">100</property> | |||
<property name="step_increment">1</property> | |||
<property name="page_increment">10</property> | |||
</object> | |||
<object class="GtkWindow" id="popup_window"> | |||
<property name="type">popup</property> | |||
<property name="can_focus">False</property> | |||
<property name="resizable">False</property> | |||
<property name="window_position">mouse</property> | |||
<property name="type_hint">utility</property> | |||
<property name="skip_taskbar_hint">True</property> | |||
<property name="skip_pager_hint">True</property> | |||
<property name="decorated">False</property> | |||
<child> | |||
<object class="GtkBox" id="vbox1"> | |||
<property name="visible">True</property> | |||
<property name="can_focus">False</property> | |||
<property name="orientation">vertical</property> | |||
<property name="margin">8</property> | |||
<property name="spacing">12</property> | |||
<child> | |||
<object class="GtkScale" id="vol_scale"> | |||
<property name="visible">True</property> | |||
<property name="can_focus">True</property> | |||
<property name="orientation">vertical</property> | |||
<property name="adjustment">vol_scale_adj</property> | |||
<property name="inverted">True</property> | |||
<property name="round_digits">0</property> | |||
<property name="digits">0</property> | |||
<property name="height_request">200</property> | |||
</object> | |||
<packing> | |||
<property name="expand">True</property> | |||
<property name="fill">True</property> | |||
<property name="position">0</property> | |||
</packing> | |||
</child> | |||
<child> | |||
<object class="GtkBox" id="vbox2"> | |||
<property name="visible">True</property> | |||
<property name="can_focus">False</property> | |||
<property name="orientation">vertical</property> | |||
<property name="spacing">4</property> | |||
<child> | |||
<object class="GtkCheckButton" id="mute_check"> | |||
<property name="label" translatable="yes">Mute</property> | |||
<property name="use_action_appearance">False</property> | |||
<property name="visible">True</property> | |||
<property name="can_focus">True</property> | |||
<property name="receives_default">False</property> | |||
<property name="draw_indicator">True</property> | |||
</object> | |||
<packing> | |||
<property name="expand">False</property> | |||
<property name="fill">False</property> | |||
<property name="position">0</property> | |||
</packing> | |||
</child> | |||
<child> | |||
<object class="GtkButton" id="mixer_button"> | |||
<property name="label" translatable="yes">Mixer</property> | |||
<property name="use_action_appearance">False</property> | |||
<property name="visible">True</property> | |||
<property name="can_focus">True</property> | |||
<property name="receives_default">True</property> | |||
</object> | |||
<packing> | |||
<property name="expand">False</property> | |||
<property name="fill">False</property> | |||
<property name="position">1</property> | |||
</packing> | |||
</child> | |||
</object> | |||
<packing> | |||
<property name="expand">False</property> | |||
<property name="fill">False</property> | |||
<property name="position">1</property> | |||
</packing> | |||
</child> | |||
</object> | |||
</child> | |||
</object> | |||
</interface> |
@@ -0,0 +1,2 @@ | |||
max_width = 80 | |||
ideal_width = 74 |
@@ -0,0 +1,273 @@ | |||
use alsa::card::Card; | |||
use alsa::mixer::SelemChannelId::*; | |||
use alsa::mixer::{Mixer, Selem, SelemId}; | |||
use alsa::poll::PollDescriptors; | |||
use alsa_sys; | |||
use errors::*; | |||
use glib_sys; | |||
use libc::c_uint; | |||
use libc::pollfd; | |||
use libc::size_t; | |||
use std::cell::Cell; | |||
use std::mem; | |||
use std::ptr; | |||
use std::rc::Rc; | |||
use std::u8; | |||
use support_alsa::*; | |||
#[derive(Clone, Copy, Debug)] | |||
pub enum AlsaEvent { | |||
AlsaCardError, | |||
AlsaCardDiconnected, | |||
AlsaCardValuesChanged, | |||
} | |||
pub struct AlsaCard { | |||
_cannot_construct: (), | |||
pub card: Card, | |||
pub mixer: Mixer, | |||
pub selem_id: SelemId, | |||
pub watch_ids: Cell<Vec<u32>>, | |||
pub cb: Rc<Fn(AlsaEvent)>, | |||
} | |||
impl AlsaCard { | |||
pub fn new(card_name: Option<String>, | |||
elem_name: Option<String>, | |||
cb: Rc<Fn(AlsaEvent)>) | |||
-> Result<Box<AlsaCard>> { | |||
let card = { | |||
match card_name { | |||
Some(name) => { | |||
if name == "(default)" { | |||
let default = get_default_alsa_card(); | |||
if alsa_card_has_playable_selem(&default) { | |||
default | |||
} else { | |||
warn!("Default alsa card not playabla, trying others"); | |||
get_first_playable_alsa_card()? | |||
} | |||
} else { | |||
let mycard = get_alsa_card_by_name(name.clone()); | |||
match mycard { | |||
Ok(card) => card, | |||
Err(_) => { | |||
warn!("Card {} not playable, trying others", | |||
name); | |||
get_first_playable_alsa_card()? | |||
} | |||
} | |||
} | |||
} | |||
None => get_first_playable_alsa_card()?, | |||
} | |||
}; | |||
let mixer = get_mixer(&card)?; | |||
let selem_id = { | |||
let requested_selem = | |||
get_playable_selem_by_name(&mixer, | |||
elem_name.unwrap_or(String::from("Master"))); | |||
match requested_selem { | |||
Ok(s) => s.get_id(), | |||
Err(_) => { | |||
warn!("No playable Selem found, trying others"); | |||
get_first_playable_selem(&mixer)?.get_id() | |||
} | |||
} | |||
}; | |||
let vec_pollfd = PollDescriptors::get(&mixer)?; | |||
let acard = Box::new(AlsaCard { | |||
_cannot_construct: (), | |||
card: card, | |||
mixer: mixer, | |||
selem_id: selem_id, | |||
watch_ids: Cell::new(vec![]), | |||
cb: cb, | |||
}); | |||
let watch_ids = AlsaCard::watch_poll_descriptors(vec_pollfd, | |||
acard.as_ref()); | |||
acard.watch_ids.set(watch_ids); | |||
return Ok(acard); | |||
} | |||
pub fn card_name(&self) -> Result<String> { | |||
return self.card.get_name().from_err(); | |||
} | |||
pub fn chan_name(&self) -> Result<String> { | |||
let n = self.selem_id | |||
.get_name() | |||
.map(|y| String::from(y))?; | |||
return Ok(n); | |||
} | |||
pub fn selem(&self) -> Selem { | |||
return self.mixer.find_selem(&self.selem_id).unwrap(); | |||
} | |||
pub fn get_vol(&self) -> Result<i64> { | |||
let selem = self.selem(); | |||
let volume = selem.get_playback_volume(FrontRight); | |||
return volume.from_err(); | |||
} | |||
pub fn set_vol(&self, new_vol: i64) -> Result<()> { | |||
let selem = self.selem(); | |||
return selem.set_playback_volume_all(new_vol).from_err(); | |||
} | |||
pub fn get_volume_range(&self) -> (i64, i64) { | |||
let selem = self.selem(); | |||
return selem.get_playback_volume_range(); | |||
} | |||
pub fn has_mute(&self) -> bool { | |||
let selem = self.selem(); | |||
return selem.has_playback_switch(); | |||
} | |||
pub fn get_mute(&self) -> Result<bool> { | |||
let selem = self.selem(); | |||
let val = selem.get_playback_switch(FrontRight)?; | |||
return Ok(val == 0); | |||
} | |||
pub fn set_mute(&self, mute: bool) -> Result<()> { | |||
let selem = self.selem(); | |||
/* true -> mute, false -> unmute */ | |||
let _ = selem.set_playback_switch_all(!mute as i32)?; | |||
return Ok(()); | |||
} | |||
fn watch_poll_descriptors(polls: Vec<pollfd>, | |||
acard: &AlsaCard) | |||
-> Vec<c_uint> { | |||
let mut watch_ids: Vec<c_uint> = vec![]; | |||
let acard_ptr = | |||
unsafe { mem::transmute::<&AlsaCard, glib_sys::gpointer>(acard) }; | |||
for poll in polls { | |||
let gioc: *mut glib_sys::GIOChannel = | |||
unsafe { glib_sys::g_io_channel_unix_new(poll.fd) }; | |||
let id = unsafe { | |||
glib_sys::g_io_add_watch( | |||
gioc, | |||
glib_sys::GIOCondition::from_bits( | |||
glib_sys::G_IO_IN.bits() | glib_sys::G_IO_ERR.bits(), | |||
).unwrap(), | |||
Some(watch_cb), | |||
acard_ptr, | |||
) | |||
}; | |||
watch_ids.push(id); | |||
unsafe { glib_sys::g_io_channel_unref(gioc) } | |||
} | |||
return watch_ids; | |||
} | |||
fn unwatch_poll_descriptors(watch_ids: &Vec<u32>) { | |||
for watch_id in watch_ids { | |||
unsafe { | |||
glib_sys::g_source_remove(*watch_id); | |||
} | |||
} | |||
} | |||
} | |||
impl Drop for AlsaCard { | |||
// call Box::new(x), transmute the Box into a raw pointer, and then | |||
// std::mem::forget | |||
// | |||
// if you unregister the callback, you should keep a raw pointer to the | |||
// box | |||
// | |||
// For instance, `register` could return a raw pointer to the | |||
// Box + a std::marker::PhantomData with the appropriate | |||
// lifetime (if applicable) | |||
// | |||
// The struct could implement Drop, which unregisters the | |||
// callback and frees the Box, by simply transmuting the | |||
// raw pointer to a Box<T> | |||
fn drop(&mut self) { | |||
debug!("Destructing watch_ids: {:?}", self.watch_ids.get_mut()); | |||
AlsaCard::unwatch_poll_descriptors(&self.watch_ids.get_mut()); | |||
} | |||
} | |||
extern "C" fn watch_cb(chan: *mut glib_sys::GIOChannel, | |||
cond: glib_sys::GIOCondition, | |||
data: glib_sys::gpointer) | |||
-> glib_sys::gboolean { | |||
let acard = | |||
unsafe { mem::transmute::<glib_sys::gpointer, &AlsaCard>(data) }; | |||
let cb = &acard.cb; | |||
unsafe { | |||
let mixer_ptr = | |||
mem::transmute::<&Mixer, &*mut alsa_sys::snd_mixer_t>(&acard.mixer); | |||
alsa_sys::snd_mixer_handle_events(*mixer_ptr); | |||
}; | |||
if cond == glib_sys::G_IO_ERR { | |||
return false as glib_sys::gboolean; | |||
} | |||
let mut sread: size_t = 1; | |||
let mut buf: Vec<u8> = vec![0; 256]; | |||
while sread > 0 { | |||
let stat: glib_sys::GIOStatus = | |||
unsafe { | |||
glib_sys::g_io_channel_read_chars(chan, | |||
buf.as_mut_ptr() as *mut u8, | |||
256, | |||
&mut sread as *mut size_t, | |||
ptr::null_mut()) | |||
}; | |||
match stat { | |||
glib_sys::G_IO_STATUS_AGAIN => { | |||
debug!("G_IO_STATUS_AGAIN"); | |||
continue; | |||
} | |||
glib_sys::G_IO_STATUS_NORMAL => { | |||
error!("Alsa failed to clear the channel"); | |||
cb(AlsaEvent::AlsaCardError); | |||
} | |||
glib_sys::G_IO_STATUS_ERROR => (), | |||
glib_sys::G_IO_STATUS_EOF => { | |||
error!("GIO error has occurred"); | |||
cb(AlsaEvent::AlsaCardError); | |||
} | |||
_ => warn!("Unknown status from g_io_channel_read_chars()"), | |||
} | |||
return true as glib_sys::gboolean; | |||
} | |||
cb(AlsaEvent::AlsaCardValuesChanged); | |||
return true as glib_sys::gboolean; | |||
} |
@@ -0,0 +1,92 @@ | |||
use audio::{Audio, AudioUser}; | |||
use errors::*; | |||
use gtk; | |||
use prefs::*; | |||
use std::cell::RefCell; | |||
use support_audio::*; | |||
use ui_entry::Gui; | |||
#[cfg(feature = "notify")] | |||
use notif::*; | |||
// TODO: destructors | |||
pub struct AppS { | |||
_cant_construct: (), | |||
pub gui: Gui, | |||
pub audio: Audio, | |||
pub prefs: RefCell<Prefs>, | |||
#[cfg(feature = "notify")] | |||
pub notif: Notif, | |||
} | |||
impl AppS { | |||
pub fn new() -> AppS { | |||
let builder_popup_window = | |||
gtk::Builder::new_from_string(include_str!(concat!(env!("CARGO_MANIFEST_DIR"), | |||
"/data/ui/popup-window.glade"))); | |||
let builder_popup_menu = | |||
gtk::Builder::new_from_string(include_str!(concat!(env!("CARGO_MANIFEST_DIR"), | |||
"/data/ui/popup-menu.glade"))); | |||
let prefs = RefCell::new(Prefs::new().unwrap()); | |||
let gui = | |||
Gui::new(builder_popup_window, builder_popup_menu, &prefs.borrow()); | |||
let card_name = prefs.borrow() | |||
.device_prefs | |||
.card | |||
.clone(); | |||
let chan_name = prefs.borrow() | |||
.device_prefs | |||
.channel | |||
.clone(); | |||
// TODO: better error handling | |||
#[cfg(feature = "notify")] | |||
let notif = Notif::new(&prefs.borrow()).unwrap(); | |||
return AppS { | |||
_cant_construct: (), | |||
gui, | |||
audio: Audio::new(Some(card_name), Some(chan_name)).unwrap(), | |||
prefs, | |||
#[cfg(feature = "notify")] | |||
notif, | |||
}; | |||
} | |||
/* some functions that need to be easily accessible */ | |||
pub fn update_tray_icon(&self) -> Result<()> { | |||
debug!("Update tray icon!"); | |||
return self.gui.tray_icon.update_all(&self.prefs.borrow(), | |||
&self.audio, | |||
None); | |||
} | |||
pub fn update_popup_window(&self) -> Result<()> { | |||
debug!("Update PopupWindow!"); | |||
return self.gui.popup_window.update(&self.audio); | |||
} | |||
#[cfg(feature = "notify")] | |||
pub fn update_notify(&self) -> Result<()> { | |||
return self.notif.reload(&self.prefs.borrow()); | |||
} | |||
#[cfg(not(feature = "notify"))] | |||
pub fn update_notify(&self) -> Result<()> { | |||
return Ok(()); | |||
} | |||
pub fn update_audio(&self, user: AudioUser) -> Result<()> { | |||
return audio_reload(&self.audio, &self.prefs.borrow(), user); | |||
} | |||
pub fn update_config(&self) -> Result<()> { | |||
let prefs = self.prefs.borrow_mut(); | |||
return prefs.store_config(); | |||
} | |||
} |
@@ -0,0 +1,358 @@ | |||
use alsa_card::*; | |||
use errors::*; | |||
use glib; | |||
use std::cell::Cell; | |||
use std::cell::Ref; | |||
use std::cell::RefCell; | |||
use std::f64; | |||
use std::rc::Rc; | |||
use support_audio::*; | |||
#[derive(Clone, Copy, Debug)] | |||
pub enum VolLevel { | |||
Muted, | |||
Low, | |||
Medium, | |||
High, | |||
Off, | |||
} | |||
#[derive(Clone, Copy, Debug)] | |||
pub enum AudioUser { | |||
Unknown, | |||
Popup, | |||
TrayIcon, | |||
Hotkeys, | |||
PrefsWindow, | |||
} | |||
#[derive(Clone, Copy, Debug)] | |||
pub enum AudioSignal { | |||
NoCard, | |||
CardInitialized, | |||
CardCleanedUp, | |||
CardDisconnected, | |||
CardError, | |||
ValuesChanged, | |||
} | |||
#[derive(Clone)] | |||
pub struct Handlers { | |||
inner: Rc<RefCell<Vec<Box<Fn(AudioSignal, AudioUser)>>>>, | |||
} | |||
impl Handlers { | |||
fn new() -> Handlers { | |||
return Handlers { inner: Rc::new(RefCell::new(vec![])) }; | |||
} | |||
fn borrow(&self) -> Ref<Vec<Box<Fn(AudioSignal, AudioUser)>>> { | |||
return self.inner.borrow(); | |||
} | |||
fn add_handler(&self, cb: Box<Fn(AudioSignal, AudioUser)>) { | |||
self.inner.borrow_mut().push(cb); | |||
} | |||
} | |||
pub struct Audio { | |||
_cannot_construct: (), | |||
pub acard: RefCell<Box<AlsaCard>>, | |||
pub last_action_timestamp: Rc<RefCell<i64>>, | |||
pub handlers: Handlers, | |||
pub scroll_step: Cell<u32>, | |||
} | |||
impl Audio { | |||
pub fn new(card_name: Option<String>, | |||
elem_name: Option<String>) | |||
-> Result<Audio> { | |||
let handlers = Handlers::new(); | |||
let last_action_timestamp = Rc::new(RefCell::new(0)); | |||
let cb = { | |||
let myhandler = handlers.clone(); | |||
let ts = last_action_timestamp.clone(); | |||
Rc::new(move |event| { | |||
on_alsa_event(&mut *ts.borrow_mut(), | |||
&myhandler.borrow(), | |||
event) | |||
}) | |||
}; | |||
let acard = AlsaCard::new(card_name, elem_name, cb); | |||
/* additionally dispatch signals */ | |||
if acard.is_err() { | |||
invoke_handlers(&handlers.borrow(), | |||
AudioSignal::NoCard, | |||
AudioUser::Unknown); | |||
} else { | |||
invoke_handlers(&handlers.borrow(), | |||
AudioSignal::CardInitialized, | |||
AudioUser::Unknown); | |||
} | |||
let audio = Audio { | |||
_cannot_construct: (), | |||
acard: RefCell::new(acard?), | |||
last_action_timestamp: last_action_timestamp.clone(), | |||
handlers: handlers.clone(), | |||
scroll_step: Cell::new(5), | |||
}; | |||
return Ok(audio); | |||
} | |||
pub fn switch_acard(&self, | |||
card_name: Option<String>, | |||
elem_name: Option<String>, | |||
user: AudioUser) | |||
-> Result<()> { | |||
debug!("Switching cards"); | |||
debug!("Old card name: {}", | |||
self.acard | |||
.borrow() | |||
.card_name() | |||
.unwrap()); | |||
debug!("Old chan name: {}", | |||
self.acard | |||
.borrow() | |||
.chan_name() | |||
.unwrap()); | |||
let cb = self.acard | |||
.borrow() | |||
.cb | |||
.clone(); | |||
{ | |||
let mut ac = self.acard.borrow_mut(); | |||
*ac = AlsaCard::new(card_name, elem_name, cb)?; | |||
} | |||
// invoke_handlers(&self.handlers.borrow(), | |||
// AudioSignal::CardCleanedUp, | |||
// user); | |||
invoke_handlers(&self.handlers.borrow(), | |||
AudioSignal::CardInitialized, | |||
user); | |||
return Ok(()); | |||
} | |||
pub fn vol(&self) -> Result<f64> { | |||
let alsa_vol = self.acard | |||
.borrow() | |||
.get_vol()?; | |||
return vol_to_percent(alsa_vol, self.acard.borrow().get_volume_range()); | |||
} | |||
pub fn vol_level(&self) -> VolLevel { | |||
let muted = self.get_mute().unwrap_or(false); | |||
if muted { | |||
return VolLevel::Muted; | |||
} | |||
let cur_vol = try_r!(self.vol(), VolLevel::Muted); | |||
match cur_vol { | |||
0. => return VolLevel::Off, | |||
0.0...33.0 => return VolLevel::Low, | |||
0.0...66.0 => return VolLevel::Medium, | |||
0.0...100.0 => return VolLevel::High, | |||
_ => return VolLevel::Off, | |||
} | |||
} | |||
pub fn set_vol(&self, | |||
new_vol: f64, | |||
user: AudioUser, | |||
dir: VolDir, | |||
auto_unmute: bool) | |||
-> Result<()> { | |||
{ | |||
let mut rc = self.last_action_timestamp.borrow_mut(); | |||
*rc = glib::get_monotonic_time(); | |||
} | |||
let alsa_vol = percent_to_vol(new_vol, | |||
self.acard.borrow().get_volume_range(), | |||
dir)?; | |||
/* only invoke handlers etc. if volume did actually change */ | |||
{ | |||
let old_alsa_vol = | |||
percent_to_vol(self.vol()?, | |||
self.acard.borrow().get_volume_range(), | |||
dir)?; | |||
if old_alsa_vol == alsa_vol { | |||
return Ok(()); | |||
} | |||
} | |||
/* auto-unmute */ | |||
if auto_unmute && self.has_mute() && self.get_mute()? { | |||
self.set_mute(false, user)?; | |||
} | |||
debug!("Setting vol on card {:?} and chan {:?} to {:?} by user {:?}", | |||
self.acard | |||
.borrow() | |||
.card_name() | |||
.unwrap(), | |||
self.acard | |||
.borrow() | |||
.chan_name() | |||
.unwrap(), | |||
new_vol, | |||
user); | |||
self.acard | |||
.borrow() | |||
.set_vol(alsa_vol)?; | |||
invoke_handlers(&self.handlers.borrow(), | |||
AudioSignal::ValuesChanged, | |||
user); | |||
return Ok(()); | |||
} | |||
pub fn increase_vol(&self, | |||
user: AudioUser, | |||
auto_unmute: bool) | |||
-> Result<()> { | |||
let old_vol = self.vol()?; | |||
let new_vol = old_vol + (self.scroll_step.get() as f64); | |||
return self.set_vol(new_vol, user, VolDir::Up, auto_unmute); | |||
} | |||
pub fn decrease_vol(&self, | |||
user: AudioUser, | |||
auto_unmute: bool) | |||
-> Result<()> { | |||
let old_vol = self.vol()?; | |||
let new_vol = old_vol - (self.scroll_step.get() as f64); | |||
return self.set_vol(new_vol, user, VolDir::Down, auto_unmute); | |||
} | |||
pub fn has_mute(&self) -> bool { | |||
return self.acard.borrow().has_mute(); | |||
} | |||
pub fn get_mute(&self) -> Result<bool> { | |||
return self.acard.borrow().get_mute(); | |||
} | |||
pub fn set_mute(&self, mute: bool, user: AudioUser) -> Result<()> { | |||
let mut rc = self.last_action_timestamp.borrow_mut(); | |||
*rc = glib::get_monotonic_time(); | |||
debug!("Setting mute to {} on card {:?} and chan {:?} by user {:?}", | |||
mute, | |||
self.acard | |||
.borrow() | |||
.card_name() | |||
.unwrap(), | |||
self.acard | |||
.borrow() | |||
.chan_name() | |||
.unwrap(), | |||
user); | |||
self.acard | |||
.borrow() | |||
.set_mute(mute)?; | |||
invoke_handlers(&self.handlers.borrow(), | |||
AudioSignal::ValuesChanged, | |||
user); | |||
return Ok(()); | |||
} | |||
pub fn toggle_mute(&self, user: AudioUser) -> Result<()> { | |||
let muted = self.get_mute()?; | |||
return self.set_mute(!muted, user); | |||
} | |||
pub fn connect_handler(&self, cb: Box<Fn(AudioSignal, AudioUser)>) { | |||
self.handlers.add_handler(cb); | |||
} | |||
} | |||
fn invoke_handlers(handlers: &Vec<Box<Fn(AudioSignal, AudioUser)>>, | |||
signal: AudioSignal, | |||
user: AudioUser) { | |||
debug!("Invoking handlers for signal {:?} by user {:?}", | |||
signal, | |||
user); | |||
if handlers.is_empty() { | |||
debug!("No handler found"); | |||
} else { | |||
debug!("Executing handlers") | |||
} | |||
for handler in handlers { | |||
let unboxed = handler.as_ref(); | |||
unboxed(signal, user); | |||
} | |||
} | |||
fn on_alsa_event(last_action_timestamp: &mut i64, | |||
handlers: &Vec<Box<Fn(AudioSignal, AudioUser)>>, | |||
alsa_event: AlsaEvent) { | |||
let last: i64 = *last_action_timestamp; | |||
if last != 0 { | |||
let now: i64 = glib::get_monotonic_time(); | |||
let delay: i64 = now - last; | |||
if delay < 1000000 { | |||
return; | |||
} | |||
debug!("Discarding last time stamp, too old"); | |||
*last_action_timestamp = 0; | |||
} | |||
/* external change */ | |||
match alsa_event { | |||
AlsaEvent::AlsaCardError => { | |||
invoke_handlers(handlers, | |||
self::AudioSignal::CardError, | |||
self::AudioUser::Unknown); | |||
} | |||
AlsaEvent::AlsaCardDiconnected => { | |||
invoke_handlers(handlers, | |||
self::AudioSignal::CardDisconnected, | |||
self::AudioUser::Unknown); | |||
} | |||
AlsaEvent::AlsaCardValuesChanged => { | |||
invoke_handlers(handlers, | |||
self::AudioSignal::ValuesChanged, | |||
self::AudioUser::Unknown); | |||
} | |||
e => warn!("Unhandled alsa event: {:?}", e), | |||
} | |||
} |
@@ -0,0 +1,113 @@ | |||
use alsa; | |||
use glib; | |||
use std::convert::From; | |||
use std; | |||
use toml; | |||
error_chain! { | |||
foreign_links { | |||
Alsa(alsa::Error); | |||
IO(std::io::Error); | |||
Toml(toml::de::Error); | |||
} | |||
} | |||
#[macro_export] | |||
macro_rules! try_w { | |||
($expr:expr) => { | |||
try_wr!($expr, ()) | |||
}; | |||
($expr:expr, $fmt:expr, $($arg:tt)+) => { | |||
try_wr!($expr, (), $fmt, $(arg)+) | |||
}; | |||
($expr:expr, $fmt:expr) => { | |||
try_wr!($expr, (), $fmt) | |||
} | |||
} | |||
#[macro_export] | |||
macro_rules! try_wr { | |||
($expr:expr, $ret:expr) => (match $expr { | |||
::std::result::Result::Ok(val) => val, | |||
::std::result::Result::Err(err) => { | |||
warn!("{:?}", err); | |||
return $ret; | |||
}, | |||
}); | |||
($expr:expr, $ret:expr, $fmt:expr) => (match $expr { | |||
::std::result::Result::Ok(val) => val, | |||
::std::result::Result::Err(err) => { | |||
warn!("Original error: {:?}", err); | |||
warn!($fmt); | |||
return $ret; | |||
}, | |||
}); | |||
($expr:expr, $ret:expr, $fmt:expr, $($arg:tt)+) => (match $expr { | |||
::std::result::Result::Ok(val) => val, | |||
::std::result::Result::Err(err) => { | |||
warn!("Original error: {:?}", err); | |||
warn!(format!($fmt, $(arg)+)); | |||
return $ret; | |||
}, | |||
}) | |||
} | |||
#[macro_export] | |||
macro_rules! try_r { | |||
($expr:expr, $ret:expr) => (match $expr { | |||
::std::result::Result::Ok(val) => val, | |||
::std::result::Result::Err(_) => { | |||
return $ret; | |||
}, | |||
}); | |||
} | |||
#[macro_export] | |||
macro_rules! try_e { | |||
($expr:expr) => { | |||
try_er!($expr, ()) | |||
}; | |||
($expr:expr, $fmt:expr, $($arg:tt)+) => { | |||
try_er!($expr, (), $fmt, $(arg)+) | |||
}; | |||
($expr:expr, $fmt:expr) => { | |||
try_er!($expr, (), $fmt) | |||
} | |||
} | |||
#[macro_export] | |||
macro_rules! try_er { | |||
($expr:expr, $ret:expr) => (match $expr { | |||
::std::result::Result::Ok(val) => val, | |||
::std::result::Result::Err(err) => { | |||
error!("{:?}", err); | |||
::std::process::exit(1); | |||
}, | |||
}); | |||
($expr:expr, $ret:expr, $fmt:expr) => (match $expr { | |||
::std::result::Result::Ok(val) => val, | |||
::std::result::Result::Err(err) => { | |||
error!("Original error: {:?}", err); | |||
error!($fmt); | |||
std::process::exit(1); | |||
}, | |||
}); | |||
($expr:expr, $ret:expr, $fmt:expr, $($arg:tt)+) => (match $expr { | |||
::std::result::Result::Ok(val) => val, | |||
::std::result::Result::Err(err) => { | |||
error!("Original error: {:?}", err); | |||
error!(format!($fmt, $(arg)+)); | |||
std::process::exit(1); | |||
}, | |||
}) | |||
} |
@@ -0,0 +1,20 @@ | |||
#[macro_export] | |||
macro_rules! create_builder_item { | |||
($sname:ident, $($element: ident: $ty: ty),+) => { | |||
pub struct $sname { | |||
$( | |||
pub $element: $ty | |||
),+ | |||
} | |||
impl $sname { | |||
pub fn new(builder: gtk::Builder) -> $sname { | |||
return $sname { | |||
$( | |||
$element: builder.get_object(stringify!($element)).unwrap() | |||
),+ | |||
}; | |||
} | |||
} | |||
} | |||
} |
@@ -0,0 +1,91 @@ | |||
#![feature(alloc_system)] | |||
extern crate alloc_system; | |||
extern crate flexi_logger; | |||
#[macro_use] | |||
extern crate log; | |||
#[macro_use] | |||
extern crate error_chain; | |||
#[macro_use] | |||
extern crate serde_derive; | |||
extern crate toml; | |||
extern crate serde; | |||
extern crate alsa; | |||
extern crate alsa_sys; | |||
extern crate ffi; | |||
extern crate gdk; | |||
extern crate gdk_pixbuf; | |||
extern crate gdk_pixbuf_sys; | |||
extern crate gdk_sys; | |||
extern crate gio; | |||
extern crate glib; | |||
extern crate glib_sys; | |||
extern crate gobject_sys; | |||
extern crate gtk; | |||
extern crate gtk_sys; | |||
extern crate libc; | |||
extern crate which; | |||
extern crate xdg; | |||
#[cfg(feature = "notify")] | |||
extern crate libnotify; | |||
use std::rc::Rc; | |||
#[macro_use] | |||
mod errors; | |||
#[macro_use] | |||
mod glade_helpers; | |||
mod alsa_card; | |||
mod app_state; | |||
mod audio; | |||
mod prefs; | |||
mod support_alsa; | |||
mod support_audio; | |||
mod support_cmd; | |||
#[macro_use] | |||
mod support_ui; | |||
mod ui_entry; | |||
mod ui_popup_menu; | |||
mod ui_popup_window; | |||
mod ui_prefs_dialog; | |||
mod ui_tray_icon; | |||
#[cfg(feature = "notify")] | |||
mod notif; | |||
use app_state::*; | |||
#[cfg(feature = "notify")] | |||
use libnotify::*; | |||
fn main() { | |||
gtk::init().unwrap(); | |||
// TODO: error handling | |||
#[cfg(feature = "notify")] | |||
init("PNMixer-rs").unwrap(); | |||
flexi_logger::LogOptions::new() | |||
.log_to_file(false) | |||
// ... your configuration options go here ... | |||
.init(Some("pnmixer=debug".to_string())) | |||
.unwrap_or_else(|e| panic!("Logger initialization failed with {}", e)); | |||
let apps = Rc::new(AppS::new()); | |||
ui_entry::init(apps); | |||
gtk::main(); | |||
#[cfg(feature = "notify")] | |||
uninit(); | |||
} |
@@ -0,0 +1,163 @@ | |||
use app_state::*; | |||
use audio::*; | |||
use errors::*; | |||
use glib::Variant; | |||
use glib::prelude::*; | |||
use gtk::DialogExt; | |||
use gtk::MessageDialogExt; | |||
use gtk::WidgetExt; | |||
use gtk::WindowExt; | |||
use gtk; | |||
use gtk_sys::{GTK_DIALOG_DESTROY_WITH_PARENT, GTK_RESPONSE_YES}; | |||
use libnotify; | |||
use prefs::*; | |||
use std::cell::Cell; | |||
use std::cell::RefCell; | |||
use std::rc::Rc; | |||
use std::thread; | |||
use std::time::Duration; | |||
use support_audio::*; | |||
use support_ui::*; | |||
use ui_popup_menu::*; | |||
use ui_popup_window::*; | |||
use ui_prefs_dialog::*; | |||
use ui_tray_icon::*; | |||
pub struct Notif { | |||
enabled: Cell<bool>, | |||
from_popup: Cell<bool>, | |||
from_tray: Cell<bool>, | |||
// TODO: from hotkey | |||
from_external: Cell<bool>, | |||
volume_notif: libnotify::Notification, | |||
text_notif: libnotify::Notification, | |||
} | |||
impl Notif { | |||
pub fn new(prefs: &Prefs) -> Result<Self> { | |||
let notif = Notif { | |||
enabled: Cell::new(false), | |||
from_popup: Cell::new(false), | |||
from_tray: Cell::new(false), | |||
from_external: Cell::new(false), | |||
volume_notif: libnotify::Notification::new("", None, None), | |||
text_notif: libnotify::Notification::new("", None, None), | |||
}; | |||
notif.reload(prefs)?; | |||
return Ok(notif); | |||
} | |||
pub fn reload(&self, prefs: &Prefs) -> Result<()> { | |||
let timeout = prefs.notify_prefs.notifcation_timeout; | |||
self.enabled.set(prefs.notify_prefs.enable_notifications); | |||
self.from_popup.set(prefs.notify_prefs.notify_popup); | |||
self.from_tray.set(prefs.notify_prefs.notify_mouse_scroll); | |||
self.from_external.set(prefs.notify_prefs.notify_external); | |||
self.volume_notif.set_timeout(timeout as i32); | |||
self.volume_notif.set_hint("x-canonical-private-synchronous", | |||
Some("".to_variant())); | |||
self.text_notif.set_timeout(timeout as i32); | |||
self.text_notif.set_hint("x-canonical-private-synchronous", | |||
Some("".to_variant())); | |||
return Ok(()); | |||
} | |||
pub fn show_volume_notif(&self, audio: &Audio) -> Result<()> { | |||
let vol = audio.vol()?; | |||
let vol_level = audio.vol_level(); | |||
let icon = { | |||
match vol_level { | |||
VolLevel::Muted => "audio-volume-muted", | |||
VolLevel::Off => "audio-volume-off", | |||
VolLevel::Low => "audio-volume-low", | |||
VolLevel::Medium => "audio-volume-medium", | |||
VolLevel::High => "audio-volume-high", | |||
} | |||
}; | |||
let summary = { | |||
match vol_level { | |||
VolLevel::Muted => String::from("Volume muted"), | |||
_ => { | |||
format!("{} ({})\nVolume: {}", | |||
audio.acard | |||
.borrow() | |||
.card_name()?, | |||
audio.acard | |||
.borrow() | |||
.chan_name()?, | |||
vol) | |||
} | |||
} | |||
}; | |||
// TODO: error handling | |||
self.volume_notif.update(summary.as_str(), None, Some(icon)).unwrap(); | |||
self.volume_notif.set_hint("value", Some((vol as i32).to_variant())); | |||
// TODO: error handling | |||
self.volume_notif.show().unwrap(); | |||
return Ok(()); | |||
} | |||
pub fn show_text_notif(&self, summary: &str, body: &str) -> Result<()> { | |||
// TODO: error handling | |||
self.text_notif.update(summary, Some(body), None).unwrap(); | |||
// TODO: error handling | |||
self.text_notif.show().unwrap(); | |||
return Ok(()); | |||
} | |||
} | |||
pub fn init_notify(appstate: Rc<AppS>) { | |||
debug!("Blah"); | |||
{ | |||
/* connect handler */ | |||
let apps = appstate.clone(); | |||
appstate.audio.connect_handler(Box::new(move |s, u| { | |||
let notif = &apps.notif; | |||
if !notif.enabled.get() { | |||
return; | |||
} | |||
match (s, | |||
u, | |||
(notif.from_popup.get(), | |||
notif.from_tray.get(), | |||
notif.from_external.get())) { | |||
(AudioSignal::NoCard, _, _) => try_w!(notif.show_text_notif("No sound card", "No playable soundcard found")), | |||
(AudioSignal::CardDisconnected, _, _) => try_w!(notif.show_text_notif("Soundcard disconnected", "Soundcard has been disconnected, reloading sound system...")), | |||
(AudioSignal::CardError, _, _) => (), | |||
(AudioSignal::ValuesChanged, | |||
AudioUser::TrayIcon, | |||
(_, true, _)) => try_w!(notif.show_volume_notif(&apps.audio)), | |||
(AudioSignal::ValuesChanged, | |||
AudioUser::Popup, | |||
(true, _, _)) => try_w!(notif.show_volume_notif(&apps.audio)), | |||
(AudioSignal::ValuesChanged, | |||
AudioUser::Unknown, | |||
(_, _, true)) => try_w!(notif.show_volume_notif(&apps.audio)), | |||
// TODO hotkeys | |||
_ => (), | |||
} | |||
})); | |||
} | |||
} |
@@ -0,0 +1,266 @@ | |||
use errors::*; | |||
use std::fmt::Display; | |||
use std::fmt::Formatter; | |||
use std::fs::File; | |||
use std::io::prelude::*; | |||
use std; | |||
use toml; | |||
use which; | |||
use xdg; | |||
const VOL_CONTROL_COMMANDS: [&str; 3] = | |||
["gnome-alsamixer", "xfce4-mixer", "alsamixergui"]; | |||
#[derive(Deserialize, Debug, Serialize, Clone, Copy)] | |||
#[serde(rename_all = "snake_case")] | |||
pub enum MiddleClickAction { | |||
ToggleMute, | |||
ShowPreferences, | |||
VolumeControl, | |||
CustomCommand, | |||
} | |||
impl Default for MiddleClickAction { | |||
fn default() -> MiddleClickAction { | |||
return MiddleClickAction::ToggleMute; | |||
} | |||
} | |||
impl From<i32> for MiddleClickAction { | |||
fn from(i: i32) -> Self { | |||
match i { | |||
0 => MiddleClickAction::ToggleMute, | |||
1 => MiddleClickAction::ShowPreferences, | |||
2 => MiddleClickAction::VolumeControl, | |||
3 => MiddleClickAction::CustomCommand, | |||
_ => MiddleClickAction::ToggleMute, | |||
} | |||
} | |||
} | |||
impl From<MiddleClickAction> for i32 { | |||
fn from(action: MiddleClickAction) -> Self { | |||
match action { | |||
MiddleClickAction::ToggleMute => 0, | |||
MiddleClickAction::ShowPreferences => 1, | |||
MiddleClickAction::VolumeControl => 2, | |||
MiddleClickAction::CustomCommand => 3, | |||
} | |||
} | |||
} | |||
#[derive(Deserialize, Debug, Serialize)] | |||
#[serde(default)] | |||
pub struct DevicePrefs { | |||
pub card: String, | |||
pub channel: String, | |||
// TODO: normalize volume? | |||
} | |||
impl Default for DevicePrefs { | |||
fn default() -> DevicePrefs { | |||
return DevicePrefs { | |||
card: String::from("(default)"), | |||
channel: String::from("Master"), | |||
}; | |||
} | |||
} | |||
#[derive(Deserialize, Debug, Serialize)] | |||
#[serde(default)] | |||
pub struct ViewPrefs { | |||
pub draw_vol_meter: bool, | |||
pub vol_meter_offset: i32, | |||
pub system_theme: bool, | |||
pub vol_meter_color: VolColor, | |||
// TODO: Display text folume/text volume pos? | |||
} | |||
impl Default for ViewPrefs { | |||
fn default() -> ViewPrefs { | |||
return ViewPrefs { | |||
draw_vol_meter: true, | |||
vol_meter_offset: 10, | |||
system_theme: true, | |||
vol_meter_color: VolColor::default(), | |||
}; | |||
} | |||
} | |||
#[derive(Deserialize, Debug, Serialize)] | |||
#[serde(default)] | |||
pub struct VolColor { | |||
pub red: f64, | |||
pub green: f64, | |||
pub blue: f64, | |||
} | |||
impl Default for VolColor { | |||
fn default() -> VolColor { | |||
return VolColor { | |||
red: 0.960784313725, | |||
green: 0.705882352941, | |||
blue: 0.0, | |||
}; | |||
} | |||
} | |||
#[derive(Deserialize, Debug, Serialize)] | |||
#[serde(default)] | |||
pub struct BehaviorPrefs { | |||
pub unmute_on_vol_change: bool, | |||
pub vol_control_cmd: Option<String>, | |||
pub vol_scroll_step: f64, | |||
pub vol_fine_scroll_step: f64, | |||
pub middle_click_action: MiddleClickAction, | |||
pub custom_command: Option<String>, // TODO: fine scroll step? | |||
} | |||
impl Default for BehaviorPrefs { | |||
fn default() -> BehaviorPrefs { | |||
return BehaviorPrefs { | |||
unmute_on_vol_change: true, | |||
vol_control_cmd: None, | |||
vol_scroll_step: 5.0, | |||
vol_fine_scroll_step: 1.0, | |||
middle_click_action: MiddleClickAction::default(), | |||
custom_command: None, | |||
}; | |||
} | |||
} | |||
#[cfg(feature = "notify")] | |||
#[derive(Deserialize, Debug, Serialize)] | |||
#[serde(default)] | |||
pub struct NotifyPrefs { | |||
pub enable_notifications: bool, | |||
pub notifcation_timeout: i64, | |||
pub notify_mouse_scroll: bool, | |||
pub notify_popup: bool, | |||
pub notify_external: bool, | |||
// TODO: notify_hotkeys? | |||
} | |||
#[cfg(feature = "notify")] | |||
impl Default for NotifyPrefs { | |||
fn default() -> NotifyPrefs { | |||
return NotifyPrefs { | |||
enable_notifications: true, | |||
notifcation_timeout: 1500, | |||
notify_mouse_scroll: true, | |||
notify_popup: true, | |||
notify_external: true, | |||
}; | |||
} | |||
} | |||
#[derive(Deserialize, Debug, Serialize, Default)] | |||
#[serde(default)] | |||
pub struct Prefs { | |||
pub device_prefs: DevicePrefs, | |||
pub view_prefs: ViewPrefs, | |||
pub behavior_prefs: BehaviorPrefs, | |||
#[cfg(feature = "notify")] | |||
pub notify_prefs: NotifyPrefs, | |||
// TODO: HotKeys? | |||
} | |||
impl Prefs { | |||
pub fn new() -> Result<Prefs> { | |||
let m_config_file = get_xdg_dirs().find_config_file("pnmixer.toml"); | |||
match m_config_file { | |||
Some(c) => { | |||
debug!("Config file present at {:?}, using it.", c); | |||
let mut f = File::open(c)?; | |||
let mut buffer = vec![]; | |||
f.read_to_end(&mut buffer)?; | |||
let prefs = toml::from_slice(buffer.as_slice())?; | |||
return Ok(prefs); | |||
} | |||
None => { | |||
debug!("No config file present, creating one with defaults."); | |||
let prefs = Prefs::default(); | |||
prefs.store_config()?; | |||
return Ok(prefs); | |||
} | |||
} | |||
} | |||
pub fn reload_config(&mut self) -> Result<()> { | |||
debug!("Reloading config..."); | |||
let new_prefs = Prefs::new()?; | |||
*self = new_prefs; | |||
return Ok(()); | |||
} | |||
pub fn store_config(&self) -> Result<()> { | |||
let config_path = get_xdg_dirs().place_config_file("pnmixer.toml") | |||
.from_err()?; | |||
debug!("Storing config in {:?}", config_path); | |||
let mut f = File::create(config_path)?; | |||
f.write_all(self.to_str().as_bytes())?; | |||
return Ok(()); | |||
} | |||
pub fn to_str(&self) -> String { | |||
return toml::to_string(self).unwrap(); | |||
} | |||
pub fn get_avail_vol_control_cmd(&self) -> Option<String> { | |||
match self.behavior_prefs.vol_control_cmd { | |||
Some(ref c) => return Some(c.clone()), | |||
None => { | |||
for command in VOL_CONTROL_COMMANDS.iter() { | |||
if which::which(command).is_ok() { | |||
return Some(String::from(*command)); | |||
} | |||
} | |||
} | |||
} | |||
return None; | |||
} | |||
} | |||
impl Display for Prefs { | |||
fn fmt(&self, | |||
f: &mut Formatter) | |||
-> std::result::Result<(), std::fmt::Error> { | |||
let s = self.to_str(); | |||
return write!(f, "{}", s); | |||
} | |||
} | |||
fn get_xdg_dirs() -> xdg::BaseDirectories { | |||
return xdg::BaseDirectories::with_prefix("pnmixer-rs").unwrap(); | |||
} |
@@ -0,0 +1,148 @@ | |||
use alsa::card::Card; | |||
use alsa::mixer::{Mixer, Selem, SelemId, Elem}; | |||
use alsa; | |||
use errors::*; | |||
use libc::c_int; | |||
use std::iter::Map; | |||
use std::iter::Filter; | |||
pub fn get_default_alsa_card() -> Card { | |||
return get_alsa_card_by_id(0); | |||
} | |||
pub fn get_alsa_card_by_id(index: c_int) -> Card { | |||
return Card::new(index); | |||
} | |||
pub fn get_alsa_cards() -> alsa::card::Iter { | |||
return alsa::card::Iter::new(); | |||
} | |||
pub fn get_first_playable_alsa_card() -> Result<Card> { | |||
for m_card in get_alsa_cards() { | |||
match m_card { | |||
Ok(card) => { | |||
if alsa_card_has_playable_selem(&card) { | |||
return Ok(card); | |||
} | |||
} | |||
_ => (), | |||
} | |||
} | |||
bail!("No playable alsa card found!") | |||
} | |||
pub fn get_playable_alsa_card_names() -> Vec<String> { | |||
let mut vec = vec![]; | |||
for m_card in get_alsa_cards() { | |||
match m_card { | |||
Ok(card) => { | |||
if alsa_card_has_playable_selem(&card) { | |||
let m_name = card.get_name(); | |||
if m_name.is_ok() { | |||
vec.push(m_name.unwrap()) | |||
} | |||
} | |||
} | |||
_ => (), | |||
} | |||
} | |||
return vec; | |||
} | |||
pub fn get_alsa_card_by_name(name: String) -> Result<Card> { | |||
for r_card in get_alsa_cards() { | |||
let card = r_card?; | |||
let card_name = card.get_name()?; | |||
if name == card_name { | |||
return Ok(card); | |||
} | |||
} | |||
bail!("Not found a matching card named {}", name); | |||
} | |||
pub fn alsa_card_has_playable_selem(card: &Card) -> bool { | |||
let mixer = try_wr!(get_mixer(&card), false); | |||
for selem in get_playable_selems(&mixer) { | |||
if selem_is_playable(&selem) { | |||
return true; | |||
} | |||
} | |||
return false; | |||
} | |||
pub fn get_mixer(card: &Card) -> Result<Mixer> { | |||
return Mixer::new(&format!("hw:{}", card.get_index()), false).from_err(); | |||
} | |||
pub fn get_selem(elem: Elem) -> Selem { | |||
/* in the ALSA API, there are currently only simple elements, | |||
* so this unwrap() should be safe. | |||
*http://www.alsa-project.org/alsa-doc/alsa-lib/group___mixer.html#enum-members */ | |||
return Selem::new(elem).unwrap(); | |||
} | |||
pub fn get_playable_selems(mixer: &Mixer) -> Vec<Selem> { | |||
let mut v = vec![]; | |||
for s in mixer.iter().map(get_selem).filter(selem_is_playable) { | |||
v.push(s); | |||
} | |||
return v; | |||
} | |||
pub fn get_first_playable_selem(mixer: &Mixer) -> Result<Selem> { | |||
for s in mixer.iter().map(get_selem).filter(selem_is_playable) { | |||
return Ok(s); | |||
} | |||
bail!("No playable Selem found!") | |||
} | |||
pub fn get_playable_selem_names(mixer: &Mixer) -> Vec<String> { | |||
let mut vec = vec![]; | |||
for selem in get_playable_selems(mixer) { | |||
let n = selem.get_id().get_name().map(|y| String::from(y)); | |||
match n { | |||
Ok(name) => vec.push(name), | |||
_ => (), | |||
} | |||
} | |||
return vec; | |||
} | |||
pub fn get_playable_selem_by_name(mixer: &Mixer, | |||
name: String) | |||
-> Result<Selem> { | |||
for selem in get_playable_selems(mixer) { | |||
let n = selem.get_id() | |||
.get_name() | |||
.map(|y| String::from(y))?; | |||
if n == name { | |||
return Ok(selem); | |||
} | |||
} | |||
bail!("Not found a matching playable selem named {}", name); | |||
} | |||
pub fn selem_is_playable(selem: &Selem) -> bool { | |||
return selem.has_playback_volume(); | |||
} |
@@ -0,0 +1,64 @@ | |||
use audio::{Audio, AudioUser}; | |||
use errors::*; | |||
use prefs::*; | |||
#[derive(Clone, Copy, Debug)] | |||
pub enum VolDir { | |||
Up, | |||
Down, | |||
Unknown, | |||
} | |||
pub fn vol_change_to_voldir(old: f64, new: f64) -> VolDir { | |||
if old < new { | |||
return VolDir::Up; | |||
} else if old > new { | |||
return VolDir::Down; | |||
} else { | |||
return VolDir::Unknown; | |||
} | |||
} | |||
pub fn lrint(v: f64, dir: VolDir) -> f64 { | |||
match dir { | |||
VolDir::Up => v.ceil(), | |||
VolDir::Down => v.floor(), | |||
_ => v, | |||
} | |||
} | |||
pub fn audio_reload(audio: &Audio, | |||
prefs: &Prefs, | |||
user: AudioUser) | |||
-> Result<()> { | |||
let card = &prefs.device_prefs.card; | |||
let channel = &prefs.device_prefs.channel; | |||
// TODO: is this clone safe? | |||
return audio.switch_acard(Some(card.clone()), Some(channel.clone()), user); | |||
} | |||
pub fn vol_to_percent(vol: i64, range: (i64, i64)) -> Result<f64> { | |||
let (min, max) = range; | |||
ensure!(min < max, | |||
"Invalid playback volume range [{} - {}]", | |||
min, | |||
max); | |||
let perc = ((vol - min) as f64) / ((max - min) as f64) * 100.0; | |||
return Ok(perc); | |||
} | |||
pub fn percent_to_vol(vol: f64, range: (i64, i64), dir: VolDir) -> Result<i64> { | |||
let (min, max) = range; | |||
ensure!(min < max, | |||
"Invalid playback volume range [{} - {}]", | |||
min, | |||
max); | |||
let _v = lrint(vol / 100.0 * ((max - min) as f64), dir) + (min as f64); | |||
return Ok(_v as i64); | |||
} |
@@ -0,0 +1,26 @@ | |||
use errors::*; | |||
use glib; | |||
use prefs::Prefs; | |||
use std::error::Error; | |||
use std; | |||
pub fn execute_vol_control_command(prefs: &Prefs) -> Result<()> { | |||
let m_cmd = prefs.get_avail_vol_control_cmd(); | |||
match m_cmd { | |||
Some(ref cmd) => execute_command(cmd.as_str()), | |||
None => bail!("No command found"), | |||
} | |||
} | |||
pub fn execute_command(cmd: &str) -> Result<()> { | |||
return glib::spawn_command_line_async(cmd) | |||
.map_err(|e| { | |||
std::io::Error::new(std::io::ErrorKind::Other, | |||
e.description()) | |||
}) | |||
.from_err(); | |||
} |
@@ -0,0 +1,90 @@ | |||
use errors::*; | |||
use gdk_pixbuf; | |||
use gdk_pixbuf_sys; | |||
use glib::translate::FromGlibPtrFull; | |||
use glib::translate::ToGlibPtr; | |||
use gtk::prelude::*; | |||
use gtk; | |||
use std::path::*; | |||
pub fn copy_pixbuf(pixbuf: &gdk_pixbuf::Pixbuf) -> gdk_pixbuf::Pixbuf { | |||
let new_pixbuf = unsafe { | |||
let gdk_pixbuf = pixbuf.to_glib_full(); | |||
let copy = gdk_pixbuf_sys::gdk_pixbuf_copy(gdk_pixbuf); | |||
FromGlibPtrFull::from_glib_full(copy) | |||
}; | |||
return new_pixbuf; | |||
} | |||
pub fn pixbuf_new_from_theme(icon_name: &str, | |||
size: i32, | |||
theme: >k::IconTheme) | |||
-> Result<gdk_pixbuf::Pixbuf> { | |||
let icon_info = | |||
theme.lookup_icon(icon_name, size, gtk::IconLookupFlags::empty()) | |||
.ok_or(format!("Couldn't find icon {}", icon_name))?; | |||
debug!("Loading stock icon {} from {:?}", | |||
icon_name, | |||
icon_info.get_filename().unwrap_or(PathBuf::new())); | |||
// TODO: propagate error | |||
let pixbuf = icon_info.load_icon().unwrap(); | |||
return Ok(pixbuf); | |||
} | |||
pub fn pixbuf_new_from_file(filename: &str) -> Result<gdk_pixbuf::Pixbuf> { | |||
ensure!(!filename.is_empty(), "Filename is empty"); | |||
let mut syspath = String::new(); | |||
let sysdir = option_env!("PIXMAPSDIR").map(|s| { | |||
syspath = format!("{}/{}", | |||
s, | |||
filename); | |||
Path::new(syspath.as_str()) | |||
}); | |||
let cargopath = format!("./data/pixmaps/{}", filename); | |||
let cargodir = Path::new(cargopath.as_str()); | |||
// prefer local dir | |||
let final_dir = { | |||
if cargodir.exists() { | |||
cargodir | |||
} else if sysdir.is_some() && sysdir.unwrap().exists() { | |||
sysdir.unwrap() | |||
} else { | |||
bail!("No valid path found") | |||
} | |||
}; | |||
let str_path = final_dir.to_str().ok_or("Path is not valid unicode")?; | |||
debug!("Loading icon from {}", str_path); | |||
// TODO: propagate error | |||
return Ok(gdk_pixbuf::Pixbuf::new_from_file(str_path).unwrap()); | |||
} | |||
#[macro_export] | |||
macro_rules! pixbuf_new_from_xpm { | |||
($name:ident) => { | |||
{ | |||
use glib::translate::from_glib_full; | |||
use libc::c_char; | |||
extern "C" { fn $name() -> *mut *mut c_char; }; | |||
unsafe { | |||
from_glib_full( | |||
gdk_pixbuf_sys::gdk_pixbuf_new_from_xpm_data($name())) | |||
} | |||
} | |||
} | |||
} |
@@ -0,0 +1,102 @@ | |||
use app_state::*; | |||
use audio::{AudioUser, AudioSignal}; | |||
use gtk::DialogExt; | |||
use gtk::MessageDialogExt; | |||
use gtk::WidgetExt; | |||
use gtk::WindowExt; | |||
use gtk; | |||
use gtk_sys::GTK_RESPONSE_YES; | |||
use prefs::*; | |||
use std::cell::RefCell; | |||
use std::rc::Rc; | |||
use support_audio::*; | |||
use ui_popup_menu::*; | |||
use ui_popup_window::*; | |||
use ui_prefs_dialog::*; | |||
use ui_tray_icon::*; | |||
#[cfg(feature = "notify")] | |||
use notif::*; | |||
pub struct Gui { | |||
_cant_construct: (), | |||
pub tray_icon: TrayIcon, | |||
pub popup_window: PopupWindow, | |||
pub popup_menu: PopupMenu, | |||
/* prefs_dialog is dynamically created and destroyed */ | |||
pub prefs_dialog: RefCell<Option<PrefsDialog>>, | |||
} | |||
impl Gui { | |||
pub fn new(builder_popup_window: gtk::Builder, | |||
builder_popup_menu: gtk::Builder, | |||
prefs: &Prefs) | |||
-> Gui { | |||
return Gui { | |||
_cant_construct: (), | |||
tray_icon: TrayIcon::new(prefs).unwrap(), | |||
popup_window: PopupWindow::new(builder_popup_window), | |||
popup_menu: PopupMenu::new(builder_popup_menu), | |||
prefs_dialog: RefCell::new(None), | |||
}; | |||
} | |||
} | |||
pub fn init(appstate: Rc<AppS>) { | |||
{ | |||
/* "global" audio signal handler */ | |||
let apps = appstate.clone(); | |||
appstate.audio.connect_handler( | |||
Box::new(move |s, u| match (s, u) { | |||
(AudioSignal::CardDisconnected, _) => { | |||
try_w!(audio_reload(&apps.audio, | |||
&apps.prefs.borrow(), | |||
AudioUser::Unknown)); | |||
}, | |||
(AudioSignal::CardError, _) => { | |||
if run_audio_error_dialog(&apps.gui.popup_menu.menu_window) == (GTK_RESPONSE_YES as i32) { | |||
try_w!(audio_reload(&apps.audio, | |||
&apps.prefs.borrow(), | |||
AudioUser::Unknown)); | |||
} | |||
}, | |||
_ => (), | |||
} | |||
)); | |||
} | |||
init_tray_icon(appstate.clone()); | |||
init_popup_window(appstate.clone()); | |||
init_popup_menu(appstate.clone()); | |||
init_prefs_callback(appstate.clone()); | |||
#[cfg(feature = "notify")] | |||
init_notify(appstate.clone()); | |||
} | |||
fn run_audio_error_dialog(parent: >k::Window) -> i32 { | |||
error!("Connection with audio failed, you probably need to restart pnmixer."); | |||
let dialog = gtk::MessageDialog::new(Some(parent), | |||
gtk::DIALOG_DESTROY_WITH_PARENT, | |||
gtk::MessageType::Error, | |||
gtk::ButtonsType::YesNo, | |||
"Warning: Connection to sound system failed."); | |||
dialog.set_property_secondary_text(Some("Do you want to re-initialize the audio connection ? | |||
If you do not, you will either need to restart PNMixer | |||
or select the 'Reload Audio' option in the right-click | |||
menu in order for PNMixer to function.")); | |||
dialog.set_title("PNMixer-rs Error"); | |||
let resp = dialog.run(); | |||
dialog.destroy(); | |||
return resp; | |||
} |
@@ -0,0 +1,177 @@ | |||
use app_state::*; | |||
use audio::{AudioUser, AudioSignal}; | |||
use gtk::prelude::*; | |||
use gtk; | |||
use std::rc::Rc; | |||
use support_audio::*; | |||
use support_cmd::*; | |||
use ui_prefs_dialog::*; | |||
const VERSION: &'static str = env!("CARGO_PKG_VERSION"); | |||
create_builder_item!(PopupMenu, | |||
menu_window: gtk::Window, | |||
menubar: gtk::MenuBar, | |||
menu: gtk::Menu, | |||
about_item: gtk::MenuItem, | |||
mixer_item: gtk::MenuItem, | |||
mute_item: gtk::MenuItem, | |||
mute_check: gtk::CheckButton, | |||
prefs_item: gtk::MenuItem, | |||
quit_item: gtk::MenuItem, | |||
reload_item: gtk::MenuItem); | |||
pub fn init_popup_menu(appstate: Rc<AppS>) { | |||
/* audio.connect_handler */ | |||
{ | |||
let apps = appstate.clone(); | |||
appstate.audio.connect_handler(Box::new(move |s, u| { | |||
/* skip if window is hidden */ | |||
if !apps.gui | |||
.popup_menu | |||
.menu | |||
.get_visible() { | |||
return; | |||
} | |||
match (s, u) { | |||
(_, _) => set_mute_check(&apps), | |||
} | |||
})); | |||
} | |||
/* popup_menu.menu.connect_show */ | |||
{ | |||
let apps = appstate.clone(); | |||
appstate.gui | |||
.popup_menu | |||
.menu | |||
.connect_show(move |_| set_mute_check(&apps)); | |||
} | |||
/* mixer_item.connect_activate_link */ | |||
{ | |||
let apps = appstate.clone(); | |||
let mixer_item = &appstate.gui.popup_menu.mixer_item; | |||
mixer_item.connect_activate(move |_| { | |||
try_w!(execute_vol_control_command(&apps.prefs.borrow())); | |||
}); | |||
} | |||
/* mute_item.connect_activate_link */ | |||
{ | |||
let apps = appstate.clone(); | |||
let mute_item = &appstate.gui.popup_menu.mute_item; | |||
mute_item.connect_activate(move |_| { | |||
if apps.audio.has_mute() { | |||
try_w!(apps.audio.toggle_mute(AudioUser::Popup)); | |||
} | |||
}); | |||
} | |||
/* about_item.connect_activate_link */ | |||
{ | |||
let apps = appstate.clone(); | |||
let about_item = &appstate.gui.popup_menu.about_item; | |||
about_item.connect_activate(move |_| { | |||
on_about_item_activate(&apps); | |||
}); | |||
} | |||
/* prefs_item.connect_activate_link */ | |||
{ | |||
let apps = appstate.clone(); | |||
let prefs_item = &appstate.gui.popup_menu.prefs_item; | |||
prefs_item.connect_activate(move |_| { | |||
on_prefs_item_activate(&apps); | |||
}); | |||
} | |||
/* reload_item.connect_activate_link */ | |||
{ | |||
let apps = appstate.clone(); | |||
let reload_item = &appstate.gui.popup_menu.reload_item; | |||
reload_item.connect_activate(move |_| { | |||
try_w!(audio_reload(&apps.audio, | |||
&apps.prefs.borrow(), | |||
AudioUser::Popup)) | |||
}); | |||
} | |||
/* quit_item.connect_activate_link */ | |||
{ | |||
let quit_item = &appstate.gui.popup_menu.quit_item; | |||
quit_item.connect_activate(|_| { gtk::main_quit(); }); | |||
} | |||
} | |||
fn on_about_item_activate(appstate: &AppS) { | |||
let popup_menu = &appstate.gui.popup_menu.menu_window; | |||
let about_dialog = create_about_dialog(); | |||
about_dialog.set_skip_taskbar_hint(true); | |||
about_dialog.set_transient_for(popup_menu); | |||
about_dialog.run(); | |||
about_dialog.destroy(); | |||
} | |||
fn create_about_dialog() -> gtk::AboutDialog { | |||
let about_dialog: gtk::AboutDialog = gtk::AboutDialog::new(); | |||
about_dialog.set_license(Some( | |||
"PNMixer-rs is free software; you can redistribute it and/or modify it | |||
under the terms of the GNU General Public License v3 as published | |||
by the Free Software Foundation. | |||
PNMixer 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 PNMixer; if not, write to the Free Software Foundation, | |||
Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.", | |||
)); | |||
about_dialog.set_copyright(Some("Copyright © 2017 Julian Ospald")); | |||
about_dialog.set_authors(&["Julian Ospald"]); | |||
about_dialog.set_artists(&["Paul Davey"]); | |||
about_dialog.set_program_name("PNMixer-rs"); | |||
about_dialog.set_logo_icon_name("pnmixer"); | |||
about_dialog.set_version(VERSION); | |||
about_dialog.set_website("https://github.com/hasufell/pnmixer-rust"); | |||
about_dialog.set_comments("A mixer for the system tray"); | |||
return about_dialog; | |||
} | |||
fn on_prefs_item_activate(appstate: &Rc<AppS>) { | |||
/* TODO: only create if needed */ | |||
show_prefs_dialog(appstate); | |||
} | |||
fn set_mute_check(apps: &Rc<AppS>) { | |||
let mute_check = &apps.gui.popup_menu.mute_check; | |||
let m_muted = apps.audio.get_mute(); | |||
match m_muted { | |||
Ok(muted) => { | |||
mute_check.set_sensitive(false); | |||
mute_check.set_active(muted); | |||
mute_check.set_tooltip_text(""); | |||
} | |||
Err(_) => { | |||
mute_check.set_active(true); | |||
mute_check.set_sensitive(false); | |||
mute_check.set_tooltip_text("Soundcard has no mute switch"); | |||
} | |||
} | |||
} |
@@ -0,0 +1,300 @@ | |||
use app_state::*; | |||
use audio::*; | |||
use errors::*; | |||
use gdk::DeviceExt; | |||
use gdk::{GrabOwnership, GrabStatus, BUTTON_PRESS_MASK, KEY_PRESS_MASK}; | |||
use gdk; | |||
use gdk_sys::{GDK_KEY_Escape, GDK_CURRENT_TIME}; | |||
use glib; | |||
use gtk::ToggleButtonExt; | |||
use gtk::prelude::*; | |||
use gtk; | |||
use prefs::*; | |||
use std::cell::Cell; | |||
use std::rc::Rc; | |||
use support_audio::*; | |||
use support_cmd::*; | |||
pub struct PopupWindow { | |||
_cant_construct: (), | |||
pub popup_window: gtk::Window, | |||
pub vol_scale_adj: gtk::Adjustment, | |||
pub vol_scale: gtk::Scale, | |||
pub mute_check: gtk::CheckButton, | |||
pub mixer_button: gtk::Button, | |||
pub toggle_signal: Cell<u64>, | |||
pub changed_signal: Cell<u64>, | |||
} | |||
impl PopupWindow { | |||
pub fn new(builder: gtk::Builder) -> PopupWindow { | |||
return PopupWindow { | |||
_cant_construct: (), | |||
popup_window: builder.get_object("popup_window").unwrap(), | |||
vol_scale_adj: builder.get_object("vol_scale_adj").unwrap(), | |||
vol_scale: builder.get_object("vol_scale").unwrap(), | |||
mute_check: builder.get_object("mute_check").unwrap(), | |||
mixer_button: builder.get_object("mixer_button").unwrap(), | |||
toggle_signal: Cell::new(0), | |||
changed_signal: Cell::new(0), | |||
}; | |||
} | |||
pub fn update(&self, audio: &Audio) -> Result<()> { | |||
let cur_vol = audio.vol()?; | |||
set_slider(&self.vol_scale_adj, cur_vol); | |||
self.update_mute_check(&audio); | |||
return Ok(()); | |||
} | |||
pub fn update_mute_check(&self, audio: &Audio) { | |||
let m_muted = audio.get_mute(); | |||
glib::signal_handler_block(&self.mute_check, self.toggle_signal.get()); | |||
match m_muted { | |||
Ok(val) => { | |||
self.mute_check.set_sensitive(true); | |||
self.mute_check.set_active(val); | |||
self.mute_check.set_tooltip_text(""); | |||
} | |||
Err(_) => { | |||
/* can't figure out whether channel is muted, grey out */ | |||
self.mute_check.set_active(true); | |||
self.mute_check.set_sensitive(false); | |||
self.mute_check.set_tooltip_text( | |||
"Soundcard has no mute switch", | |||
); | |||
} | |||
} | |||
glib::signal_handler_unblock(&self.mute_check, | |||
self.toggle_signal.get()); | |||
} | |||
fn set_vol_increment(&self, prefs: &Prefs) { | |||
self.vol_scale_adj | |||
.set_page_increment(prefs.behavior_prefs.vol_scroll_step); | |||
self.vol_scale_adj | |||
.set_step_increment(prefs.behavior_prefs.vol_fine_scroll_step); | |||
} | |||
} | |||
pub fn init_popup_window(appstate: Rc<AppS>) { | |||
/* audio.connect_handler */ | |||
{ | |||
let apps = appstate.clone(); | |||
appstate.audio.connect_handler(Box::new(move |s, u| { | |||
/* skip if window is hidden */ | |||
if !apps.gui | |||
.popup_window | |||
.popup_window | |||
.get_visible() { | |||
return; | |||
} | |||
match (s, u) { | |||
/* Update only mute check here | |||
* If the user changes the volume through the popup window, | |||
* we MUST NOT update the slider value, it's been done already. | |||
* It means that, as long as the popup window is visible, | |||
* the slider value reflects the value set by user, | |||
* and not the real value reported by the audio system. | |||
*/ | |||
(_, AudioUser::Popup) => { | |||
apps.gui.popup_window.update_mute_check(&apps.audio); | |||
} | |||
/* external change, safe to update slider too */ | |||
(_, _) => { | |||
try_w!(apps.gui.popup_window.update(&apps.audio)); | |||
} | |||
} | |||
})); | |||
} | |||
/* mute_check.connect_toggled */ | |||
{ | |||
let _appstate = appstate.clone(); | |||
let mute_check = &appstate.clone() | |||
.gui | |||
.popup_window | |||
.mute_check; | |||
let toggle_signal = | |||
mute_check.connect_toggled(move |_| { | |||
on_mute_check_toggled(&_appstate) | |||
}); | |||
appstate.gui | |||
.popup_window | |||
.toggle_signal | |||
.set(toggle_signal); | |||
} | |||
/* popup_window.connect_show */ | |||
{ | |||
let _appstate = appstate.clone(); | |||
let popup_window = &appstate.clone() | |||
.gui | |||
.popup_window | |||
.popup_window; | |||
popup_window.connect_show(move |_| on_popup_window_show(&_appstate)); | |||
} | |||
/* vol_scale_adj.connect_value_changed */ | |||
{ | |||
let _appstate = appstate.clone(); | |||
let vol_scale_adj = &appstate.clone() | |||
.gui | |||
.popup_window | |||
.vol_scale_adj; | |||
let changed_signal = vol_scale_adj.connect_value_changed( | |||
move |_| on_vol_scale_value_changed(&_appstate), | |||
); | |||
appstate.gui | |||
.popup_window | |||
.changed_signal | |||
.set(changed_signal); | |||
} | |||
/* popup_window.connect_event */ | |||
{ | |||
let popup_window = &appstate.clone() | |||
.gui | |||
.popup_window | |||
.popup_window; | |||
popup_window.connect_event(move |w, e| on_popup_window_event(w, e)); | |||
} | |||
/* mixer_button.connect_clicked */ | |||
{ | |||
let apps = appstate.clone(); | |||
let mixer_button = &appstate.clone() | |||
.gui | |||
.popup_window | |||
.mixer_button; | |||
mixer_button.connect_clicked(move |_| { | |||
apps.gui | |||
.popup_window | |||
.popup_window | |||
.hide(); | |||
try_w!(execute_vol_control_command(&apps.prefs.borrow())); | |||
}); | |||
} | |||
} | |||
fn on_popup_window_show(appstate: &AppS) { | |||
let popup_window = &appstate.gui.popup_window; | |||
appstate.gui.popup_window.set_vol_increment(&appstate.prefs.borrow()); | |||
glib::signal_handler_block(&popup_window.vol_scale_adj, | |||
popup_window.changed_signal.get()); | |||
try_w!(appstate.gui.popup_window.update(&appstate.audio)); | |||
glib::signal_handler_unblock(&popup_window.vol_scale_adj, | |||
popup_window.changed_signal.get()); | |||
popup_window.vol_scale.grab_focus(); | |||
try_w!(grab_devices(&appstate.gui.popup_window.popup_window)); | |||
} | |||
fn on_popup_window_event(w: >k::Window, e: &gdk::Event) -> gtk::Inhibit { | |||
match gdk::Event::get_event_type(e) { | |||
gdk::EventType::GrabBroken => w.hide(), | |||
gdk::EventType::KeyPress => { | |||
let key: gdk::EventKey = e.clone().downcast().unwrap(); | |||
if key.get_keyval() == (GDK_KEY_Escape as u32) { | |||
w.hide(); | |||
} | |||
} | |||
gdk::EventType::ButtonPress => { | |||
let device = try_wr!( | |||
gtk::get_current_event_device().ok_or( | |||
"No current event device!", | |||
), | |||
Inhibit(false) | |||
); | |||
let (window, _, _) = | |||
gdk::DeviceExt::get_window_at_position(&device); | |||
if window.is_none() { | |||
w.hide(); | |||
} | |||
} | |||
_ => (), | |||
} | |||
return Inhibit(false); | |||
} | |||
fn on_vol_scale_value_changed(appstate: &AppS) { | |||
let audio = &appstate.audio; | |||
let old_vol = try_w!(audio.vol()); | |||
let val = appstate.gui | |||
.popup_window | |||
.vol_scale | |||
.get_value(); | |||
let dir = vol_change_to_voldir(old_vol, val); | |||
try_w!(audio.set_vol(val, | |||
AudioUser::Popup, | |||
dir, | |||
appstate.prefs | |||
.borrow() | |||
.behavior_prefs | |||
.unmute_on_vol_change)); | |||
} | |||
fn on_mute_check_toggled(appstate: &AppS) { | |||
let audio = &appstate.audio; | |||
try_w!(audio.toggle_mute(AudioUser::Popup)) | |||
} | |||
pub fn set_slider(vol_scale_adj: >k::Adjustment, scale: f64) { | |||
vol_scale_adj.set_value(scale); | |||
} | |||
fn grab_devices(window: >k::Window) -> Result<()> { | |||
let device = gtk::get_current_event_device().ok_or("No current device")?; | |||
let gdk_window = window.get_window().ok_or("No window?!")?; | |||
/* Grab the mouse */ | |||
let m_grab_status = | |||
device.grab(&gdk_window, | |||
GrabOwnership::None, | |||
true, | |||
BUTTON_PRESS_MASK, | |||
None, | |||
GDK_CURRENT_TIME as u32); | |||
if m_grab_status != GrabStatus::Success { | |||
warn!("Could not grab {}", | |||
device.get_name().unwrap_or(String::from("UNKNOWN DEVICE"))); | |||
} | |||
/* Grab the keyboard */ | |||
let k_dev = device.get_associated_device() | |||
.ok_or("Couldn't get associated device")?; | |||
let k_grab_status = k_dev.grab(&gdk_window, | |||
GrabOwnership::None, | |||
true, | |||
KEY_PRESS_MASK, | |||
None, | |||
GDK_CURRENT_TIME as u32); | |||
if k_grab_status != GrabStatus::Success { | |||
warn!("Could not grab {}", | |||
k_dev.get_name().unwrap_or(String::from("UNKNOWN DEVICE"))); | |||
} | |||
return Ok(()); | |||
} |
@@ -0,0 +1,413 @@ | |||
use app_state::*; | |||
use audio::{AudioUser, AudioSignal}; | |||
use errors::*; | |||
use gdk; | |||
use gtk::ResponseType; | |||
use gtk::prelude::*; | |||
use gtk; | |||
use prefs::*; | |||
use std::rc::Rc; | |||
use support_alsa::*; | |||
pub struct PrefsDialog { | |||
_cant_construct: (), | |||
prefs_dialog: gtk::Dialog, | |||
notebook: gtk::Notebook, | |||
/* DevicePrefs */ | |||
card_combo: gtk::ComboBoxText, | |||
chan_combo: gtk::ComboBoxText, | |||
/* ViewPrefs */ | |||
vol_meter_draw_check: gtk::CheckButton, | |||
vol_meter_pos_spin: gtk::SpinButton, | |||
vol_meter_color_button: gtk::ColorButton, | |||
system_theme: gtk::CheckButton, | |||
/* BehaviorPrefs */ | |||
unmute_on_vol_change: gtk::CheckButton, | |||
vol_control_entry: gtk::Entry, | |||
scroll_step_spin: gtk::SpinButton, | |||
fine_scroll_step_spin: gtk::SpinButton, | |||
middle_click_combo: gtk::ComboBoxText, | |||
custom_entry: gtk::Entry, | |||
/* NotifyPrefs */ | |||
#[cfg(feature = "notify")] | |||
noti_enable_check: gtk::CheckButton, | |||
#[cfg(feature = "notify")] | |||
noti_timeout_spin: gtk::SpinButton, | |||
// pub noti_hotkey_check: gtk::CheckButton, | |||
#[cfg(feature = "notify")] | |||
noti_mouse_check: gtk::CheckButton, | |||
#[cfg(feature = "notify")] | |||
noti_popup_check: gtk::CheckButton, | |||
#[cfg(feature = "notify")] | |||
noti_ext_check: gtk::CheckButton, | |||
} | |||
impl PrefsDialog { | |||
fn new() -> PrefsDialog { | |||
let builder = | |||
gtk::Builder::new_from_string(include_str!(concat!(env!("CARGO_MANIFEST_DIR"), | |||
"/data/ui/prefs-dialog.glade"))); | |||
let prefs_dialog = PrefsDialog { | |||
_cant_construct: (), | |||
prefs_dialog: builder.get_object("prefs_dialog").unwrap(), | |||
notebook: builder.get_object("notebook").unwrap(), | |||
/* DevicePrefs */ | |||
card_combo: builder.get_object("card_combo").unwrap(), | |||
chan_combo: builder.get_object("chan_combo").unwrap(), | |||
/* ViewPrefs */ | |||
vol_meter_draw_check: builder.get_object("vol_meter_draw_check") | |||
.unwrap(), | |||
vol_meter_pos_spin: builder.get_object("vol_meter_pos_spin") | |||
.unwrap(), | |||
vol_meter_color_button: builder.get_object("vol_meter_color_button") | |||
.unwrap(), | |||
system_theme: builder.get_object("system_theme").unwrap(), | |||
/* BehaviorPrefs */ | |||
unmute_on_vol_change: builder.get_object("unmute_on_vol_change") | |||
.unwrap(), | |||
vol_control_entry: builder.get_object("vol_control_entry").unwrap(), | |||
scroll_step_spin: builder.get_object("scroll_step_spin").unwrap(), | |||
fine_scroll_step_spin: builder.get_object("fine_scroll_step_spin") | |||
.unwrap(), | |||
middle_click_combo: builder.get_object("middle_click_combo") | |||
.unwrap(), | |||
custom_entry: builder.get_object("custom_entry").unwrap(), | |||
/* NotifyPrefs */ | |||
#[cfg(feature = "notify")] | |||
noti_enable_check: builder.get_object("noti_enable_check").unwrap(), | |||
#[cfg(feature = "notify")] | |||
noti_timeout_spin: builder.get_object("noti_timeout_spin").unwrap(), | |||
// noti_hotkey_check: builder.get_object("noti_hotkey_check").unwrap(), | |||
#[cfg(feature = "notify")] | |||
noti_mouse_check: builder.get_object("noti_mouse_check").unwrap(), | |||
#[cfg(feature = "notify")] | |||
noti_popup_check: builder.get_object("noti_popup_check").unwrap(), | |||
#[cfg(feature = "notify")] | |||
noti_ext_check: builder.get_object("noti_ext_check").unwrap(), | |||
}; | |||
#[cfg(feature = "notify")] | |||
let notify_tab: gtk::Box = builder.get_object("noti_vbox_enabled") | |||
.unwrap(); | |||
#[cfg(not(feature = "notify"))] | |||
let notify_tab: gtk::Box = builder.get_object("noti_vbox_disabled") | |||
.unwrap(); | |||
prefs_dialog.notebook.append_page(¬ify_tab, | |||
Some(>k::Label::new(Some("Notifications")))); | |||
return prefs_dialog; | |||
} | |||
fn from_prefs(&self, prefs: &Prefs) { | |||
/* DevicePrefs */ | |||
/* filled on show signal with audio info */ | |||
self.card_combo.remove_all(); | |||
self.chan_combo.remove_all(); | |||
/* ViewPrefs */ | |||
self.vol_meter_draw_check.set_active(prefs.view_prefs.draw_vol_meter); | |||
self.vol_meter_pos_spin.set_value(prefs.view_prefs.vol_meter_offset as | |||
f64); | |||
let rgba = gdk::RGBA { | |||
red: prefs.view_prefs.vol_meter_color.red, | |||
green: prefs.view_prefs.vol_meter_color.green, | |||
blue: prefs.view_prefs.vol_meter_color.blue, | |||
alpha: 1.0, | |||
}; | |||
self.vol_meter_color_button.set_rgba(&rgba); | |||
self.system_theme.set_active(prefs.view_prefs.system_theme); | |||
/* BehaviorPrefs */ | |||
self.unmute_on_vol_change | |||
.set_active(prefs.behavior_prefs.unmute_on_vol_change); | |||
self.vol_control_entry.set_text(prefs.behavior_prefs | |||
.vol_control_cmd | |||
.as_ref() | |||
.unwrap_or(&String::from("")) | |||
.as_str()); | |||
self.scroll_step_spin.set_value(prefs.behavior_prefs.vol_scroll_step); | |||
self.fine_scroll_step_spin | |||
.set_value(prefs.behavior_prefs.vol_fine_scroll_step); | |||
// TODO: make sure these values always match, must be a better way | |||
// also check to_prefs() | |||
self.middle_click_combo.append_text("Toggle Mute"); | |||
self.middle_click_combo.append_text("Show Preferences"); | |||
self.middle_click_combo.append_text("Volume Control"); | |||
self.middle_click_combo.append_text("Custom Command (set below)"); | |||
self.middle_click_combo.set_active(prefs.behavior_prefs | |||
.middle_click_action | |||
.into()); | |||
self.custom_entry.set_text(prefs.behavior_prefs | |||
.custom_command | |||
.as_ref() | |||
.unwrap_or(&String::from("")) | |||
.as_str()); | |||
/* NotifyPrefs */ | |||
#[cfg(feature = "notify")] | |||
{ | |||
self.noti_enable_check | |||
.set_active(prefs.notify_prefs.enable_notifications); | |||
self.noti_timeout_spin | |||
.set_value(prefs.notify_prefs.notifcation_timeout as f64); | |||
self.noti_mouse_check | |||
.set_active(prefs.notify_prefs.notify_mouse_scroll); | |||
self.noti_popup_check.set_active(prefs.notify_prefs.notify_popup); | |||
self.noti_ext_check.set_active(prefs.notify_prefs.notify_external); | |||
} | |||
} | |||
fn to_prefs(&self) -> Prefs { | |||
let card = self.card_combo.get_active_text(); | |||
let channel = self.chan_combo.get_active_text(); | |||
if card.is_none() || channel.is_none() { | |||
return Prefs::default(); | |||
} | |||
let device_prefs = DevicePrefs { | |||
card: self.card_combo.get_active_text().unwrap(), | |||
channel: self.chan_combo.get_active_text().unwrap(), | |||
}; | |||
let vol_meter_color = VolColor { | |||
red: (self.vol_meter_color_button.get_rgba().red), | |||
green: (self.vol_meter_color_button.get_rgba().green), | |||
blue: (self.vol_meter_color_button.get_rgba().blue), | |||
}; | |||
let view_prefs = ViewPrefs { | |||
draw_vol_meter: self.vol_meter_draw_check.get_active(), | |||
vol_meter_offset: self.vol_meter_pos_spin.get_value_as_int(), | |||
system_theme: self.system_theme.get_active(), | |||
vol_meter_color, | |||
}; | |||
let vol_control_cmd = | |||
self.vol_control_entry.get_text().and_then(|x| if x.is_empty() { | |||
None | |||
} else { | |||
Some(x) | |||
}); | |||
let custom_command = | |||
self.custom_entry.get_text().and_then(|x| if x.is_empty() { | |||
None | |||
} else { | |||
Some(x) | |||
}); | |||
let behavior_prefs = BehaviorPrefs { | |||
unmute_on_vol_change: self.unmute_on_vol_change.get_active(), | |||
vol_control_cmd, | |||
vol_scroll_step: self.scroll_step_spin.get_value(), | |||
vol_fine_scroll_step: self.fine_scroll_step_spin.get_value(), | |||
middle_click_action: self.middle_click_combo.get_active().into(), | |||
custom_command, | |||
}; | |||
#[cfg(feature = "notify")] | |||
let notify_prefs = NotifyPrefs { | |||
enable_notifications: self.noti_enable_check.get_active(), | |||
notifcation_timeout: self.noti_timeout_spin.get_value_as_int() as | |||
i64, | |||
notify_mouse_scroll: self.noti_mouse_check.get_active(), | |||
notify_popup: self.noti_popup_check.get_active(), | |||
notify_external: self.noti_ext_check.get_active(), | |||
}; | |||
return Prefs { | |||
device_prefs, | |||
view_prefs, | |||
behavior_prefs, | |||
#[cfg(feature = "notify")] | |||
notify_prefs, | |||
}; | |||
} | |||
} | |||
pub fn show_prefs_dialog(appstate: &Rc<AppS>) { | |||
if appstate.gui | |||
.prefs_dialog | |||
.borrow() | |||
.is_some() { | |||
return; | |||
} | |||
*appstate.gui.prefs_dialog.borrow_mut() = Some(PrefsDialog::new()); | |||
init_prefs_dialog(&appstate); | |||
{ | |||
let m_pd = appstate.gui.prefs_dialog.borrow(); | |||
let prefs_dialog = &m_pd.as_ref().unwrap(); | |||
let prefs_dialog_w = &prefs_dialog.prefs_dialog; | |||
prefs_dialog.from_prefs(&appstate.prefs.borrow()); | |||
prefs_dialog_w.set_transient_for(&appstate.gui.popup_menu.menu_window); | |||
prefs_dialog_w.present(); | |||
} | |||
} | |||
pub fn init_prefs_callback(appstate: Rc<AppS>) { | |||
let apps = appstate.clone(); | |||
appstate.audio.connect_handler(Box::new(move |s, u| { | |||
/* skip if prefs window is not present */ | |||
if apps.gui | |||
.prefs_dialog | |||
.borrow() | |||
.is_none() { | |||
return; | |||
} | |||
match (s, u) { | |||
(AudioSignal::CardInitialized, _) => (), | |||
(AudioSignal::CardCleanedUp, _) => { | |||
fill_card_combo(&apps); | |||
fill_chan_combo(&apps, None); | |||
} | |||
_ => (), | |||
} | |||
})); | |||
} | |||
fn init_prefs_dialog(appstate: &Rc<AppS>) { | |||
/* prefs_dialog.connect_show */ | |||
{ | |||
let apps = appstate.clone(); | |||
let m_pd = appstate.gui.prefs_dialog.borrow(); | |||
let pd = m_pd.as_ref().unwrap(); | |||
pd.prefs_dialog.connect_show(move |_| { | |||
fill_card_combo(&apps); | |||
fill_chan_combo(&apps, None); | |||
}); | |||
} | |||
/* card_combo.connect_changed */ | |||
{ | |||
let apps = appstate.clone(); | |||
let m_cc = appstate.gui.prefs_dialog.borrow(); | |||
let card_combo = &m_cc.as_ref().unwrap().card_combo; | |||
card_combo.connect_changed(move |_| { | |||
let m_cc = apps.gui.prefs_dialog.borrow(); | |||
let card_combo = &m_cc.as_ref().unwrap().card_combo; | |||
let card_name = card_combo.get_active_text().unwrap(); | |||
fill_chan_combo(&apps, Some(card_name)); | |||
return; | |||
}); | |||
} | |||
/* prefs_dialog.connect_response */ | |||
{ | |||
let apps = appstate.clone(); | |||
let m_pd = appstate.gui.prefs_dialog.borrow(); | |||
let pd = m_pd.as_ref().unwrap(); | |||
pd.prefs_dialog.connect_response(move |_, response_id| { | |||
if response_id == ResponseType::Ok.into() || | |||
response_id == ResponseType::Apply.into() { | |||
let mut prefs = apps.prefs.borrow_mut(); | |||
let prefs_dialog = apps.gui.prefs_dialog.borrow(); | |||
*prefs = prefs_dialog.as_ref().unwrap().to_prefs(); | |||
} | |||
if response_id != ResponseType::Apply.into() { | |||
let mut prefs_dialog = apps.gui.prefs_dialog.borrow_mut(); | |||
prefs_dialog.as_ref() | |||
.unwrap() | |||
.prefs_dialog | |||
.destroy(); | |||
*prefs_dialog = None; | |||
} | |||
if response_id == ResponseType::Ok.into() || | |||
response_id == ResponseType::Apply.into() { | |||
// TODO: update hotkeys | |||
try_w!(apps.update_notify()); | |||
try_w!(apps.update_tray_icon()); | |||
try_w!(apps.update_popup_window()); | |||
try_w!(apps.update_audio(AudioUser::PrefsWindow)); | |||
try_w!(apps.update_config()); | |||
} | |||
}); | |||
} | |||
} | |||
fn fill_card_combo(appstate: &AppS) { | |||
let m_cc = appstate.gui.prefs_dialog.borrow(); | |||
let card_combo = &m_cc.as_ref().unwrap().card_combo; | |||
card_combo.remove_all(); | |||
let acard = appstate.audio.acard.borrow(); | |||
/* set card combo */ | |||
let cur_card_name = try_w!(acard.card_name(), | |||
"Can't get current card name!"); | |||
let available_card_names = get_playable_alsa_card_names(); | |||
/* set_active_id doesn't work, so save the index */ | |||
let mut c_index: i32 = -1; | |||
for i in 0..available_card_names.len() { | |||
let name = available_card_names.get(i).unwrap(); | |||
if *name == cur_card_name { | |||
c_index = i as i32; | |||
} | |||
card_combo.append_text(&name); | |||
} | |||
// TODO, block signal? | |||
card_combo.set_active(c_index); | |||
} | |||
fn fill_chan_combo(appstate: &AppS, cardname: Option<String>) { | |||
let m_cc = appstate.gui.prefs_dialog.borrow(); | |||
let chan_combo = &m_cc.as_ref().unwrap().chan_combo; | |||
chan_combo.remove_all(); | |||
let cur_acard = appstate.audio.acard.borrow(); | |||
let card = match cardname { | |||
Some(name) => try_w!(get_alsa_card_by_name(name).from_err()), | |||
None => cur_acard.as_ref().card, | |||
}; | |||
/* set chan combo */ | |||
let cur_chan_name = try_w!(cur_acard.chan_name()); | |||
let mixer = try_w!(get_mixer(&card)); | |||
let available_chan_names = get_playable_selem_names(&mixer); | |||
/* set_active_id doesn't work, so save the index */ | |||
let mut c_index: i32 = -1; | |||
for i in 0..available_chan_names.len() { | |||
let name = available_chan_names.get(i).unwrap(); | |||
if *name == cur_chan_name { | |||
c_index = i as i32; | |||
} | |||
chan_combo.append_text(&name); | |||
} | |||
/* TODO, block signal?`*/ | |||
chan_combo.set_active(c_index); | |||
} |
@@ -0,0 +1,489 @@ | |||
use app_state::*; | |||
use audio::*; | |||
use errors::*; | |||
use gdk; | |||
use gdk_pixbuf; | |||
use gdk_pixbuf_sys; | |||
use gtk::prelude::*; | |||
use gtk; | |||
use prefs::{Prefs, MiddleClickAction}; | |||
use std::cell::Cell; | |||
use std::cell::RefCell; | |||
use std::rc::Rc; | |||
use support_cmd::*; | |||
use support_ui::*; | |||
use ui_prefs_dialog::show_prefs_dialog; | |||
const ICON_MIN_SIZE: i32 = 16; | |||
pub struct TrayIcon { | |||
_cant_construct: (), | |||
pub volmeter: RefCell<Option<VolMeter>>, | |||
pub audio_pix: RefCell<AudioPix>, | |||
pub status_icon: gtk::StatusIcon, | |||
pub icon_size: Cell<i32>, | |||
} | |||
impl TrayIcon { | |||
pub fn new(prefs: &Prefs) -> Result<TrayIcon> { | |||
let draw_vol_meter = prefs.view_prefs.draw_vol_meter; | |||
let volmeter = { | |||
if draw_vol_meter { | |||
RefCell::new(Some(VolMeter::new(prefs))) | |||
} else { | |||
RefCell::new(None) | |||
} | |||
}; | |||
let audio_pix = AudioPix::new(ICON_MIN_SIZE, prefs)?; | |||
let status_icon = gtk::StatusIcon::new(); | |||
return Ok(TrayIcon { | |||
_cant_construct: (), | |||
volmeter, | |||
audio_pix: RefCell::new(audio_pix), | |||
status_icon, | |||
icon_size: Cell::new(ICON_MIN_SIZE), | |||
}); | |||
} | |||
fn update_vol_meter(&self, cur_vol: f64, vol_level: VolLevel) -> Result<()> { | |||
let audio_pix = self.audio_pix.borrow(); | |||
let pixbuf = audio_pix.select_pix(vol_level); | |||
let vol_borrow = self.volmeter.borrow(); | |||
let volmeter = &vol_borrow.as_ref(); | |||
match volmeter { | |||
&Some(v) => { | |||
let vol_pix = v.meter_draw(cur_vol as i64, &pixbuf)?; | |||
self.status_icon.set_from_pixbuf(Some(&vol_pix)); | |||
} | |||
&None => self.status_icon.set_from_pixbuf(Some(pixbuf)), | |||
}; | |||
return Ok(()); | |||
} | |||
fn update_tooltip(&self, audio: &Audio) { | |||
let cardname = audio.acard | |||
.borrow() | |||
.card_name() | |||
.unwrap_or(String::from("Unknown card")); | |||
let channame = audio.acard | |||
.borrow() | |||
.chan_name() | |||
.unwrap_or(String::from("unknown channel")); | |||
let vol = audio.vol() | |||
.map(|s| format!("{}", s.round())) | |||
.unwrap_or(String::from("unknown volume")); | |||
let mute_info = { | |||
if !audio.has_mute() { | |||
"\nNo mute switch" | |||
} else if audio.get_mute().unwrap_or(false) { | |||
"\nMuted" | |||
} else { | |||
"" | |||
} | |||
}; | |||
self.status_icon.set_tooltip_text(format!("{} ({})\nVolume: {}{}", | |||
cardname, | |||
channame, | |||
vol, | |||
mute_info) | |||
.as_str()); | |||
} | |||
pub fn update_all(&self, | |||
prefs: &Prefs, | |||
audio: &Audio, | |||
m_size: Option<i32>) | |||
-> Result<()> { | |||
match m_size { | |||
Some(s) => { | |||
if s < ICON_MIN_SIZE { | |||
self.icon_size.set(ICON_MIN_SIZE); | |||
} else { | |||
self.icon_size.set(s); | |||
} | |||
} | |||
None => (), | |||
} | |||
let audio_pix = AudioPix::new(self.icon_size.get(), &prefs)?; | |||
*self.audio_pix.borrow_mut() = audio_pix; | |||
let draw_vol_meter = prefs.view_prefs.draw_vol_meter; | |||
if draw_vol_meter { | |||
let volmeter = VolMeter::new(&prefs); | |||
*self.volmeter.borrow_mut() = Some(volmeter); | |||
} | |||
self.update_tooltip(&audio); | |||
return self.update_vol_meter(audio.vol()?, audio.vol_level()); | |||
} | |||
} | |||
pub struct VolMeter { | |||
red: u8, | |||
green: u8, | |||
blue: u8, | |||
x_offset_pct: i64, | |||
y_offset_pct: i64, | |||
/* dynamic */ | |||
width: Cell<i64>, | |||
row: RefCell<Vec<u8>>, | |||
} | |||
impl VolMeter { | |||
fn new(prefs: &Prefs) -> VolMeter { | |||
return VolMeter { | |||
red: (prefs.view_prefs.vol_meter_color.red * 255.0) as u8, | |||
green: (prefs.view_prefs.vol_meter_color.green * 255.0) as | |||
u8, | |||
blue: (prefs.view_prefs.vol_meter_color.blue * 255.0) as u8, | |||
x_offset_pct: prefs.view_prefs.vol_meter_offset as i64, | |||
y_offset_pct: 10, | |||
/* dynamic */ | |||
width: Cell::new(0), | |||
row: RefCell::new(vec![]), | |||
}; | |||
} | |||
// TODO: cache input pixbuf? | |||
fn meter_draw(&self, | |||
volume: i64, | |||
pixbuf: &gdk_pixbuf::Pixbuf) | |||
-> Result<gdk_pixbuf::Pixbuf> { | |||
ensure!(pixbuf.get_colorspace() == gdk_pixbuf_sys::GDK_COLORSPACE_RGB, | |||
"Invalid colorspace in pixbuf"); | |||
ensure!(pixbuf.get_bits_per_sample() == 8, | |||
"Invalid bits per sample in pixbuf"); | |||
ensure!(pixbuf.get_has_alpha(), "No alpha channel in pixbuf"); | |||
ensure!(pixbuf.get_n_channels() == 4, | |||
"Invalid number of channels in pixbuf"); | |||
let i_width = pixbuf.get_width() as i64; | |||
let i_height = pixbuf.get_height() as i64; | |||
let new_pixbuf = copy_pixbuf(pixbuf); | |||
let vm_width = i_width / 6; | |||
let x = (self.x_offset_pct as f64 * | |||
((i_width - vm_width) as f64 / 100.0)) as i64; | |||
ensure!(x >= 0 && (x + vm_width) <= i_width, | |||
"x coordinate invalid: {}", | |||
x); | |||
let y = (self.y_offset_pct as f64 * (i_height as f64 / 100.0)) as i64; | |||
let vm_height = | |||
((i_height - (y * 2)) as f64 * (volume as f64 / 100.0)) as i64; | |||
ensure!(y >= 0 && (y + vm_height) <= i_height, | |||
"y coordinate invalid: {}", | |||
y); | |||
/* Let's check if the icon width changed, in which case we | |||
* must reinit our internal row of pixels. | |||
*/ | |||
if vm_width != self.width.get() { | |||
self.width.set(vm_width); | |||
let mut row = self.row.borrow_mut(); | |||
*row = vec![]; | |||
} | |||
if self.row.borrow().len() == 0 { | |||
debug!("Allocating vol meter row (width {})", vm_width); | |||
let mut row = self.row.borrow_mut(); | |||
*row = [self.red, self.green, self.blue, 255] | |||
.iter() | |||
.cloned() | |||
.cycle() | |||
.take((vm_width * 4) as usize) | |||
.collect(); | |||
} | |||
/* Draw the volume meter. | |||
* Rows in the image are stored top to bottom. | |||
*/ | |||
{ | |||
let y = i_height - y; | |||
let rowstride: i64 = new_pixbuf.get_rowstride() as i64; | |||
let pixels: &mut [u8] = unsafe { new_pixbuf.get_pixels() }; | |||
for i in 0..(vm_height - 1) { | |||
let row_offset: i64 = y - i; | |||
let col_offset: i64 = x * 4; | |||
let p_index = ((row_offset * rowstride) + col_offset) as usize; | |||
let row = self.row.borrow(); | |||
pixels[p_index..p_index + row.len()] | |||
.copy_from_slice(row.as_ref()); | |||
} | |||
} | |||
return Ok(new_pixbuf); | |||
} | |||
} | |||
// TODO: connect on icon theme change | |||
#[derive(Clone, Debug)] | |||
pub struct AudioPix { | |||
muted: gdk_pixbuf::Pixbuf, | |||
low: gdk_pixbuf::Pixbuf, | |||
medium: gdk_pixbuf::Pixbuf, | |||
high: gdk_pixbuf::Pixbuf, | |||
off: gdk_pixbuf::Pixbuf, | |||
} | |||
impl AudioPix { | |||
fn new(size: i32, prefs: &Prefs) -> Result<AudioPix> { | |||
let system_theme = prefs.view_prefs.system_theme; | |||
let pix = { | |||
if system_theme { | |||
let theme: gtk::IconTheme = | |||
gtk::IconTheme::get_default().ok_or( | |||
"Couldn't get default icon theme", | |||
)?; | |||
AudioPix { | |||
muted: pixbuf_new_from_theme( | |||
"audio-volume-muted", | |||
size, | |||
&theme, | |||
)?, | |||
low: pixbuf_new_from_theme( | |||
"audio-volume-low", | |||
size, | |||
&theme, | |||
)?, | |||
medium: pixbuf_new_from_theme( | |||
"audio-volume-medium", | |||
size, | |||
&theme, | |||
)?, | |||
high: pixbuf_new_from_theme( | |||
"audio-volume-high", | |||
size, | |||
&theme, | |||
)?, | |||
/* 'audio-volume-off' is not available in every icon set. | |||
* Check freedesktop standard for more info: | |||
* http://standards.freedesktop.org/icon-naming-spec/ | |||
* icon-naming-spec-latest.html | |||
*/ | |||
off: pixbuf_new_from_theme( | |||
"audio-volume-off", | |||
size, | |||
&theme, | |||
).or(pixbuf_new_from_theme( | |||
"audio-volume-low", | |||
size, | |||
&theme, | |||
))?, | |||
} | |||
} else { | |||
AudioPix { | |||
muted: pixbuf_new_from_xpm!(pnmixer_muted), | |||
low: pixbuf_new_from_xpm!(pnmixer_low), | |||
medium: pixbuf_new_from_xpm!(pnmixer_medium), | |||
high: pixbuf_new_from_xpm!(pnmixer_high), | |||
off: pixbuf_new_from_xpm!(pnmixer_off), | |||
} | |||
} | |||
}; | |||
return Ok(pix); | |||
} | |||
fn select_pix(&self, vol_level: VolLevel) -> &gdk_pixbuf::Pixbuf { | |||
match vol_level { | |||
VolLevel::Muted => &self.muted, | |||
VolLevel::Low => &self.low, | |||
VolLevel::Medium => &self.medium, | |||
VolLevel::High => &self.high, | |||
VolLevel::Off => &self.off, | |||
} | |||
} | |||
} | |||
pub fn init_tray_icon(appstate: Rc<AppS>) { | |||
let audio = &appstate.audio; | |||
let tray_icon = &appstate.gui.tray_icon; | |||
try_e!(tray_icon.update_all(&appstate.prefs.borrow_mut(), &audio, None)); | |||
tray_icon.status_icon.set_visible(true); | |||
/* connect audio handler */ | |||
{ | |||
let apps = appstate.clone(); | |||
appstate.audio.connect_handler(Box::new(move |s, u| match (s, u) { | |||
(_, _) => { | |||
apps.gui.tray_icon.update_tooltip(&apps.audio); | |||
try_w!(apps.gui.tray_icon.update_vol_meter(try_w!(apps.audio.vol()), | |||
apps.audio.vol_level())); | |||
} | |||
})); | |||
} | |||
/* tray_icon.connect_size_changed */ | |||
{ | |||
let apps = appstate.clone(); | |||
tray_icon.status_icon.connect_size_changed(move |_, size| { | |||
try_wr!(apps.gui.tray_icon.update_all(&apps.prefs.borrow_mut(), | |||
&apps.audio, | |||
Some(size)), | |||
false); | |||
return false; | |||
}); | |||
} | |||
/* tray_icon.connect_activate */ | |||
{ | |||
let apps = appstate.clone(); | |||
tray_icon.status_icon.connect_activate(move |_| { | |||
on_tray_icon_activate(&apps) | |||
}); | |||
} | |||
/* tray_icon.connect_scroll_event */ | |||
{ | |||
let apps = appstate.clone(); | |||
tray_icon.status_icon.connect_scroll_event( | |||
move |_, e| on_tray_icon_scroll_event(&apps, &e), | |||
); | |||
} | |||
/* tray_icon.connect_popup_menu */ | |||
{ | |||
let apps = appstate.clone(); | |||
tray_icon.status_icon.connect_popup_menu(move |_, _, _| { | |||
on_tray_icon_popup_menu(&apps) | |||
}); | |||
} | |||
/* tray_icon.connect_button_release_event */ | |||
{ | |||
let apps = appstate.clone(); | |||
tray_icon.status_icon.connect_button_release_event(move |_, eb| { | |||
on_tray_button_release_event(&apps, eb) | |||
}); | |||
} | |||
/* default_theme.connect_changed */ | |||
{ | |||
let apps = appstate.clone(); | |||
let default_theme = try_w!(gtk::IconTheme::get_default().ok_or( | |||
"Couldn't get default icon theme", | |||
)); | |||
default_theme.connect_changed(move |_| { | |||
let tray_icon = &apps.gui.tray_icon; | |||
let audio = &apps.audio; | |||
try_e!(tray_icon.update_all(&apps.prefs.borrow_mut(), &audio, None)); | |||
}); | |||
} | |||
} | |||
fn on_tray_icon_activate(appstate: &AppS) { | |||
let popup_window = &appstate.gui.popup_window.popup_window; | |||
if popup_window.get_visible() { | |||
popup_window.hide(); | |||
} else { | |||
popup_window.show_now(); | |||
} | |||
} | |||
fn on_tray_icon_popup_menu(appstate: &AppS) { | |||
let popup_window = &appstate.gui.popup_window.popup_window; | |||
let popup_menu = &appstate.gui.popup_menu.menu; | |||
popup_window.hide(); | |||
popup_menu.popup_at_pointer(None); | |||
} | |||
fn on_tray_icon_scroll_event(appstate: &AppS, | |||
event: &gdk::EventScroll) | |||
-> bool { | |||
let scroll_dir: gdk::ScrollDirection = event.get_direction(); | |||
match scroll_dir { | |||
gdk::ScrollDirection::Up => { | |||
try_wr!(appstate.audio.increase_vol(AudioUser::TrayIcon, | |||
appstate.prefs | |||
.borrow() | |||
.behavior_prefs | |||
.unmute_on_vol_change), | |||
false); | |||
} | |||
gdk::ScrollDirection::Down => { | |||
try_wr!(appstate.audio.decrease_vol(AudioUser::TrayIcon, | |||
appstate.prefs | |||
.borrow() | |||
.behavior_prefs | |||
.unmute_on_vol_change), | |||
false); | |||
} | |||
_ => (), | |||
} | |||
return false; | |||
} | |||
fn on_tray_button_release_event(appstate: &Rc<AppS>, | |||
event_button: &gdk::EventButton) | |||
-> bool { | |||
let button = event_button.get_button(); | |||
if button != 2 { | |||
// not middle-click | |||
return false; | |||
} | |||
let audio = &appstate.audio; | |||
let prefs = &appstate.prefs.borrow(); | |||
let middle_click_action = &prefs.behavior_prefs.middle_click_action; | |||
let custom_command = &prefs.behavior_prefs.custom_command; | |||
match middle_click_action { | |||
&MiddleClickAction::ToggleMute => { | |||
if audio.has_mute() { | |||
try_wr!(audio.toggle_mute(AudioUser::Popup), false); | |||
} | |||
} | |||
// TODO | |||
&MiddleClickAction::ShowPreferences => show_prefs_dialog(&appstate), | |||
&MiddleClickAction::VolumeControl => { | |||
try_wr!(execute_vol_control_command(&appstate.prefs.borrow()), | |||
false); | |||
} | |||
&MiddleClickAction::CustomCommand => { | |||
match custom_command { | |||
&Some(ref cmd) => try_wr!(execute_command(cmd.as_str()), false), | |||
&None => warn!("No custom command found"), | |||
} | |||
} | |||
} | |||
return false; | |||
} |
@@ -0,0 +1,32 @@ | |||
#include "../data/pixmaps/pnmixer-about.xpm" | |||
#include "../data/pixmaps/pnmixer-high.xpm" | |||
#include "../data/pixmaps/pnmixer-low.xpm" | |||
#include "../data/pixmaps/pnmixer-medium.xpm" | |||
#include "../data/pixmaps/pnmixer-muted.xpm" | |||
#include "../data/pixmaps/pnmixer-off.xpm" | |||
char** pnmixer_about() { | |||
return pnmixer_about_xpm; | |||
} | |||
char** pnmixer_high() { | |||
return pnmixer_high_xpm; | |||
} | |||
char** pnmixer_low() { | |||
return pnmixer_low_xpm; | |||
} | |||
char** pnmixer_medium() { | |||
return pnmixer_medium_xpm; | |||
} | |||
char** pnmixer_muted() { | |||
return pnmixer_muted_xpm; | |||
} | |||
char** pnmixer_off() { | |||
return pnmixer_off_xpm; | |||
} | |||