commit 215562fb73b7b8756b713c9f803b23d1103fbe28 Author: Gustavo Adolfo Mesa Roldán Date: Fri Feb 14 00:07:55 2020 +0100 Init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5b0f95a --- /dev/null +++ b/.gitignore @@ -0,0 +1,42 @@ +*.*~ +Makefile +Makefile.in +config.log +config.status +gnome15-*.tar.gz +py-compile +compile +missing +install-sh +autom4te.cache +configure +GNOME_G15Applet.server +g15-config.desktop +g15-macros.desktop +g15-indicator.desktop +g15-systemtray.desktop +gnome15.desktop +ltoptions.m4 +ltsugar.m4 +ltversion.m4 +lt~obsolete.m4 +aclocal.m4 +libtool.m4 +*.o +*.la +*.lo +.deps +*.pyc +*.pyo +ltmain.sh +libtool +depcomp +config.sub +config.guess +src/pylibg19/dist +src/pylibg19/MANIFEST +src/pylibg19/build +.libs +en_GB +data/udev/*.rules +src/gnome15/g15globals.py diff --git a/AUTHORS b/AUTHORS new file mode 100644 index 0000000..1df4ffc --- /dev/null +++ b/AUTHORS @@ -0,0 +1,47 @@ +Authors: +* Brett Smith +* Nuno Araujo +* NoXPhasma +* Huskynarr + +Contains work based on "Logitech-G19-Linux-Daemon" [1], written by +"MultiCoreNop" [2]. + +Contains work based on Impulse [3], written by Ian Halpern [4]. + +Contains code taken from Pitivi [5], authored by Edward Hervey [6]. + +Contains objgraph [7], written by Marius Gedminas [8], released under +the MIT license. + +Contains code from tailer [9], written by Mike Thornton, released under +the MIT license. + +Contains code from Things [10], written by Donn. C. Ingle [11]. + +Contains code from python-teamspeak3 [12], written by Adam +Coddington, released under the MIT license. + +Contains code from pywapi [13], written by Eugene Kaznacheev [14], +released under the MIT license. + +Some of the graphical work was made by Andrea Calabrò. + +Images src/plugins/pommodoro/Machovka_tomato.png was taken from [15] +and was released under the public domain. + +1. http://github.com/MultiCoreNop/Logitech-G19-Linux-Daemon +2. http://github.com/MultiCoreNop +3. http://impulse.ian-halpern.com/ +4. +5. http://www.pitivi.org +6. Edward Hervey +7. http://mg.pov.lt/objgraph/ +8. Marius Gedminas +9. http://github.com/six8/pytailer +10. https://savannah.nongnu.org/projects/things/ +11. Donn.C.Ingle +12. https://bitbucket.org/latestrevision/python-teamspeak3/ +13. https://code.google.com/p/python-weather-api/ +14. Eugene Kaznacheev +15. http://openclipart.org/detail/2542/tomato-by-machovka diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..8dfeb52 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,9 @@ +## 1.0.0 (2017-06-18) + +Bugfixes: + + - `function --params` this is only for an example + +Features: + + - Add an not exist function (#commitid, @huskynarr) diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..0313a33 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,46 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at contact@gnome15.org. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] + +[homepage]: http://contributor-covenant.org +[version]: http://contributor-covenant.org/version/1/4/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..9f1ab7b --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,92 @@ +# Contributing + +When contributing to this repository, please first discuss the change you wish to make via issue, +email, or any other method with the owners of this repository before making a change. + +Please note we have a code of conduct, please follow it in all your interactions with the project. + +## Pull Request Process + +1. Ensure any install or build dependencies are removed before the end of the layer when doing a + build. +2. Update the README.md with details of changes to the interface, this includes new environment + variables, exposed ports, useful file locations and container parameters. +3. Increase the version numbers in any examples files and the README.md to the new version that this + Pull Request would represent. The versioning scheme we use is [SemVer](http://semver.org/). +4. You may merge the Pull Request in once you have the sign-off of two other developers, or if you + do not have permission to do that, you may request the second reviewer to merge it for you. + +## Code of Conduct + +### Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, gender identity and expression, level of experience, +nationality, personal appearance, race, religion, or sexual identity and +orientation. + +### Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or +advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic + address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +### Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +### Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +### Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting the project team at [INSERT EMAIL ADDRESS]. All +complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate to the circumstances. The project team is +obligated to maintain confidentiality with regard to the reporter of an incident. +Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +### Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, +available at [http://contributor-covenant.org/version/1/4][version] + +[homepage]: http://contributor-covenant.org +[version]: http://contributor-covenant.org/version/1/4/ diff --git a/COPYING b/COPYING new file mode 100644 index 0000000..94a9ed0 --- /dev/null +++ b/COPYING @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + 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. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 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 . + +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: + + Copyright (C) + 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 +. + + 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 +. diff --git a/ChangeLog b/ChangeLog new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/ChangeLog @@ -0,0 +1 @@ + diff --git a/INSTALL b/INSTALL new file mode 100644 index 0000000..6e90e07 --- /dev/null +++ b/INSTALL @@ -0,0 +1,370 @@ +Installation Instructions +************************* + +Copyright (C) 1994-1996, 1999-2002, 2004-2012 Free Software Foundation, +Inc. + + Copying and distribution of this file, with or without modification, +are permitted in any medium without royalty provided the copyright +notice and this notice are preserved. This file is offered as-is, +without warranty of any kind. + +Basic Installation +================== + + Briefly, the shell commands `./configure; make; make install' should +configure, build, and install this package. The following +more-detailed instructions are generic; see the `README' file for +instructions specific to this package. Some packages provide this +`INSTALL' file but do not implement all of the features documented +below. The lack of an optional feature in a given package is not +necessarily a bug. More recommendations for GNU packages can be found +in *note Makefile Conventions: (standards)Makefile Conventions. + + The `configure' shell script attempts to guess correct values for +various system-dependent variables used during compilation. It uses +those values to create a `Makefile' in each directory of the package. +It may also create one or more `.h' files containing system-dependent +definitions. Finally, it creates a shell script `config.status' that +you can run in the future to recreate the current configuration, and a +file `config.log' containing compiler output (useful mainly for +debugging `configure'). + + It can also use an optional file (typically called `config.cache' +and enabled with `--cache-file=config.cache' or simply `-C') that saves +the results of its tests to speed up reconfiguring. Caching is +disabled by default to prevent problems with accidental use of stale +cache files. + + If you need to do unusual things to compile the package, please try +to figure out how `configure' could check whether to do them, and mail +diffs or instructions to the address given in the `README' so they can +be considered for the next release. If you are using the cache, and at +some point `config.cache' contains results you don't want to keep, you +may remove or edit it. + + The file `configure.ac' (or `configure.in') is used to create +`configure' by a program called `autoconf'. You need `configure.ac' if +you want to change it or regenerate `configure' using a newer version +of `autoconf'. + + The simplest way to compile this package is: + + 1. `cd' to the directory containing the package's source code and type + `./configure' to configure the package for your system. + + Running `configure' might take a while. While running, it prints + some messages telling which features it is checking for. + + 2. Type `make' to compile the package. + + 3. Optionally, type `make check' to run any self-tests that come with + the package, generally using the just-built uninstalled binaries. + + 4. Type `make install' to install the programs and any data files and + documentation. When installing into a prefix owned by root, it is + recommended that the package be configured and built as a regular + user, and only the `make install' phase executed with root + privileges. + + 5. Optionally, type `make installcheck' to repeat any self-tests, but + this time using the binaries in their final installed location. + This target does not install anything. Running this target as a + regular user, particularly if the prior `make install' required + root privileges, verifies that the installation completed + correctly. + + 6. You can remove the program binaries and object files from the + source code directory by typing `make clean'. To also remove the + files that `configure' created (so you can compile the package for + a different kind of computer), type `make distclean'. There is + also a `make maintainer-clean' target, but that is intended mainly + for the package's developers. If you use it, you may have to get + all sorts of other programs in order to regenerate files that came + with the distribution. + + 7. Often, you can also type `make uninstall' to remove the installed + files again. In practice, not all packages have tested that + uninstallation works correctly, even though it is required by the + GNU Coding Standards. + + 8. Some packages, particularly those that use Automake, provide `make + distcheck', which can by used by developers to test that all other + targets like `make install' and `make uninstall' work correctly. + This target is generally not run by end users. + +Compilers and Options +===================== + + Some systems require unusual options for compilation or linking that +the `configure' script does not know about. Run `./configure --help' +for details on some of the pertinent environment variables. + + You can give `configure' initial values for configuration parameters +by setting variables in the command line or in the environment. Here +is an example: + + ./configure CC=c99 CFLAGS=-g LIBS=-lposix + + *Note Defining Variables::, for more details. + +Compiling For Multiple Architectures +==================================== + + You can compile the package for more than one kind of computer at the +same time, by placing the object files for each architecture in their +own directory. To do this, you can use GNU `make'. `cd' to the +directory where you want the object files and executables to go and run +the `configure' script. `configure' automatically checks for the +source code in the directory that `configure' is in and in `..'. This +is known as a "VPATH" build. + + With a non-GNU `make', it is safer to compile the package for one +architecture at a time in the source code directory. After you have +installed the package for one architecture, use `make distclean' before +reconfiguring for another architecture. + + On MacOS X 10.5 and later systems, you can create libraries and +executables that work on multiple system types--known as "fat" or +"universal" binaries--by specifying multiple `-arch' options to the +compiler but only a single `-arch' option to the preprocessor. Like +this: + + ./configure CC="gcc -arch i386 -arch x86_64 -arch ppc -arch ppc64" \ + CXX="g++ -arch i386 -arch x86_64 -arch ppc -arch ppc64" \ + CPP="gcc -E" CXXCPP="g++ -E" + + This is not guaranteed to produce working output in all cases, you +may have to build one architecture at a time and combine the results +using the `lipo' tool if you have problems. + +Installation Names +================== + + By default, `make install' installs the package's commands under +`/usr/local/bin', include files under `/usr/local/include', etc. You +can specify an installation prefix other than `/usr/local' by giving +`configure' the option `--prefix=PREFIX', where PREFIX must be an +absolute file name. + + You can specify separate installation prefixes for +architecture-specific files and architecture-independent files. If you +pass the option `--exec-prefix=PREFIX' to `configure', the package uses +PREFIX as the prefix for installing programs and libraries. +Documentation and other data files still use the regular prefix. + + In addition, if you use an unusual directory layout you can give +options like `--bindir=DIR' to specify different values for particular +kinds of files. Run `configure --help' for a list of the directories +you can set and what kinds of files go in them. In general, the +default for these options is expressed in terms of `${prefix}', so that +specifying just `--prefix' will affect all of the other directory +specifications that were not explicitly provided. + + The most portable way to affect installation locations is to pass the +correct locations to `configure'; however, many packages provide one or +both of the following shortcuts of passing variable assignments to the +`make install' command line to change installation locations without +having to reconfigure or recompile. + + The first method involves providing an override variable for each +affected directory. For example, `make install +prefix=/alternate/directory' will choose an alternate location for all +directory configuration variables that were expressed in terms of +`${prefix}'. Any directories that were specified during `configure', +but not in terms of `${prefix}', must each be overridden at install +time for the entire installation to be relocated. The approach of +makefile variable overrides for each directory variable is required by +the GNU Coding Standards, and ideally causes no recompilation. +However, some platforms have known limitations with the semantics of +shared libraries that end up requiring recompilation when using this +method, particularly noticeable in packages that use GNU Libtool. + + The second method involves providing the `DESTDIR' variable. For +example, `make install DESTDIR=/alternate/directory' will prepend +`/alternate/directory' before all installation names. The approach of +`DESTDIR' overrides is not required by the GNU Coding Standards, and +does not work on platforms that have drive letters. On the other hand, +it does better at avoiding recompilation issues, and works well even +when some directory options were not specified in terms of `${prefix}' +at `configure' time. + +Optional Features +================= + + If the package supports it, you can cause programs to be installed +with an extra prefix or suffix on their names by giving `configure' the +option `--program-prefix=PREFIX' or `--program-suffix=SUFFIX'. + + Some packages pay attention to `--enable-FEATURE' options to +`configure', where FEATURE indicates an optional part of the package. +They may also pay attention to `--with-PACKAGE' options, where PACKAGE +is something like `gnu-as' or `x' (for the X Window System). The +`README' should mention any `--enable-' and `--with-' options that the +package recognizes. + + For packages that use the X Window System, `configure' can usually +find the X include and library files automatically, but if it doesn't, +you can use the `configure' options `--x-includes=DIR' and +`--x-libraries=DIR' to specify their locations. + + Some packages offer the ability to configure how verbose the +execution of `make' will be. For these packages, running `./configure +--enable-silent-rules' sets the default to minimal output, which can be +overridden with `make V=1'; while running `./configure +--disable-silent-rules' sets the default to verbose, which can be +overridden with `make V=0'. + +Particular systems +================== + + On HP-UX, the default C compiler is not ANSI C compatible. If GNU +CC is not installed, it is recommended to use the following options in +order to use an ANSI C compiler: + + ./configure CC="cc -Ae -D_XOPEN_SOURCE=500" + +and if that doesn't work, install pre-built binaries of GCC for HP-UX. + + HP-UX `make' updates targets which have the same time stamps as +their prerequisites, which makes it generally unusable when shipped +generated files such as `configure' are involved. Use GNU `make' +instead. + + On OSF/1 a.k.a. Tru64, some versions of the default C compiler cannot +parse its `' header file. The option `-nodtk' can be used as +a workaround. If GNU CC is not installed, it is therefore recommended +to try + + ./configure CC="cc" + +and if that doesn't work, try + + ./configure CC="cc -nodtk" + + On Solaris, don't put `/usr/ucb' early in your `PATH'. This +directory contains several dysfunctional programs; working variants of +these programs are available in `/usr/bin'. So, if you need `/usr/ucb' +in your `PATH', put it _after_ `/usr/bin'. + + On Haiku, software installed for all users goes in `/boot/common', +not `/usr/local'. It is recommended to use the following options: + + ./configure --prefix=/boot/common + +Specifying the System Type +========================== + + There may be some features `configure' cannot figure out +automatically, but needs to determine by the type of machine the package +will run on. Usually, assuming the package is built to be run on the +_same_ architectures, `configure' can figure that out, but if it prints +a message saying it cannot guess the machine type, give it the +`--build=TYPE' option. TYPE can either be a short name for the system +type, such as `sun4', or a canonical name which has the form: + + CPU-COMPANY-SYSTEM + +where SYSTEM can have one of these forms: + + OS + KERNEL-OS + + See the file `config.sub' for the possible values of each field. If +`config.sub' isn't included in this package, then this package doesn't +need to know the machine type. + + If you are _building_ compiler tools for cross-compiling, you should +use the option `--target=TYPE' to select the type of system they will +produce code for. + + If you want to _use_ a cross compiler, that generates code for a +platform different from the build platform, you should specify the +"host" platform (i.e., that on which the generated programs will +eventually be run) with `--host=TYPE'. + +Sharing Defaults +================ + + If you want to set default values for `configure' scripts to share, +you can create a site shell script called `config.site' that gives +default values for variables like `CC', `cache_file', and `prefix'. +`configure' looks for `PREFIX/share/config.site' if it exists, then +`PREFIX/etc/config.site' if it exists. Or, you can set the +`CONFIG_SITE' environment variable to the location of the site script. +A warning: not all `configure' scripts look for a site script. + +Defining Variables +================== + + Variables not defined in a site shell script can be set in the +environment passed to `configure'. However, some packages may run +configure again during the build, and the customized values of these +variables may be lost. In order to avoid this problem, you should set +them in the `configure' command line, using `VAR=value'. For example: + + ./configure CC=/usr/local2/bin/gcc + +causes the specified `gcc' to be used as the C compiler (unless it is +overridden in the site shell script). + +Unfortunately, this technique does not work for `CONFIG_SHELL' due to +an Autoconf limitation. Until the limitation is lifted, you can use +this workaround: + + CONFIG_SHELL=/bin/bash ./configure CONFIG_SHELL=/bin/bash + +`configure' Invocation +====================== + + `configure' recognizes the following options to control how it +operates. + +`--help' +`-h' + Print a summary of all of the options to `configure', and exit. + +`--help=short' +`--help=recursive' + Print a summary of the options unique to this package's + `configure', and exit. The `short' variant lists options used + only in the top level, while the `recursive' variant lists options + also present in any nested packages. + +`--version' +`-V' + Print the version of Autoconf used to generate the `configure' + script, and exit. + +`--cache-file=FILE' + Enable the cache: use and save the results of the tests in FILE, + traditionally `config.cache'. FILE defaults to `/dev/null' to + disable caching. + +`--config-cache' +`-C' + Alias for `--cache-file=config.cache'. + +`--quiet' +`--silent' +`-q' + Do not print messages saying which checks are being made. To + suppress all normal output, redirect it to `/dev/null' (any error + messages will still be shown). + +`--srcdir=DIR' + Look for the package's source code in directory DIR. Usually + `configure' can determine that directory automatically. + +`--prefix=DIR' + Use DIR as the installation prefix. *note Installation Names:: + for more details, including other options available for fine-tuning + the installation locations. + +`--no-create' +`-n' + Run the configure checks, but stop before creating any output + files. + +`configure' also accepts some other, not widely useful, options. Run +`configure --help' for more details. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..9cecc1d --- /dev/null +++ b/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + 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 . + +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: + + {project} Copyright (C) {year} {fullname} + 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 +. + + 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 +. diff --git a/Makefile.am b/Makefile.am new file mode 100644 index 0000000..c0edabf --- /dev/null +++ b/Makefile.am @@ -0,0 +1,5 @@ +SUBDIRS = src data man i18n + +# For Python autoconf macros +ACLOCAL_AMFLAGS = -I m4 + diff --git a/NEWS b/NEWS new file mode 100644 index 0000000..31ae182 --- /dev/null +++ b/NEWS @@ -0,0 +1,359 @@ +gnome15 0.11.0 (2018-02-10) +=========================== + +New community at https://gnome15.org/ + + +gnome15 0.10.2 (2015-05-14) +========================== + +Gnome15 is a suite of tools for the Logitech G series keyboards and +headsets, including the G15, G19, G13, G930, G35, G510, G11, G110 and +the Z-10 speakers aiming to provide the best integration possible with +the Linux Desktop. + +Gnome15 provides: +* A panel indicator (or applet) +* A configuration tool +* A macro system +* A set of plugins + +Developers can extend Gnome15 by writing plugins in the Python +programming language, or they can use the provided D-Bus API. +Gnome15 uses GNOME technologies, while staying well integrated with +other desktops such as Ubuntu Unity and XFCE. It should also work with +KDE. + +About this release +------------------ + +Release 0.10.0 is a major release because of the changes to the kernel +driver, which makes it incopatible with the 0.9.x series. + +So, users wanting to stick with gnome15-0.9.x should use branch `pre-refactor` +of the kernel drivers found at https://github.com/CMoH/lg4l, while +gnome15-0.10.x would rely on branch `master`. + +Also, this rather unofficial release is made outside russo79.com, since +the website is down for quite a while. The rest of the release is the same +as 0.9.8. + +A new "Pommodoro Timer" plugin is now available. +The Gnome shell extension now supports Gnome 3.10 + +Changes since 0.10.1 +-------------------- + +* g15-config: fix import error with pygobject-3.14.0 + +Changes since 0.10.0 +-------------------- + +* driver_kernel: fix keymaps for G110 and G15v2 + +Changes since 0.9.7 +------------------- + +* Break compatibility with old kernel module implementation. +* The Gnome Shell extension now displays the plugin list in + alphabetical order. (https://projects.russo79.com/issues/172) +* A new "Pommodoro Timer" plugin is now available. + (https://projects.russo79.com/issues/240) +* The Gnome Shell extension now supports Gnome 3.10. + (https://projects.russo79.com/issues/290) + +Bugs fixed in this release +-------------------------- + +* The build now displays an error error if neither PIL or pillow python + libraries are available. (https://projects.russo79.com/issues/286) +* The stop key (multimedia) now sends the correct keycode when Gnome15 + is running. (https://projects.russo79.com/issues/291) +* The weather plugin now keeps refreshing its data even if it is not + the active plugin. (https://projects.russo79.com/issues/298) +* The screensaver plugin now correctly displays the user message on + G19 keyboards. (https://projects.russo79.com/issues/299) +* The g15top module now provides a uptime method. This module is used + as a fallback replacement for python-gtop on systems that don't + provide it. (https://projects.russo79.com/issues/301) + +Under the hood changes for this release +--------------------------------------- + +* Exceptions are now logged in a consistent way + (https://projects.russo79.com/issues/269) +* XDG directories are now used instead of hard coded paths. + (https://projects.russo79.com/issues/278) +* mono icons are no longer installed by default. These icons are only + used on Ubuntu systems. (https://projects.russo79.com/issues/285) +* The python interpreter for Gnome15 scripts was changed from 'python' + to 'python2'. 'python2' is available on all the supported + distributions except Debian 7 (Wheezy). Users of Debian 7 building + Gnome15 from source must manually change the shebang lines of the + files in the src/scripts directory. + (https://projects.russo79.com/issues/289) +* User custom plugins should now be installed in + $XDG_DATA_HOME/gnome15/plugins instead of + $XDG_CONFIG_HOME/gnome15/plugins. + Support for $XDG_CONFIG_HOME/gnome15/plugins will be removed in a + future release. + +Have contributed code to this release: + +* Bram Faas + +gnome15 0.9.7 (2013-10-05) +========================== + +Gnome15 is a suite of tools for the Logitech G series keyboards and +headsets, including the G15, G19, G13, G930, G35, G510, G11, G110 and +the Z-10 speakers aiming to provide the best integration possible with +the Linux Desktop. + +Gnome15 provides: +* A panel indicator (or applet) +* A configuration tool +* A macro system +* A set of plugins + +Developers can extend Gnome15 by writing plugins in the Python +programming language, or they can use the provided D-Bus API. +Gnome15 uses GNOME technologies, while staying well integrated with +other desktops such as Ubuntu Unity and XFCE. It should also work with +KDE. + +About this release +------------------ + +Release 0.9.7 is mainly a bug fix release with some enhancements. +Some minor changes took place with the configuration tool. +The "Driver" tab no longer exists and the driver can now be selected +directly from the "Keyboard" tab. A new button is used to display the +driver options. + +Changes since 0.9.6 +------------------- + +* The volume monitor plugin now allows the selection of the sound card + to monitor. (https://projects.russo79.com/issues/212) +* The sense plugin now supports udisks version 2. + (https://projects.russo79.com/issues/261) +* Users using the g15direct driver can now make the G13 joystick behave + as a digital joystick. (https://projects.russo79.com/issues/280) + +Bugs fixed in this release +-------------------------- + +* The sense plugin now gracefully handles errors when it cannot connect + to a sensor source. (https://projects.russo79.com/issues/235) +* The screensaver plugin now detects Gnome Shell integrated screensaver. + (https://projects.russo79.com/issues/257) +* Fix a issue with the build system that automatically tries to build + appindicator even when the appindicator python library was not + available. (https://projects.russo79.com/issues/258) +* Fixed the license displayed in the About dialogs. + (https://projects.russo79.com/issues/259) +* Fixed the detection of the pillow library when building Gnome15. + (https://projects.russo79.com/issues/260) +* The sense plugin udisks sensor source is now periodically refreshed. + (https://projects.russo79.com/issues/262) +* The Display resolution plugin kept checking the display resolution + even when the plugin was disabled. + (https://projects.russo79.com/issues/266) +* The impulse plugin wasn't correctly linked to the libpulse and fftw3 + libraries. (https://projects.russo79.com/issues/267) +* The virtual joysticks simulated by Gnome15 are now calibrated to at + they center instead of their top-left position. + (https://projects.russo79.com/issues/271) +* Simulation of joystick events using G keys has been fixed and no + longer throws an exception. + (https://projects.russo79.com/issues/273) +* No longer display a "Unknown property: GtkMenu.ubuntu-local" when + displaying some windows. (https://projects.russo79.com/issues/274) +* Correctly simulate joystick axis movements when using the kernel + driver. (https://projects.russo79.com/issues/275) +* Correctly handle changes of the kernel driver options. + (https://projects.russo79.com/issues/276) +* The g15-system-service can now be manually stopped (useful for + development only). (https://projects.russo79.com/issues/281) + +Under the hood changes for this release +--------------------------------------- + +* The GetPagesBelowPriority D-Bus method is now marked as deprecated + and should no longer be used. + (https://projects.russo79.com/issues/270) + +Have contributed code to this release: + +* NoXPhasma + +gnome15 0.9.6 (2013-09-01) +========================== + +Gnome15 is a suite of tools for the Logitech G series keyboards and +headsets, including the G15, G19, G13, G930, G35, G510, G11, G110 and +the Z-10 speakers aiming to provide the best integration possible with +the Linux Desktop. + +Gnome15 provides: +* A panel indicator (or applet) +* A configuration tool +* A macro system +* A set of plugins + +Developers can extend Gnome15 by writing plugins in the Python +programming language, or they can use the provided D-Bus API. +Gnome15 uses GNOME technologies, while staying well integrated with +other desktops such as Ubuntu Unity and XFCE. It should also work with +KDE. + +About this release +------------------ + +Release 0.9.6 was focused on simplifying and cleaning up the source +code tree as well as the build system. +The new structure has less levels of depth and should be simpler to +maintain on the long term. +Most of the previous sub-projects that were maintained separatedly are +now aggregated into a single tree. +Besides some external dependencies, gnome15 can now be build by +issuing a single './configure; make; make install' command. + +The kernel modules have also been updated with the latest upstream +changes. + +Of course, some bugs were also fixed. + +This is also the first release of Gnome15 to be a 'official' one. +Brett Smith, the original maintainer has decided to retire himself from +the project, and he supports what was until now a unofficial fork of +Gnome15. + +He will however keep contributing to the project. + +Changes since 0.9.5 +------------------- + +* https://projects.russo79.com/issues/256 + +Bugs fixed in this release +-------------------------- + +* https://projects.russo79.com/issues/130 +* https://projects.russo79.com/issues/242 +* https://projects.russo79.com/issues/246 +* https://projects.russo79.com/issues/247 +* https://projects.russo79.com/issues/248 +* https://projects.russo79.com/issues/250 +* https://projects.russo79.com/issues/253 +* https://projects.russo79.com/issues/254 + +Under the hood changes for this release +--------------------------------------- + +* https://projects.russo79.com/issues/171 +* https://projects.russo79.com/issues/219 +* https://projects.russo79.com/issues/245 + +gnome15 0.9.5 (2013-07-03) +========================== + +This is an "unofficial" release of Gnome15. +There was no news from the original author since almost +seven months now. + +Changes since 0.9.4: + +* https://projects.russo79.com/issues/195 +* https://projects.russo79.com/issues/208 +* https://projects.russo79.com/issues/209 + +Bugs fixed in this release: + +* https://projects.russo79.com/issues/173 +* https://projects.russo79.com/issues/191 +* https://projects.russo79.com/issues/194 +* https://projects.russo79.com/issues/220 +* https://projects.russo79.com/issues/223 +* https://projects.russo79.com/issues/227 +* https://projects.russo79.com/issues/228 +* https://projects.russo79.com/issues/229 +* https://projects.russo79.com/issues/232 +* https://projects.russo79.com/issues/233 +* https://projects.russo79.com/issues/234 + +Under the hood changes for this release: + +* https://projects.russo79.com/issues/190 +* https://projects.russo79.com/issues/218 +* https://projects.russo79.com/issues/226 +* https://projects.russo79.com/issues/236 + +gnome15 0.9.4 (2013-06-04) +========================== + +This is an "unofficial" release of Gnome15. +There was no news from the original author since almost +six months now. + +Changes since 0.9.3: + +* https://projects.russo79.com/issues/196 + +Bugs fixed in this release: + +* https://projects.russo79.com/issues/160 +* https://projects.russo79.com/issues/162 +* https://projects.russo79.com/issues/167 +* https://projects.russo79.com/issues/174 +* https://projects.russo79.com/issues/181 +* https://projects.russo79.com/issues/183 +* https://projects.russo79.com/issues/186 +* https://projects.russo79.com/issues/187 +* https://projects.russo79.com/issues/188 +* https://projects.russo79.com/issues/189 +* https://projects.russo79.com/issues/191 +* https://projects.russo79.com/issues/192 +* https://projects.russo79.com/issues/194 +* https://projects.russo79.com/issues/211 + +Under the hood changes for this release: + +* https://projects.russo79.com/issues/193 + +gnome15 0.9.3 (2013-04-29) +========================== + +This is an "unofficial" release of Gnome15. +There was no news from the original author since four +months now. + +Changes since 0.9.2: + +* Update URL for the project. + +Bugs fixed in this release: + +* https://projects.russo79.com/issues/113 +* https://projects.russo79.com/issues/148 +* https://projects.russo79.com/issues/149 +* https://projects.russo79.com/issues/150 +* https://projects.russo79.com/issues/156 +* https://projects.russo79.com/issues/160 +* https://projects.russo79.com/issues/161 +* https://projects.russo79.com/issues/162 +* https://projects.russo79.com/issues/163 +* https://projects.russo79.com/issues/167 +* https://projects.russo79.com/issues/170 + +gnome15 0.5.0 (2011-03-09) + + Big changes under the hood. g15-desktop-service is now the + process that manages the plugins, LCD and macros. Panel + integration is now provided by separate packages. + + Macro creation and editing in the configuration UI is now + possible. + + Lots of bug fixes as well (see the changelog). diff --git a/README b/README new file mode 100644 index 0000000..8682c75 --- /dev/null +++ b/README @@ -0,0 +1,40 @@ +STATUS OF GNOME15 +================= + +Gnome15 is currently **not complete maintained**. +The original primary repository has been unavailable since November 2014 due to a hosting server crash. +This fork was made to add a feature and has not been updated since November 2013, but it appears to be the latest snapshot of the repository that is currently publicly available. + +I intend to bring this repository up to date with the latest version (the version before the server crash) using the code contained in the latest distribution packages available. +We want to maintain it, so many we can. Feel free to work with. + +Gnome15 +======= + +A set of tools for configuring the Logitech G15 keyboard. + +Contains pylibg19, a library providing support for the Logitech G19 until there +is kernel support available. It was based "Logitech-G19-Linux-Daemon" [1], +the work of "MultiCoreNop" [2]. + +1. http://github.com/MultiCoreNop/Logitech-G19-Linux-Daemon +2. http://github.com/MultiCoreNop + +Installation +============ + +See the 'INSTALL' file or the [Wiki Entry](https://github.com/Huskynarr/gnome15/wiki/INSTALL) + +How to report bugs +================== + +Issues can be submited on the [github website](https://github.com/Huskynarr/gnome15/issues) [3]. + +3. https://github.com/Huskynarr/gnome15/issues + +Requirements +============ + +- Python 2.6 +- PyUSB 0.4 +- PIL (Python Image Library, just about any version should be ok) diff --git a/TRANSLATION_PROGRESS.txt b/TRANSLATION_PROGRESS.txt new file mode 100644 index 0000000..baab97c --- /dev/null +++ b/TRANSLATION_PROGRESS.txt @@ -0,0 +1,40 @@ +SVG +=== + +themes/default/mx5500-menu-screen.svg +themes/default/default-menu-screen.svg +themes/default/default-menu-child-entry.svg +themes/default/default-error-screen.svg +themes/default/default-confirmation-screen.svg +themes/default/mx5500-confirmation-screen.svg +themes/default/g19-confirmation-screen.svg +themes/default/mx5500-menu-separator.svg +themes/default/mx5500-error-screen.svg +themes/default/g19-menu-screen.svg +themes/default/default-menu-entry.svg +themes/default/default-menu-separator.svg +themes/default/g19-error-screen.svg +themes/default/mx5500-menu-child-entry.svg +themes/default/mx5500-menu-entry.svg +themes/default/g19-menu-entry.svg +themes/default/g19-menu-child-entry.svg +themes/default/g19-menu-separator.svg +main/resources/images/g19-background.svg +main/resources/images/mx5500-background.svg +main/resources/images/default-background.svg +main/resources/icons/hicolor/scalable/apps/gnome15.svg +main/resources/icons/hicolor/scalable/status/logitech-g-keyboard-panel.svg +main/resources/icons/hicolor/scalable/status/logitech-g-keyboard-error-panel.svg + + +Desktop +======= + +gnome/applications/g15-config.desktop +gnome/applications/g15-config.desktop.in +gnome/autostart/gnome15.desktop +gnome/autostart/gnome15.desktop.in +gnome/autostart/g15-indicator.desktop.in +gnome/autostart/g15-systemtray.desktop.in +gnome/autostart/g15-indicator.desktop +gnome/autostart/g15-systemtray.desktop diff --git a/_config.yml b/_config.yml new file mode 100644 index 0000000..9da9a02 --- /dev/null +++ b/_config.yml @@ -0,0 +1 @@ +theme: jekyll-theme-dinky \ No newline at end of file diff --git a/b.sh b/b.sh new file mode 100755 index 0000000..30c2cec --- /dev/null +++ b/b.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +SUFFIX="" +VERSION=$(grep "AC_INIT" configure.in|awk -F, '{ print $2 }'|awk -F\) '{ print $1 }'|sed 's/ //g') + +autoreconf -f && ./configure --enable-udev=/lib/udev/rules.d && make && make dist && cp gnome15-${VERSION}.tar.gz ~/Workspaces/home\:tanktarta\:gnome15${SUFFIX}/gnome15 diff --git a/build-po.sh b/build-po.sh new file mode 100755 index 0000000..f57876f --- /dev/null +++ b/build-po.sh @@ -0,0 +1,41 @@ +#!/bin/bash + +echo -e "Locale: \c" +read locale +if [ -n "${locale}" ]; then + + cd $(dirname $0) + basedir=$(pwd) + for i in src/*; do + if [ -d ${i} ]; then + modname=$(basename $i) + echo $modname + pushd ${i} >/dev/null + mkdir -p i18n + + # Generate python / ui + pushd i18n >/dev/null + for i in *.pot; do + bn=$(basename $i .pot).${locale}.po + msginit --no-translator --input=${i} --output=${bn} --locale=${locale} + done + popd >/dev/null + + # Generate theme + for j in * + do + if [ -d $j/i18n ]; then + pushd $j/i18n >/dev/null + for k in *.pot ; do + bn=$(basename $k .pot).${locale}.po + echo "$k -> $bn [$locale]" + msginit --no-translator --input=${k} --output=${bn} --locale=${locale} + done + popd >/dev/null + fi + done + + popd >/dev/null + fi + done +fi diff --git a/build-pot.sh b/build-pot.sh new file mode 100755 index 0000000..f9b502a --- /dev/null +++ b/build-pot.sh @@ -0,0 +1,54 @@ +#!/bin/bash + +# +# Creates the initial i18n structure for plugins +# + +cd $(dirname $0) +basedir=$(pwd) +for i in src/*; do + if [ -d ${i} ]; then + modname=$(basename $i) + pushd ${i} + mkdir -p i18n + + # Python + xgettext --language=Python --keyword=_ --output=i18n/${modname}.pot *.py + + # Theme files + + for m in *; do + if [ -d "$m" ]; then + pushd $m + if [ $(ls *.svg 2>/dev/null|wc -l) -gt 0 ]; then + mkdir -p i18n + echo "Found SVG" + for s in *.svg; do + echo "Generating C header$s" + svgname=$(basename ${s} .svg) + ${basedir}/../gnome15/mksvgheaders.py ${s} > i18n/${svgname}.h + if [ -s i18n/${svgname}.h ]; then + echo "Generating POT for ${svgname}.h" + xgettext --language=Python --keyword=_ --keyword=N_ --output=i18n/${svgname}.pot i18n/${svgname}.h + else + rm -f i18n/${svgname}.h + fi + done + fi + popd + fi + done + + # .ui files + if [ $(ls *.ui 2>/dev/null|wc -l) -gt 0 ]; then + for i in *.ui; do + intltool-extract --type=gettext/glade ${i} + uiname=$(basename $i .ui) + mv -f ${i}.h i18n + xgettext --language=Python --keyword=_ --keyword=N_ --output=i18n/${uiname}.pot i18n/${i}.h + done + fi + + popd + fi +done diff --git a/compile.sh b/compile.sh new file mode 100755 index 0000000..9b110fd --- /dev/null +++ b/compile.sh @@ -0,0 +1,5 @@ +autoreconf -i +./configure +make +sudo make install + diff --git a/configure.ac b/configure.ac new file mode 100644 index 0000000..b66330d --- /dev/null +++ b/configure.ac @@ -0,0 +1,1401 @@ +AC_INIT([gnome15], [0.11.0], [bugs@gnome15.org]) +AC_CONFIG_SRCDIR([src/gnome15/g15service.py]) +AM_INIT_AUTOMAKE([tar-ustar]) +AM_MAINTAINER_MODE + +AC_CONFIG_MACRO_DIR([m4]) + +dnl +dnl Dependencies +dnl + +dnl C compiler toolchain +LT_PREREQ([2.2.6]) +LT_INIT() + +AC_PROG_CC + +dnl Python +AM_PATH_PYTHON +AX_PYTHON_DEVEL([2.6]) + +dnl Python modules (mandatory) +PKG_CHECK_MODULES(PYGTK, pygtk-2.0) +AC_SUBST(PYGTK_CFLAGS) +AC_SUBST(PYGTK_LIBS) + +AX_PYTHON_MODULE(keyring, []) +AS_IF([test "x${HAVE_PYMOD_KEYRING}" = "xno"], + [AC_MSG_ERROR([Requires Python Keyring Library])]) + +AX_PYTHON_MODULE(virtkey, []) +AS_IF([test "x${HAVE_PYMOD_VIRTKEY}" = "xno"], + [AC_MSG_ERROR([Requires Python Virtkey Library])]) + +AX_PYTHON_MODULE(PIL.Image, []) +AS_IF([test "x${HAVE_PYMOD_PIL_IMAGE}" = "xno"], + [AC_MSG_ERROR([Requires Python Image Library])]) + +AX_PYTHON_MODULE(cairo, []) +AS_IF([test "x${HAVE_PYMOD_CAIRO}" = "xno"], + [AC_MSG_ERROR([Requires Python bindings for the Cairo vector graphics library])]) + +AX_PYTHON_MODULE(dbus, []) +AS_IF([test "x${HAVE_PYMOD_DBUS}" = "xno"], + [AC_MSG_ERROR([Requires DBUS bindings for Python])]) + +AX_PYTHON_MODULE(pyinotify, []) +AS_IF([test "x${HAVE_PYMOD_PYINOTIFY}" = "xno"], + [AC_MSG_ERROR([Requires Pyinotify bindings for Python])]) + +AX_PYTHON_MODULE(lxml, []) +AS_IF([test "x${HAVE_PYMOD_LXML}" = "xno"], + [AC_MSG_ERROR([Requires LXML bindings for Python])]) + +AX_PYTHON_MODULE(gobject, []) +AS_IF([test "x${HAVE_PYMOD_GOBJECT}" = "xno"], + [AC_MSG_ERROR([Requires GObject for Python])]) + +AX_PYTHON_MODULE(xdg, []) +AS_IF([test "x${HAVE_PYMOD_XDG}" = "xno"], + [AC_MSG_ERROR([Requires Python XDG])]) + +AX_PYTHON_MODULE(usb, []) +AS_IF([test "x${HAVE_PYMOD_USB}" = "xno"], + [AC_MSG_ERROR([Requires PyUSB, python bindings for libusb])]) + +AX_PYTHON_MODULE(gconf, []) +AS_IF([test "x${HAVE_PYMOD_GCONF}" = "xno"], + [AC_MSG_ERROR([Requires GConf bindings for Python])]) + +AX_PYTHON_MODULE(rsvg, []) +AS_IF([test "x${HAVE_PYMOD_RSVG}" = "xno"], + [AC_MSG_ERROR([Requires RSVG for Python])]) + +AX_PYTHON_MODULE(pango, []) +AS_IF([test "x${HAVE_PYMOD_PANGO}" = "xno"], + [AC_MSG_ERROR([Requires Pango for Python])]) + +AX_PYTHON_MODULE(uinput, []) +AS_IF([test "x${HAVE_PYMOD_UINPUT}" = "xno"], + [AC_MSG_ERROR([Requires Python uinput and libsuinput])]) + +AX_PYTHON_MODULE(Xlib, []) +AS_IF([test "x${HAVE_PYMOD_XLIB}" = "xno"], + [AC_MSG_ERROR([Requires Python Xlib - Python Xlib bindings])]) + +dnl Python modules (optional) +AX_PYTHON_MODULE(setproctitle, []) +AS_IF([test "x${HAVE_PYMOD_SETPROCTITLE}" = "xno"], + [AC_MSG_WARN([It is recommend that setproctitle is installed])]) + +AX_PYTHON_MODULE(pyudev, []) +AS_IF([test "x${HAVE_PYUDEV}" = "xno"], + [AC_MSG_WARN([It is recommended that PyUdev is installed. Without this, there will be no hot-plugging support])]) + +dnl Python modules (mandatory for some drivers) +AX_PYTHON_MODULE(pyinputevent, []) +AS_IF([test "x${HAVE_PYMOD_PYINPUTEVENT}" = "xyes"], + [have_pyinputevent=yes], + [have_pyinputevent=no]) + +dnl Python modules (mandatory for some plugins) +AX_PYTHON_MODULE(gst, []) +AS_IF([test "x${HAVE_PYMOD_GST}" = "xyes"], + [have_gst=yes], + [have_gst=no]) + +AX_PYTHON_MODULE(appindicator, []) +AS_IF([test "x${HAVE_PYMOD_APPINDICATOR}" = "xyes"], + [have_appindicator=yes], + [have_appindicator=no]) + +AX_PYTHON_MODULE(telepathy, []) +AS_IF([test "x${HAVE_PYMOD_TELEPATHY}" = "xyes"], + [have_telepathy=yes], + [have_telepathy=no]) + +AX_PYTHON_MODULE(alsaaudio, []) +AS_IF([test "x${HAVE_PYMOD_ALSAAUDIO}" = "xyes"], + [have_alsaaudio=yes], + [have_alsaaudio=no]) + +AX_PYTHON_MODULE(feedparser, []) +AS_IF([test "x${HAVE_PYMOD_FEEDPARSER}" = "xyes"], + [have_feedparser=yes], + [have_feedparser=no]) + +AX_PYTHON_MODULE(vobject, []) +AS_IF([test "x${HAVE_PYMOD_VOBJECT}" = "xyes" ], + [have_vobject=yes], + [have_vobject=no]) + +AX_PYTHON_MODULE(gdata.calendar, []) +AS_IF([test "x${HAVE_PYMOD_GDATA_CALENDAR}" = "xyes" ], + [have_gdata_calendar=yes], + [have_gdata_calendar=no]) + +AX_PYTHON_MODULE(gdata.analytics, []) +AS_IF([test "x${HAVE_PYMOD_GDATA_ANALYTICS}" = "xyes" ], + [have_gdata_analytics=yes], + [have_gdata_analytics=no]) + +AX_PYTHON_MODULE(cairoplot, []) +AS_IF([test "x${HAVE_PYMOD_CAIROPLOT}" = "xyes" ], + [have_cairoplot=yes], + [have_cairoplot=no]) + +AX_PYTHON_MODULE(sensors, []) +AS_IF([test "x${HAVE_PYMOD_SENSORS}" = "xyes" ], + [have_sensors=yes], + [have_sensors=no]) + +dnl libg15 (Gnome15 version) +AC_CHECK_LIB(g15, initLibG15, + [have_libg15=yes], + [have_libg15=no]) + +dnl fftw3 +PKG_CHECK_MODULES(FFTW, fftw3, + [have_fftw3=yes], + [have_fftw3=no]) +AC_SUBST(FFTW_CFLAGS) +AC_SUBST(FFTW_LIBS) + +dnl libpulse +PKG_CHECK_MODULES(PULSE, libpulse, + [have_pulse=yes], + [have_pulse=no]) +AC_SUBST(PULSE_CFLAGS) +AC_SUBST(PULSE_LIBS) + +dnl +dnl Parse configure arguments +dnl + +dnl Enabled locales +AC_ARG_VAR(ENABLED_LOCALES, [List of locales to enable]) +AS_IF([test -z "${ENABLED_LOCALES}"], + AC_SUBST([ENABLED_LOCALES], [en_GB])) + +dnl Name of Fixed size font +AC_ARG_VAR(FIXED_SIZE_FONT, [Font to use for fixed fonts. Defaults to Fixed]) +AS_IF([test -z "${FIXED_SIZE_FONT}"], + AC_SUBST([FIXED_SIZE_FONT], [Fixed])) + +dnl udev file path and settings +AC_ARG_VAR(UDEV_RULES_PATH, [Path for udev rules. Defaults to /lib/udev/rules.d]) +AS_IF([test -z "${UDEV_RULES_PATH}"], + AC_SUBST([UDEV_RULES_PATH], [/lib/udev/rules.d])) +AC_ARG_VAR(DEVICEGROUP, [Group that the devices will be owned by. Defaults to plugdev]) +AS_IF([test -z "${DEVICEGROUP}"], + AC_SUBST([DEVICEGROUP], [plugdev])) +AC_ARG_VAR(DEVICEMODE, [Permissions of the devices. Defaults to 0660]) +AS_IF([test -z "${DEVICEMODE}"], + AC_SUBST([DEVICEMODE], [0660])) + +dnl Drivers +AC_ARG_ENABLE([driver-kernel], + [AS_HELP_STRING([--enable-driver-kernel], + [Enable Kernel driver support (requires pyinputevent and lg4l kernel drivers).])], + [case "${enableval}" in + yes) driver_kernel=yes ;; + no) driver_kernel=no ;; + *) AC_MSG_ERROR([bad value ${enableval} for --enable-driver-kernel]) ;; + esac], + [driver_kernel=${have_pyinputevent}]) +AS_IF([test "x${driver_kernel}" = "xyes" -a "x$have_pyinputevent" = "xno"], + [AC_MSG_ERROR([kernel driver cannot be built without pyinputevent])]) +AM_CONDITIONAL([ENABLE_DRIVER_KERNEL], [test x$driver_kernel = xyes]) + +AC_ARG_ENABLE([driver-g19direct], + [AS_HELP_STRING([--enable-driver-g19direct], + [Enable G19Direct driver support.])], + [case "${enableval}" in + yes) driver_g19direct=yes ;; + no) driver_g19direct=no ;; + *) AC_MSG_ERROR([bad value ${enableval} for --enable-driver-g19direct]) ;; + esac], + [driver_g19direct=yes]) +AM_CONDITIONAL([ENABLE_DRIVER_G19DIRECT], [test x$driver_g19direct = xyes]) + +AC_ARG_ENABLE([driver-g930], + [AS_HELP_STRING([--enable-driver-g930], + [Enable G930 headset driver support])], + [case "${enableval}" in + yes) driver_g930=yes ;; + no) driver_g930=no ;; + *) AC_MSG_ERROR([bad value ${enableval} for --enable-driver-g930]) ;; + esac], + [driver_g930=yes]) +AM_CONDITIONAL([ENABLE_DRIVER_G930], [test x$driver_g930 = xyes]) + +AC_ARG_ENABLE([driver-g15direct], + [AS_HELP_STRING([--enable-driver-g15direct], + [Enable G15 direct driver support (requires libg15).])], + [case "${enableval}" in + yes) driver_g15direct=yes ;; + no) driver_g15direct=no ;; + *) AC_MSG_ERROR([bad value ${enableval} for --enable-driver-g15direct]) ;; + esac], + [driver_g15direct=${have_libg15}]) +AS_IF([test "x${driver_g15direct}" = "xyes" -a "x$have_libg15" = "xno"], + [AC_MSG_ERROR([g5direct driver cannot be built without libg15])]) +AM_CONDITIONAL([ENABLE_DRIVER_G15DIRECT], [test x$driver_g15direct = xyes]) + +dnl System Tray +AC_ARG_ENABLE([systemtray], + [AS_HELP_STRING([--enable-systemtray], + [Enable System Tray panel integration.])], + [case "${enableval}" in + yes) systemtray=yes ;; + no) systemtray=no ;; + *) AC_MSG_ERROR([bad value ${enableval} for --enable-systemtray]) ;; + esac], + systemtray=yes) +AM_CONDITIONAL([ENABLE_SYSTEMTRAY], [test x$systemtray = xyes]) + +dnl Ubuntu Indicator +AC_ARG_ENABLE([indicator], + [AS_HELP_STRING([--enable-indicator], + [Enable Ubuntu Indicator integration.])], + [case "${enableval}" in + yes) indicator=yes ;; + no) indicator=no ;; + *) AC_MSG_ERROR([bad value ${enableval} for --enable-indicator]) ;; + esac], + indicator=${have_appindicator}) +AS_IF([test "x${indicator}" = "xyes" -a "x$have_appindicator" = "xno"], + [AC_MSG_ERROR([Ubuntu indicator cannot be built without appindicator python module])]) +AM_CONDITIONAL([ENABLE_INDICATOR], [test x$indicator = xyes]) + +dnl Gnome Shell Extension +AC_ARG_ENABLE([gnome-shell-extension], + [AS_HELP_STRING([--enable-gnome-shell-extension], + [Enable Gnome Shell extension.])], + [case "${enableval}" in + yes) gnome_shell_extension=yes ;; + no) gnome_shell_extension=no ;; + *) AC_MSG_ERROR([bad value ${enableval} for --enable-gnome-shell-extension]) ;; + esac], + gnome_shell_extension=yes) +AM_CONDITIONAL([ENABLE_GNOME_SHELL_EXTENSION], [test x$gnome_shell_extension = xyes]) + +dnl Icons + +AC_ARG_ENABLE([icons-awoken], + [AS_HELP_STRING([--enable-icons-awoken], + [Build and deploy awoken icons.])], + [case "${enableval}" in + yes) icons_awoken=yes ;; + no) icons_awoken=no ;; + *) AC_MSG_ERROR([bad value ${enableval} for --enable-icons-awoken]) ;; + esac], + [icons_awoken=no]) +AM_CONDITIONAL([ENABLE_ICONS_AWOKEN], [test x$icons_awoken = xyes]) + +AC_ARG_ENABLE([icons-mono], + [AS_HELP_STRING([--enable-icons-mono], + [Build and deploy monochrome icons.])], + [case "${enableval}" in + yes) icons_mono=yes ;; + no) icons_mono=no ;; + *) AC_MSG_ERROR([bad value ${enableval} for --enable-icons-mono]) ;; + esac], + [icons_mono=no]) +AM_CONDITIONAL([ENABLE_ICONS_MONO], [test x$icons_mono = xyes]) + +dnl +dnl Plugins +dnl + +dnl Each plugin by default is only enabled +dnl if dependencies are available. Each plugin may also be +dnl individually enabled / disabled using configure options + +dnl Background plugin +AC_ARG_ENABLE([plugin-background], + [AS_HELP_STRING([--enable-plugin-background], + [Enable Background plugin.])], + [case "${enableval}" in + yes) plugin_background=yes ;; + no) plugin_background=no ;; + *) AC_MSG_ERROR([bad value ${enableval} for --enable-plugin-background]) ;; + esac], + [plugin_background=yes]) +AM_CONDITIONAL([ENABLE_PLUGIN_BACKGROUND], [test x$plugin_background = xyes]) + +dnl Cairo Clock plugin +AC_ARG_ENABLE([plugin-cairo-clock], + [AS_HELP_STRING([--enable-plugin-cairo-clock], + [Enable Cairo Clock plugin.])], + [case "${enableval}" in + yes) plugin_cairo_clock=yes ;; + no) plugin_cairo_clock=no ;; + *) AC_MSG_ERROR([bad value ${enableval} for --enable-plugin-cairo-clock]) ;; + esac], + [plugin_cairo_clock=yes]) +AM_CONDITIONAL([ENABLE_PLUGIN_CAIRO_CLOCK], [test x$plugin_cairo_clock = xyes]) + +dnl Run App plugin +AC_ARG_ENABLE([plugin-rundapp], + [AS_HELP_STRING([--enable-plugin-rundapp], + [Enable Run App plugin.])], + [case "${enableval}" in + yes) plugin_rundapp=yes ;; + no) plugin_rundapp=no ;; + *) AC_MSG_ERROR([bad value ${enableval} for --enable-plugin-rundapp]) ;; + esac], + [plugin_rundapp=yes]) +AM_CONDITIONAL([ENABLE_PLUGIN_RUNDAPP], [test x$plugin_cairo_clock = xyes]) + +dnl Clock plugin +AC_ARG_ENABLE([plugin-clock], + [AS_HELP_STRING([--enable-plugin-clock], + [Enable Simple Clock plugin (plugin used in website example).])], + [case "${enableval}" in + yes) plugin_clock=yes ;; + no) plugin_clock=no ;; + *) AC_MSG_ERROR([bad value ${enableval} for --enable-plugin-clock]) ;; + esac], + [plugin_clock=yes]) +AM_CONDITIONAL([ENABLE_PLUGIN_CLOCK], [test x$plugin_clock = xyes]) + +dnl Special effects plugin +AC_ARG_ENABLE([plugin-fx], + [AS_HELP_STRING([--enable-plugin-fx], + [Enable Special Effects plugin.])], + [case "${enableval}" in + yes) plugin_fx=yes ;; + no) plugin_fx=no ;; + *) AC_MSG_ERROR([bad value ${enableval} for --enable-plugin-fx]) ;; + esac], + [plugin_fx=yes]) +AM_CONDITIONAL([ENABLE_PLUGIN_FX], [test x$plugin_fx = xyes]) + +dnl G15daemon Server plugin +AC_ARG_ENABLE([plugin-g15daemon-server], + [AS_HELP_STRING([--enable-plugin-g15daemon-server], + [Enable G15Daemon Server plugin (network server with protocol compatible with g15daemon, allows g15daemon scripts to be used when it is not installed).])], + [case "${enableval}" in + yes) plugin_g15daemon_server=yes ;; + no) plugin_g15daemon_server=no ;; + *) AC_MSG_ERROR([bad value ${enableval} for --enable-plugin-g15daemon-server]) ;; + esac], + [plugin_g15daemon_server=yes]) +AM_CONDITIONAL([ENABLE_PLUGIN_G15DAEMON_SERVER], [test x$plugin_g15daemon_server = xyes]) + +dnl Instant Messenger plugin +AC_ARG_ENABLE([plugin-im], + [AS_HELP_STRING([--enable-plugin-im], + [Enable Instant Messenger plugin. Displays current contact list and status (currently works with Telepathy framework based clients).])], + [case "${enableval}" in + yes) plugin_im=yes ;; + no) plugin_im=no ;; + *) AC_MSG_ERROR([bad value ${enableval} for --enable-plugin-im]) ;; + esac], + [plugin_im=${have_telepathy}]) +AS_IF([test "x${plugin_im}" = "xyes" -a "x$have_telepathy" = "xno"], + [AC_MSG_ERROR([Plugin Instant Messenger cannot be built without telepathy python module])]) +AM_CONDITIONAL([ENABLE_PLUGIN_IM], [test x$plugin_im = xyes]) + +dnl Macro Recorder plugin +AC_ARG_ENABLE([plugin-macro-recorder], + [AS_HELP_STRING([--enable-plugin-macro-recorder], + [Enable Macro Recorder plugin.])], + [case "${enableval}" in + yes) plugin_macro_recorder=yes ;; + no) plugin_macro_recorder=no ;; + *) AC_MSG_ERROR([bad value ${enableval} for --enable-plugin-macro-recorder]) ;; + esac], + [plugin_macro_recorder=yes]) +AM_CONDITIONAL([ENABLE_PLUGIN_MACRO_RECORDER], [test x$plugin_macro_recorder = xyes]) + +dnl Macro Display plugin +AC_ARG_ENABLE([plugin-macros], + [AS_HELP_STRING([--enable-plugin-macros], + [Enable Macro Display plugin.])], + [case "${enableval}" in + yes) plugin_macros=yes ;; + no) plugin_macros=no ;; + *) AC_MSG_ERROR([bad value ${enableval} for --enable-plugin-macros]) ;; + esac], + [plugin_macros=yes]) +AM_CONDITIONAL([ENABLE_PLUGIN_MACROS], [test x$plugin_macros = xyes]) + +dnl Profile selection plugin +AC_ARG_ENABLE([plugin-profiles], + [AS_HELP_STRING([--enable-plugin-profiles], + [Enable profile selector plugin.])], + [case "${enableval}" in + yes) plugin_profiles=yes ;; + no) plugin_profiles=no ;; + *) AC_MSG_ERROR([bad value ${enableval} for --enable-plugin-profiles]) ;; + esac], + [plugin_profiles=yes]) +AM_CONDITIONAL([ENABLE_PLUGIN_PROFILES], [test x$plugin_profiles = xyes]) + +dnl Menu plugin +AC_ARG_ENABLE([plugin-menu], + [AS_HELP_STRING([--enable-plugin-menu], + [Enable Menu plugin.])], + [case "${enableval}" in + yes) plugin_menu=yes ;; + no) plugin_menu=no ;; + *) AC_MSG_ERROR([bad value ${enableval} for --enable-plugin-menu]) ;; + esac], + [plugin_menu=yes]) +AM_CONDITIONAL([ENABLE_PLUGIN_MENU], [test x$plugin_menu = xyes]) + +dnl Mounts plugin +AC_ARG_ENABLE([plugin-mounts], + [AS_HELP_STRING([--enable-plugin-mounts], + [Enable Mounts plugin.])], + [case "${enableval}" in + yes) plugin_mounts=yes ;; + no) plugin_mounts=no ;; + *) AC_MSG_ERROR([bad value ${enableval} for --enable-plugin-mounts]) ;; + esac], + [plugin_mounts=yes]) +AM_CONDITIONAL([ENABLE_PLUGIN_MOUNTS], [test x$plugin_mounts = xyes]) + +dnl MPRIS plugin +AC_ARG_ENABLE([plugin-mpris], + [AS_HELP_STRING([--enable-plugin-mpris], + [Enable MPRIS plugin. Displays currently playing media players that support MPRIS])], + [case "${enableval}" in + yes) plugin_mpris=yes ;; + no) plugin_mpris=no ;; + *) AC_MSG_ERROR([bad value ${enableval} for --enable-plugin-mpris]) ;; + esac], + [plugin_mpris=yes]) +AM_CONDITIONAL([ENABLE_PLUGIN_MPRIS], [test x$plugin_mpris = xyes]) + +dnl Notify plugin +AC_ARG_ENABLE([plugin-notify-lcd], + [AS_HELP_STRING([--enable-plugin-notify-lcd], + [Enable Notify LCD plugin. Takes over as notification daemon and displays messages on LCD, blinks keyboard])], + [case "${enableval}" in + yes) plugin_notify_lcd=yes ;; + no) plugin_notify_lcd=no ;; + *) AC_MSG_ERROR([bad value ${enableval} for --enable-plugin-notify-lcd]) ;; + esac], + [plugin_notify_lcd=yes]) +AM_CONDITIONAL([ENABLE_PLUGIN_NOTIFY_LCD], [test x$plugin_notify_lcd = xyes]) + +dnl Panel plugin +AC_ARG_ENABLE([plugin-panel], + [AS_HELP_STRING([--enable-plugin-panel], + [Enable Panel plugin. Reserves area of screen for other plugins to display permanent information])], + [case "${enableval}" in + yes) plugin_panel=yes ;; + no) plugin_panel=no ;; + *) AC_MSG_ERROR([bad value ${enableval} for --enable-plugin-panel]) ;; + esac], + [plugin_panel=yes]) +AM_CONDITIONAL([ENABLE_PLUGIN_PANEL], [test x$plugin_panel = xyes]) + +dnl Screensaver plugin +AC_ARG_ENABLE([plugin-screensaver], + [AS_HELP_STRING([--enable-plugin-screensaver], + [Enable Screensaver plugin. Displays mesage and dims keyboard when desktop screesaver is activated])], + [case "${enableval}" in + yes) plugin_screensaver=yes ;; + no) plugin_screensaver=no ;; + *) AC_MSG_ERROR([bad value ${enableval} for --enable-plugin-screensaver]) ;; + esac], + [plugin_screensaver=yes]) +AM_CONDITIONAL([ENABLE_PLUGIN_SCREENSAVER], [test x$plugin_screensaver = xyes]) + +dnl Stopwatch plugin +AC_ARG_ENABLE([plugin-stopwatch], + [AS_HELP_STRING([--enable-plugin-stopwatch], + [Enable Stopwatch plugin. Dual mode, dual timer stopwatch])], + [case "${enableval}" in + yes) plugin_stopwatch=yes ;; + no) plugin_stopwatch=no ;; + *) AC_MSG_ERROR([bad value ${enableval} for --enable-plugin-stopwatch]) ;; + esac], + [plugin_stopwatch=yes]) +AM_CONDITIONAL([ENABLE_PLUGIN_STOPWATCH], [test x$plugin_stopwatch = xyes]) + +dnl Media Player plugin +AC_ARG_ENABLE([plugin-mediaplayer], + [AS_HELP_STRING([--enable-plugin-mediaplayer], + [Enable Media Player plugin. Requires GStreamer])], + [case "${enableval}" in + yes) plugin_mediaplayer=yes ;; + no) plugin_mediaplayer=no ;; + *) AC_MSG_ERROR([bad value ${enableval} for --enable-plugin-mediaplayer]) ;; + esac], + [plugin_mediaplayer=${have_gst}]) +AS_IF([test "x${plugin_mediaplayer}" = "xyes" -a "x$have_gst" = "xno"], + [AC_MSG_ERROR([Plugin Mediaplayer cannot be built without gst python module])]) +AM_CONDITIONAL([ENABLE_PLUGIN_MEDIAPLAYER], [test x$plugin_mediaplayer = xyes]) + +dnl Weather plugin +AC_ARG_ENABLE([plugin-weather], + [AS_HELP_STRING([--enable-plugin-weather], + [Enable Weather plugin. Requires additional backend plugin such as weather-noaa])], + [case "${enableval}" in + yes) plugin_weather=yes ;; + no) plugin_weather=no ;; + *) AC_MSG_ERROR([bad value ${enableval} for --enable-plugin-weather]) ;; + esac], + [plugin_weather=yes]) +AM_CONDITIONAL([ENABLE_PLUGIN_WEATHER], [test x$plugin_weather = xyes]) + +AC_ARG_ENABLE([plugin-weather-noaa], + [AS_HELP_STRING([--enable-plugin-weather-noaa], + [Enable NOAA support for the Weather plugin.])], + [case "${enableval}" in + yes) plugin_weather_noaa=yes ;; + no) plugin_weather_noaa=no ;; + *) AC_MSG_ERROR([bad value ${enableval} for --enable-plugin-weather-noaa]) ;; + esac], + [plugin_weather_noaa=${plugin_weather}]) +AS_IF([test "x${plugin_weather_noaa}" = "xyes" -a "x$plugin_weather" = "xno"], + [AC_MSG_ERROR([Weather NOAA backend plugin cannot be built without Weather plugin])]) +AM_CONDITIONAL([ENABLE_PLUGIN_WEATHER_NOAA], [test x$plugin_weather_noaa = xyes]) + +AC_ARG_ENABLE([plugin-weather-yahoo], + [AS_HELP_STRING([--enable-plugin-weather-yahoo], + [Enable Yahoo support for the Weather plugin.])], + [case "${enableval}" in + yes) plugin_weather_yahoo=yes ;; + no) plugin_weather_yahoo=no ;; + *) AC_MSG_ERROR([bad value ${enableval} for --enable-plugin-weather-yahoo]) ;; + esac], + [plugin_weather_yahoo=${plugin_weather}]) +AS_IF([test "x${plugin_weather_yahoo}" = "xyes" -a "x$plugin_weather" = "xno"], + [AC_MSG_ERROR([Weather Yahoo backend plugin cannot be built without Weather plugin])]) +AM_CONDITIONAL([ENABLE_PLUGIN_WEATHER_YAHOO], [test x$plugin_weather_yahoo = xyes]) + +dnl Indicator Messages Plugin +AC_ARG_ENABLE([plugin-indicator-messages], + [AS_HELP_STRING([--enable-plugin-indicator-messages], + [Enable Indicator Messages plugin.])], + [case "${enableval}" in + yes) plugin_indicator_messages=yes ;; + no) plugin_indicator_messages=no ;; + *) AC_MSG_ERROR([bad value ${enableval} for --enable-plugin-indicator-messages]) ;; + esac], + [plugin_indicator_messages=${have_appindicator}]) +AS_IF([test "x${plugin_indicator_messages}" = "xyes" -a "x$have_appindicator" = "xno"], + [AC_MSG_ERROR([Plugin Indicator Messages cannot be built without appindicator python module])]) +AM_CONDITIONAL([ENABLE_PLUGIN_INDICATOR_MESSAGES], [test x$plugin_indicator_messages = xyes]) + +dnl ALSA Volume Monitor Plugin +AC_ARG_ENABLE([plugin-volume], + [AS_HELP_STRING([--enable-volume], + [Enable ALSA volume monitor plugin. Requires python-alsaaudio])], + [case "${enableval}" in + yes) plugin_volume=yes ;; + no) plugin_volume=no ;; + *) AC_MSG_ERROR([bad value ${enableval} for --enable-plugin-volume]) ;; + esac], + [plugin_volume=${have_alsaaudio}]) +AS_IF([test "x${plugin_volume}" = "xyes" -a "x$have_alsaaudio" = "xno"], + [AC_MSG_ERROR([Plugin Volume cannot be built without alsaaudio python module])]) +AM_CONDITIONAL([ENABLE_PLUGIN_VOLUME], [test x$plugin_volume = xyes]) + +dnl RSS Plugin +AC_ARG_ENABLE([plugin-rss], + [AS_HELP_STRING([--enable-plugin-rss], + [Enable RSS feed plugin. Requires python feedparser])], + [case "${enableval}" in + yes) plugin_rss=yes ;; + no) plugin_rss=no ;; + *) AC_MSG_ERROR([bad value ${enableval} for --enable-plugin-rss]) ;; + esac], + [plugin_rss=${have_feedparser}]) +AS_IF([test "x${plugin_rss}" = "xyes" -a "x$have_feedparser" = "xno"], + [AC_MSG_ERROR([Plugin RSS cannot be built without feedparser python module])]) +AM_CONDITIONAL([ENABLE_PLUGIN_RSS], [test x$plugin_rss = xyes]) + +dnl System Monitor Plugin +AC_ARG_ENABLE([plugin-sysmon], + [AS_HELP_STRING([--enable-plugin-sysmon], + [Enable System Monitor plugin. Recommends python gtop])], + [case "${enableval}" in + yes) plugin_sysmon=yes ;; + no) plugin_sysmon=no ;; + *) AC_MSG_ERROR([bad value ${enableval} for --enable-plugin-sysmon]) ;; + esac], + [plugin_sysmon=yes]) +AM_CONDITIONAL([ENABLE_PLUGIN_SYSMON], [test x$plugin_sysmon = xyes]) + +dnl Processes Plugin +AC_ARG_ENABLE([plugin-processes], + [AS_HELP_STRING([--enable-plugin-processes], + [Enable Processes plugin. Recommends python gtop])], + [case "${enableval}" in + yes) plugin_processes=yes ;; + no) plugin_processes=no ;; + *) AC_MSG_ERROR([bad value ${enableval} for --enable-plugin-processes]) ;; + esac], + [plugin_processes=yes]) +AM_CONDITIONAL([ENABLE_PLUGIN_PROCESSES], [test x$plugin_processes = xyes]) + +dnl Debug Plugin +AC_ARG_ENABLE([plugin-debug], + [AS_HELP_STRING([--enable-plugin-debug], + [Enable Debug plugin.])], + [case "${enableval}" in + yes) plugin_debug=yes ;; + no) plugin_debug=no ;; + *) AC_MSG_ERROR([bad value ${enableval} for --enable-plugin-debug]) ;; + esac], + [plugin_debug=no]) +AM_CONDITIONAL([ENABLE_PLUGIN_DEBUG], [test x$plugin_debug = xyes]) + +dnl Calendar plugin (base) +AC_ARG_ENABLE([plugin-cal], + [AS_HELP_STRING([--enable-plugin-cal], + [Enable calendar plugin. (required for any calendar support)])], + [case "${enableval}" in + yes) plugin_cal=yes ;; + no) plugin_cal=no ;; + *) AC_MSG_ERROR([bad value ${enableval} for --enable-plugin-cal]) ;; + esac], + [plugin_cal=yes]) +AM_CONDITIONAL([ENABLE_PLUGIN_CAL], [test x$plugin_cal = xyes]) + +dnl Evolution Calendar plugin +AS_IF([test "x${plugin_cal}" = "xyes" -a "x${have_vobject}" = "xyes" ], + [deps_plugin_cal_evolution=yes], + [deps_plugin_cal_evolution=no]) +AC_ARG_ENABLE([plugin-cal-evolution], + [AS_HELP_STRING([--enable-plugin-cal-evolution], + [Enable Evolution calendar plugin. Requires python vobject])], + [case "${enableval}" in + yes) plugin_cal_evolution=yes ;; + no) plugin_cal_evolution=no ;; + *) AC_MSG_ERROR([bad value ${enableval} for --enable-plugin-cal-evolution]) ;; + esac], + [plugin_cal_evolution=${deps_plugin_cal_evolution}]) +AS_IF([test "x${plugin_cal_evolution}" = "xyes" -a "x$plugin_cal" = "xno"], + [AC_MSG_ERROR([Plugin Evolution calendar cannot be built without Calendar plugin])]) +AS_IF([test "x${plugin_cal_evolution}" = "xyes" -a "x$have_vobject" = "xno"], + [AC_MSG_ERROR([Plugin Evolution calendar cannot be built without vobject python module])]) +AM_CONDITIONAL([ENABLE_PLUGIN_CAL_EVOLUTION], [test x$plugin_cal_evolution = xyes]) + +dnl Google Calendar plugin +AS_IF([test "x${plugin_cal}" = "xyes" -a "x${have_gdata_calendar}" = "xyes" ], + [deps_plugin_cal_google=yes], + [deps_plugin_cal_google=no]) +AC_ARG_ENABLE([plugin-cal-google], + [AS_HELP_STRING([--enable-plugin-cal-google], + [Enable Google calendar plugin. Requires calendar plugin, python gdata])], + [case "${enableval}" in + yes) plugin_cal_google=yes ;; + no) plugin_cal_google=no ;; + *) AC_MSG_ERROR([bad value ${enableval} for --enable-plugin-cal-google]) ;; + esac], + [plugin_cal_google=${deps_plugin_cal_google}]) +AS_IF([test "x${plugin_cal_google}" = "xyes" -a "x$plugin_cal" = "xno"], + [AC_MSG_ERROR([Plugin Google calendar cannot be built without Calendar plugin])]) +AS_IF([test "x${plugin_cal_google}" = "xyes" -a "x$have_gdata_calendar" = "xno"], + [AC_MSG_ERROR([Plugin Google calendar cannot be built without gdata python modules])]) +AM_CONDITIONAL([ENABLE_PLUGIN_CAL_GOOGLE], [test x$plugin_cal_google = xyes]) + +dnl Google Analytics plugin +AS_IF([test "x${have_gdata_analytics}" = "xyes" -a "x${have_cairoplot}" = "xyes" ], + [deps_plugin_google_analytics=yes], + [deps_plugin_google_analytics=no]) +AC_ARG_ENABLE([plugin-google-analytics], + [AS_HELP_STRING([--enable-plugin-google-analytics], + [Enable Google Analytics plugin. Requires python gdata and cairoplot])], + [case "${enableval}" in + yes) plugin_google_analytics=yes ;; + no) plugin_google_analytics=no ;; + *) AC_MSG_ERROR([bad value ${enableval} for --enable-plugin-google-analytics]) ;; + esac], + [plugin_google_analytics=${deps_plugin_google_analytics}]) +AS_IF([test "x${plugin_google_analytics}" = "xyes" -a "x$have_gdata_analytics" = "xno"], + [AC_MSG_ERROR([Plugin Google analytics cannot be built without gdata python modules])]) +AS_IF([test "x${plugin_google_analytics}" = "xyes" -a "x$have_cairoplot" = "xno"], + [AC_MSG_ERROR([Plugin Google analytics cannot be built without cairoplot python modules])]) +AM_CONDITIONAL([ENABLE_PLUGIN_GOOGLE_ANALYTICS], [test x$plugin_google_analytics = xyes]) + +dnl POP3/IMAP Email Checker plugin +AC_ARG_ENABLE([plugin-lcdbiff], + [AS_HELP_STRING([--enable-plugin-lcdbiff], + [Enable POP3 / IMAP email checker.])], + [case "${enableval}" in + yes) plugin_lcdbiff=yes ;; + no) plugin_lcdbiff=no ;; + *) AC_MSG_ERROR([bad value ${enableval} for --enable-plugin-lcdbiff]) ;; + esac], + [plugin_lcdbiff=yes]) +AM_CONDITIONAL([ENABLE_PLUGIN_LCDBIFF], [test x$plugin_lcdbiff = xyes]) + +dnl Sensors plugin +AC_ARG_ENABLE([plugin-sense], + [AS_HELP_STRING([--enable-plugin-sense], + [Enable Sense plugin. Requires pysensors])], + [case "${enableval}" in + yes) plugin_sense=yes ;; + no) plugin_sense=no ;; + *) AC_MSG_ERROR([bad value ${enableval} for --enable-plugin-sense]) ;; + esac], + [plugin_sense=${have_sensors}]) +AS_IF([test "x${plugin_sense}" = "xyes" -a "x$have_sensors" = "xno"], + [AC_MSG_ERROR([Plugin Sense cannot be built without sensors python modules])]) +AM_CONDITIONAL([ENABLE_PLUGIN_SENSE], [test x$plugin_sense = xyes]) + +dnl LCDShot plugin +AC_ARG_ENABLE([plugin-lcdshot], + [AS_HELP_STRING([--enable-plugin-lcdshot], + [Enable LCDShot plugin. Take a picture of whatever is on the LCD])], + [case "${enableval}" in + yes) plugin_lcdshot=yes ;; + no) plugin_lcdshot=no ;; + *) AC_MSG_ERROR([bad value ${enableval} for --enable-plugin-lcdshot]) ;; + esac], + [plugin_lcdshot=yes]) +AM_CONDITIONAL([ENABLE_PLUGIN_LCDSHOT], [test x$plugin_lcdshot = xyes]) + +dnl Tweak plugin +AC_ARG_ENABLE([plugin-tweak], + [AS_HELP_STRING([--enable-plugin-tweak], + [Enable Tweak plugin.])], + [case "${enableval}" in + yes) plugin_tweak=yes ;; + no) plugin_tweak=no ;; + *) AC_MSG_ERROR([bad value ${enableval} for --enable-plugin-tweak]) ;; + esac], + [plugin_tweak=yes]) +AM_CONDITIONAL([ENABLE_PLUGIN_TWEAK], [test x$plugin_tweak = xyes]) + +dnl Tails plugin +AC_ARG_ENABLE([plugin-tails], + [AS_HELP_STRING([--enable-plugin-tails], + [Enable Tails.])], + [case "${enableval}" in + yes) plugin_tails=yes ;; + no) plugin_tails=no ;; + *) AC_MSG_ERROR([bad value ${enableval} for --enable-plugin-tails]) ;; + esac], + [plugin_tails=yes]) +AM_CONDITIONAL([ENABLE_PLUGIN_TAILS], [test x$plugin_tails = xyes]) + +dnl Display plugin +AC_ARG_ENABLE([plugin-display], + [AS_HELP_STRING([--enable-plugin-display], + [Enable Display (XRandR for resolutions and rotation).])], + [case "${enableval}" in + yes) plugin_display=yes ;; + no) plugin_display=no ;; + *) AC_MSG_ERROR([bad value ${enableval} for --enable-plugin-display]) ;; + esac], + [plugin_display=yes]) +AM_CONDITIONAL([ENABLE_PLUGIN_DISPLAY], [test x$plugin_display = xyes]) + +dnl Voip +AC_ARG_ENABLE([plugin-voip], + [AS_HELP_STRING([--enable-plugin-voip], + [Enable Voip plugin. Integrate with Voip apps. Requires backend plugin as well (e.g. voip-teamspeak3)])], + [case "${enableval}" in + yes) plugin_voip=yes ;; + no) plugin_voip=no ;; + *) AC_MSG_ERROR([bad value ${enableval} for --enable-plugin-voip]) ;; + esac], + [plugin_voip=yes]) +AM_CONDITIONAL([ENABLE_PLUGIN_VOIP], [test x$plugin_voip = xyes]) + +AC_ARG_ENABLE([plugin-voip-teamspeak3], + [AS_HELP_STRING([--enable-plugin-voip-teamspeak3], + [Enable Teamspeak3 plugin. Requires Voip plugin as well])], + [case "${enableval}" in + yes) plugin_voip_teamspeak3=yes ;; + no) plugin_voip_teamspeak3=no ;; + *) AC_MSG_ERROR([bad value ${enableval} for --enable-plugin-voip-teamspeak3]) ;; + esac], + [plugin_voip_teamspeak3=${plugin_voip}]) +AS_IF([test "x${plugin_voip_teamspeak3}" = "xyes" -a "x$plugin_voip" = "xno"], + [AC_MSG_ERROR([Plugin Teamspeak3 cannot be built without Voip plugin])]) +AM_CONDITIONAL([ENABLE_PLUGIN_VOIP_TEAMSPEAK3], [test x$plugin_voip_teamspeak3 = xyes]) + +dnl Traffic Stats Plugin +AC_ARG_ENABLE([plugin-trafficstats], + [AS_HELP_STRING([--enable-plugin-trafficstats], + [Enable Traffic Stats plugin.])], + [case "${enableval}" in + yes) plugin_trafficstats=yes ;; + no) plugin_trafficstats=no ;; + *) AC_MSG_ERROR([bad value ${enableval} for --enable-plugin-trafficstats]) ;; + esac], + [plugin_trafficstats=yes]) +AM_CONDITIONAL([ENABLE_PLUGIN_TRAFFIC_STATS], [test x$plugin_trafficstats = xyes]) + +dnl impulse15 plugin +AS_IF([test "x${have_fftw3}" = "xyes" \ + -a "x${have_pulse}" = "xyes"], + [deps_plugin_impulse15=yes], + [deps_plugin_impulse15=no]) +AC_ARG_ENABLE([plugin-impulse15], + [AS_HELP_STRING([--enable-plugin-impulse15], + [Enable Impulse15 plugin.])], + [case "${enableval}" in + yes) plugin_impulse15=yes ;; + no) plugin_impulse15=no ;; + *) AC_MSG_ERROR([bad value ${enableval} for --enable-plugin-impulse15]) ;; + esac], + [plugin_impulse15=${deps_plugin_impulse15}]) +AS_IF([test "x${plugin_impulse15}" = "xyes" -a "x$have_fftw3" = "xno"], + [AC_MSG_ERROR([Plugin Impulse15 cannot be built without fftw3])]) +AS_IF([test "x${plugin_impulse15}" = "xyes" -a "x$have_pulse" = "xno"], + [AC_MSG_ERROR([Plugin Impulse15 cannot be built without pulse library])]) +AM_CONDITIONAL([ENABLE_PLUGIN_IMPULSE15], [test x$plugin_impulse15 = xyes]) + +dnl Pommodoro Timer plugin +AC_ARG_ENABLE([plugin-pommodoro], + [AS_HELP_STRING([--enable-plugin-pommodoro], + [Enable Pommodoro Timer plugin.])], + [case "${enableval}" in + yes) plugin_pommodoro=yes ;; + no) plugin_pommodoro=no ;; + *) AC_MSG_ERROR([bad value ${enableval} for --enable-plugin-pommodoro]) ;; + esac], + [plugin_pommodoro=yes]) +AM_CONDITIONAL([ENABLE_PLUGIN_POMMODORO], [test x$plugin_pommodoro = xyes]) + +dnl +dnl Experimental (under development) plugins +dnl + +dnl Nexuiz plugin +AC_ARG_ENABLE([plugin-game-nexuiz], + [AS_HELP_STRING([--enable-plugin-game-nexuiz], + [Enable Nexuiz plugin.])], + [case "${enableval}" in + yes) plugin_game_nexuiz=yes ;; + no) plugin_game_nexuiz=no ;; + *) AC_MSG_ERROR([bad value ${enableval} for --enable-plugin-game-nexuiz]) ;; + esac], + [plugin_game_nexuiz=no]) +AM_CONDITIONAL([ENABLE_PLUGIN_GAME_NEXUIZ], [test x$plugin_game_nexuiz = xyes]) + +dnl Backlight plugin +AC_ARG_ENABLE([plugin-backlight], + [AS_HELP_STRING([--enable-plugin-backlight], + [Enable Backlight plugin.])], + [case "${enableval}" in + yes) plugin_backlight=yes ;; + no) plugin_backlight=no ;; + *) AC_MSG_ERROR([bad value ${enableval} for --enable-plugin-backlight]) ;; + esac], + [plugin_backlight=no]) +AM_CONDITIONAL([ENABLE_PLUGIN_BACKLIGHT], [test x$plugin_backlight = xyes]) + +dnl Notify 2 plugin +AC_ARG_ENABLE([plugin-notify-lcd2], + [AS_HELP_STRING([--enable-plugin-notify-lcd2], + [Enable Notify LCD plugin. Takes over as notification daemon and displays messages on LCD, blinks keyboard])], + [case "${enableval}" in + yes) plugin_notify_lcd2=yes ;; + no) plugin_notify_lcd2=no ;; + *) AC_MSG_ERROR([bad value ${enableval} for --enable-plugin-notify-lcd2]) ;; + esac], + [plugin_notify_lcd2=no]) +AM_CONDITIONAL([ENABLE_PLUGIN_NOTIFY_LCD2], [test x$plugin_notify_lcd2 = xyes]) + +dnl PPAStats plugin +AC_ARG_ENABLE([plugin-ppastats], + [AS_HELP_STRING([--enable-plugin-ppastats], + [Enable PPAStats plugin.])], + [case "${enableval}" in + yes) plugin_ppastats=yes ;; + no) plugin_ppastats=no ;; + *) AC_MSG_ERROR([bad value ${enableval} for --enable-plugin-ppastats]) ;; + esac], + [plugin_ppastats=no]) +AM_CONDITIONAL([ENABLE_PLUGIN_PPASTATS], [test x$plugin_ppastats = xyes]) + +dnl NM plugin +AC_ARG_ENABLE([plugin-nm], + [AS_HELP_STRING([--enable-plugin-nm], + [Enable NM (Network Manager) plugin.])], + [case "${enableval}" in + yes) plugin_nm=yes ;; + no) plugin_nm=no ;; + *) AC_MSG_ERROR([bad value ${enableval} for --enable-plugin-nm]) ;; + esac], + [plugin_nm=no]) +AM_CONDITIONAL([ENABLE_PLUGIN_NM], [test x$plugin_nm = xyes]) + +dnl Lens plugin +AC_ARG_ENABLE([plugin-lens], + [AS_HELP_STRING([--enable-plugin-lens], + [Enable Unity Lens plugin.])], + [case "${enableval}" in + yes) plugin_lens=yes ;; + no) plugin_lens=no ;; + *) AC_MSG_ERROR([bad value ${enableval} for --enable-plugin-lens]) ;; + esac], + [plugin_lens=no]) +AM_CONDITIONAL([ENABLE_PLUGIN_LENS], [test x$plugin_lens = xyes]) + +dnl WebKit browser plugin +AC_ARG_ENABLE([plugin-webkit-browser], + [AS_HELP_STRING([--enable-plugin-webkit-browser], + [Enable Webkit browser plugin.])], + [case "${enableval}" in + yes) plugin_webkit_browser=yes ;; + no) plugin_webkit_browser=no ;; + *) AC_MSG_ERROR([bad value ${enableval} for --enable-plugin-webkit-browser]) ;; + esac], + [plugin_webkit_browser=no]) +AM_CONDITIONAL([ENABLE_PLUGIN_WEBKIT_BROWSER], [test x$plugin_webkit_browser = xyes]) + +dnl Things plugin +AC_ARG_ENABLE([plugin-things], + [AS_HELP_STRING([--enable-plugin-things], + [Enable Things python animation API plugin.])], + [case "${enableval}" in + yes) plugin_things=yes ;; + no) plugin_things=no ;; + *) AC_MSG_ERROR([bad value ${enableval} for --enable-plugin-things]) ;; + esac], + [plugin_things=no]) +AM_CONDITIONAL([ENABLE_PLUGIN_THINGS], [test x$plugin_things = xyes]) + +AC_OUTPUT([ +Makefile +data/Makefile +data/applications/Makefile +data/applications/g15-config.desktop +data/autostart/Makefile +data/autostart/gnome15.desktop +data/autostart/g15-systemtray.desktop +data/autostart/g15-indicator.desktop +data/dbus/Makefile +data/icons/Makefile +data/icons/hicolor/Makefile +data/icons/hicolor/16x16/Makefile +data/icons/hicolor/16x16/status/Makefile +data/icons/hicolor/22x22/Makefile +data/icons/hicolor/22x22/apps/Makefile +data/icons/hicolor/22x22/status/Makefile +data/icons/hicolor/24x24/Makefile +data/icons/hicolor/24x24/apps/Makefile +data/icons/hicolor/24x24/status/Makefile +data/icons/hicolor/48x48/Makefile +data/icons/hicolor/48x48/apps/Makefile +data/icons/hicolor/64x64/Makefile +data/icons/hicolor/64x64/apps/Makefile +data/icons/hicolor/scalable/Makefile +data/icons/hicolor/scalable/apps/Makefile +data/icons/hicolor/scalable/status/Makefile +data/icons/hicolor/scalable/devices/Makefile +data/icons/AwOken/Makefile +data/icons/AwOken/status/Makefile +data/icons/AwOken/status/16/Makefile +data/icons/AwOken/status/22/Makefile +data/icons/AwOken/status/24/Makefile +data/icons/AwOken/status/48/Makefile +data/icons/AwOken/status/64/Makefile +data/icons/AwOken/status/128/Makefile +data/icons/AwOken/apps/Makefile +data/icons/AwOken/apps/16/Makefile +data/icons/AwOken/apps/22/Makefile +data/icons/AwOken/apps/24/Makefile +data/icons/AwOken/apps/48/Makefile +data/icons/AwOken/apps/64/Makefile +data/icons/AwOken/apps/128/Makefile +data/icons/ubuntu-mono-dark/Makefile +data/icons/ubuntu-mono-dark/status/Makefile +data/icons/ubuntu-mono-dark/status/16/Makefile +data/icons/ubuntu-mono-dark/status/22/Makefile +data/icons/ubuntu-mono-dark/status/24/Makefile +data/icons/ubuntu-mono-light/Makefile +data/icons/ubuntu-mono-light/status/Makefile +data/icons/ubuntu-mono-light/status/16/Makefile +data/icons/ubuntu-mono-light/status/22/Makefile +data/icons/ubuntu-mono-light/status/24/Makefile +data/images/Makefile +data/themes/Makefile +data/themes/default/Makefile +data/udev/Makefile +data/udev/98-gnome15.rules +data/udev/99-gnome15-kernel.rules +data/udev/99-gnome15-g15direct.rules +data/udev/99-gnome15-g19direct.rules +data/udev/99-gnome15-g930.rules +data/ukeys/Makefile +data/ui/Makefile +i18n/Makefile +man/Makefile +src/Makefile +src/pylibg19/Makefile +src/pylibg19/g19/Makefile +src/libimpulse/Makefile +src/gnome15/Makefile +src/gnome15/g15globals.py +src/gnome15/drivers/Makefile +src/gnome15/util/Makefile +src/scripts/Makefile +src/gnome-shell-extension/Makefile +src/gnome-shell-extension/icons/Makefile +src/plugins/Makefile +src/plugins/cal/Makefile +src/plugins/cal/default/Makefile +src/plugins/cal-evolution/Makefile +src/plugins/cal-google/Makefile +src/plugins/lcdbiff/Makefile +src/plugins/lcdbiff/default/Makefile +src/plugins/debug/Makefile +src/plugins/debug/default/Makefile +src/plugins/background/Makefile +src/plugins/cairo-clock/Makefile +src/plugins/cairo-clock/g15/Makefile +src/plugins/cairo-clock/g15/default/Makefile +src/plugins/cairo-clock/g19/Makefile +src/plugins/cairo-clock/g19/default/Makefile +src/plugins/cairo-clock/mx5500/Makefile +src/plugins/cairo-clock/mx5500/default/Makefile +src/plugins/clock/Makefile +src/plugins/clock/default/Makefile +src/plugins/fx/Makefile +src/plugins/g15daemon-server/Makefile +src/plugins/macro-recorder/Makefile +src/plugins/macro-recorder/default/Makefile +src/plugins/macros/Makefile +src/plugins/macros/default/Makefile +src/plugins/mpris/Makefile +src/plugins/mpris/default/Makefile +src/plugins/mpris/bigcover/Makefile +src/plugins/mounts/Makefile +src/plugins/mounts/default/Makefile +src/plugins/runapp/Makefile +src/plugins/menu/Makefile +src/plugins/panel/Makefile +src/plugins/pommodoro/Makefile +src/plugins/pommodoro/default/Makefile +src/plugins/profiles/Makefile +src/plugins/processes/Makefile +src/plugins/im/Makefile +src/plugins/indicator-messages/Makefile +src/plugins/lcdshot/Makefile +src/plugins/notify-lcd/Makefile +src/plugins/notify-lcd/default/Makefile +src/plugins/screensaver/Makefile +src/plugins/screensaver/default/Makefile +src/plugins/stopwatch/Makefile +src/plugins/stopwatch/default/Makefile +src/plugins/sense/Makefile +src/plugins/sense/default/Makefile +src/plugins/sysmon/Makefile +src/plugins/sysmon/default/Makefile +src/plugins/sysmon/graphs/Makefile +src/plugins/rss/Makefile +src/plugins/rss/default/Makefile +src/plugins/sysmon/dials/Makefile +src/plugins/tweak/Makefile +src/plugins/volume/Makefile +src/plugins/volume/default/Makefile +src/plugins/mediaplayer/Makefile +src/plugins/mediaplayer/default/Makefile +src/plugins/weather/Makefile +src/plugins/weather/default/Makefile +src/plugins/weather/forecasts/Makefile +src/plugins/weather-noaa/Makefile +src/plugins/weather-yahoo/Makefile +src/plugins/tails/Makefile +src/plugins/tails/tailer/Makefile +src/plugins/tails/default/Makefile +src/plugins/display/Makefile +src/plugins/voip/Makefile +src/plugins/voip/default/Makefile +src/plugins/voip-teamspeak3/Makefile +src/plugins/voip-teamspeak3/ts3/Makefile +src/plugins/google-analytics/Makefile +src/plugins/google-analytics/default/Makefile +src/plugins/trafficstats/Makefile +src/plugins/trafficstats/default/Makefile +src/plugins/impulse15/Makefile +src/plugins/impulse15/themes/Makefile +src/plugins/impulse15/themes/default/Makefile +src/plugins/impulse15/themes/circlelcd/Makefile +src/plugins/impulse15/themes/circleline/Makefile +src/plugins/impulse15/themes/original/Makefile +src/plugins/game-nexuiz/Makefile +src/plugins/game-nexuiz/default/Makefile +src/plugins/game-nexuiz/resources/Makefile +src/plugins/backlight/Makefile +src/plugins/backlight/default/Makefile +src/plugins/notify-lcd2/Makefile +src/plugins/notify-lcd2/default/Makefile +src/plugins/ppastats/Makefile +src/plugins/ppastats/default/Makefile +src/plugins/nm/Makefile +src/plugins/nm/default/Makefile +src/plugins/lens/Makefile +src/plugins/webkitbrowser/Makefile +src/plugins/webkitbrowser/default/Makefile +src/plugins/things/Makefile +src/plugins/things/cg.stuff/Makefile +src/plugins/things/clouds.stuff/Makefile +]) + +AS_ECHO("Available features :-") +AS_ECHO("") +AS_ECHO("Panel Integration") +AS_ECHO("-----------------") +AS_ECHO_N("systemtray - ") +AS_IF([test "x$systemtray" = "xyes" ], + [AS_ECHO("Enabled")], + [AS_ECHO("Disabled")]) +AS_ECHO_N("Indicator - ") +AS_IF([test "x$indicator" = "xyes" ], + [AS_ECHO("Enabled")], + [AS_ECHO("Disabled")]) +AS_ECHO_N("gnome-shell-extension - ") +AS_IF([test "x$gnome_shell_extension" = "xyes" ], + [AS_ECHO("Enabled")], + [AS_ECHO("Disabled")]) +AS_ECHO("") +AS_ECHO("Drivers") +AS_ECHO("-------") +AS_ECHO_N("g15direct - ") +AS_IF([test "x$driver_g15direct" = "xyes" ], + [AS_ECHO("Enabled")], + [AS_ECHO("Disabled")]) +AS_ECHO_N("g930 - ") +AS_IF([test "x$driver_g930" = "xyes" ], + [AS_ECHO("Enabled")], + [AS_ECHO("Disabled")]) +AS_ECHO_N("g19direct - ") +AS_IF([test "x$driver_g19direct" = "xyes" ], + [AS_ECHO("Enabled")], + [AS_ECHO("Disabled")]) +AS_ECHO_N("Kernel - ") +AS_IF([test "x$driver_kernel" = "xyes" ], + [AS_ECHO("Enabled")], + [AS_ECHO("Disabled")]) +AS_ECHO("") +AS_ECHO("Icons") +AS_ECHO("-----") +AS_ECHO_N("icons-awoken - ") +AS_IF([test "x$icons_awoken" = "xyes" ], + [AS_ECHO("Enabled")], + [AS_ECHO("Disabled")]) +AS_ECHO_N("icons-mono - ") +AS_IF([test "x$icons_mono" = "xyes" ], + [AS_ECHO("Enabled")], + [AS_ECHO("Disabled")]) +AS_ECHO("") +AS_ECHO("Enabled Plugins") +AS_ECHO("---------------") +AS_ECHO_N("volume - ") +AS_IF([test "x$plugin_volume" = "xyes" ], + [AS_ECHO("Enabled")], + [AS_ECHO("Disabled")]) +AS_ECHO_N("rss - ") +AS_IF([test "x$plugin_rss" = "xyes" ], + [AS_ECHO("Enabled")], + [AS_ECHO("Disabled")]) +AS_ECHO_N("processes - ") +AS_IF([test "x$plugin_processes" = "xyes" ], + [AS_ECHO("Enabled")], + [AS_ECHO("Disabled")]) +AS_ECHO_N("sysmon - ") +AS_IF([test "x$plugin_sysmon" = "xyes" ], + [AS_ECHO("Enabled")], + [AS_ECHO("Disabled")]) +AS_ECHO_N("cal - ") +AS_IF([test "x$plugin_cal" = "xyes" ], + [AS_ECHO("Enabled")], + [AS_ECHO("Disabled")]) +AS_ECHO_N("cal-evolution - ") +AS_IF([test "x$plugin_cal_evolution" = "xyes" ], + [AS_ECHO("Enabled")], + [AS_ECHO("Disabled")]) +AS_ECHO_N("cal-google - ") +AS_IF([test "x$plugin_cal_google" = "xyes" ], + [AS_ECHO("Enabled")], + [AS_ECHO("Disabled")]) +AS_ECHO_N("lcdbiff - ") +AS_IF([test "x$plugin_lcdbiff" = "xyes" ], + [AS_ECHO("Enabled")], + [AS_ECHO("Disabled")]) +AS_ECHO_N("background - ") +AS_IF([test "x$plugin_background" = "xyes" ], + [AS_ECHO("Enabled")], + [AS_ECHO("Disabled")]) +AS_ECHO_N("cairo-clock - ") +AS_IF([test "x$plugin_cairo_clock" = "xyes" ], + [AS_ECHO("Enabled")], + [AS_ECHO("Disabled")]) +AS_ECHO_N("clock - ") +AS_IF([test "x$plugin_clock" = "xyes" ], + [AS_ECHO("Enabled")], + [AS_ECHO("Disabled")]) +AS_ECHO_N("fx - ") +AS_IF([test "x$plugin_fx" = "xyes" ], + [AS_ECHO("Enabled")], + [AS_ECHO("Disabled")]) +AS_ECHO_N("g15daemon-server - ") +AS_IF([test "x$plugin_g15daemon_server" = "xyes" ], + [AS_ECHO("Enabled")], + [AS_ECHO("Disabled")]) +AS_ECHO_N("im - ") +AS_IF([test "x$plugin_im" = "xyes" ], + [AS_ECHO("Enabled")], + [AS_ECHO("Disabled")]) +AS_ECHO_N("indicator-messages - ") +AS_IF([test "x$plugin_indicator_messages" = "xyes" ], + [AS_ECHO("Enabled")], + [AS_ECHO("Disabled")]) +AS_ECHO_N("lcdshot - ") +AS_IF([test "x$plugin_lcdshot" = "xyes" ], + [AS_ECHO("Enabled")], + [AS_ECHO("Disabled")]) +AS_ECHO_N("macro-recorder - ") +AS_IF([test "x$plugin_macro_recorder" = "xyes" ], + [AS_ECHO("Enabled")], + [AS_ECHO("Disabled")]) +AS_ECHO_N("macros - ") +AS_IF([test "x$plugin_macros" = "xyes" ], + [AS_ECHO("Enabled")], + [AS_ECHO("Disabled")]) +AS_ECHO_N("profiles - ") +AS_IF([test "x$plugin_profiles" = "xyes" ], + [AS_ECHO("Enabled")], + [AS_ECHO("Disabled")]) +AS_ECHO_N("menu - ") +AS_IF([test "x$plugin_menu" = "xyes" ], + [AS_ECHO("Enabled")], + [AS_ECHO("Disabled")]) +AS_ECHO_N("mounts - ") +AS_IF([test "x$plugin_mounts" = "xyes" ], + [AS_ECHO("Enabled")], + [AS_ECHO("Disabled")]) +AS_ECHO_N("mpris - ") +AS_IF([test "x$plugin_mpris" = "xyes" ], + [AS_ECHO("Enabled")], + [AS_ECHO("Disabled")]) +AS_ECHO_N("notify-lcd - ") +AS_IF([test "x$plugin_notify_lcd" = "xyes" ], + [AS_ECHO("Enabled")], + [AS_ECHO("Disabled")]) +AS_ECHO_N("panel - ") +AS_IF([test "x$plugin_panel" = "xyes" ], + [AS_ECHO("Enabled")], + [AS_ECHO("Disabled")]) +AS_ECHO_N("screensaver - ") +AS_IF([test "x$plugin_screensaver" = "xyes" ], + [AS_ECHO("Enabled")], + [AS_ECHO("Disabled")]) +AS_ECHO_N("stopwatch - ") +AS_IF([test "x$plugin_stopwatch" = "xyes" ], + [AS_ECHO("Enabled")], + [AS_ECHO("Disabled")]) +AS_ECHO_N("tweak - ") +AS_IF([test "x$plugin_tweak" = "xyes" ], + [AS_ECHO("Enabled")], + [AS_ECHO("Disabled")]) +AS_ECHO_N("mediaplayer - ") +AS_IF([test "x$plugin_mediaplayer" = "xyes" ], + [AS_ECHO("Enabled")], + [AS_ECHO("Disabled")]) +AS_ECHO_N("weather - ") +AS_IF([test "x$plugin_weather" = "xyes" ], + [AS_ECHO("Enabled")], + [AS_ECHO("Disabled")]) +AS_ECHO_N("weather-noaa - ") +AS_IF([test "x$plugin_weather_noaa" = "xyes" ], + [AS_ECHO("Enabled")], + [AS_ECHO("Disabled")]) +AS_ECHO_N("weather-yahoo - ") +AS_IF([test "x$plugin_weather_yahoo" = "xyes" ], + [AS_ECHO("Enabled")], + [AS_ECHO("Disabled")]) +AS_ECHO_N("sense - ") +AS_IF([test "x$plugin_sense" = "xyes" ], + [AS_ECHO("Enabled")], + [AS_ECHO("Disabled")]) +AS_ECHO_N("tails - ") +AS_IF([test "x$plugin_tails" = "xyes" ], + [AS_ECHO("Enabled")], + [AS_ECHO("Disabled")]) +AS_ECHO_N("display - ") +AS_IF([test "x$plugin_display" = "xyes" ], + [AS_ECHO("Enabled")], + [AS_ECHO("Disabled")]) +AS_ECHO_N("voip - ") +AS_IF([test "x$plugin_voip" = "xyes" ], + [AS_ECHO("Enabled")], + [AS_ECHO("Disabled")]) +AS_ECHO_N("voip-teamspeak3 - ") +AS_IF([test "x$plugin_voip_teamspeak3" = "xyes" ], + [AS_ECHO("Enabled")], + [AS_ECHO("Disabled")]) +AS_ECHO_N("google-analytics - ") +AS_IF([test "x$plugin_google_analytics" = "xyes" ], + [AS_ECHO("Enabled")], + [AS_ECHO("Disabled")]) +AS_ECHO_N("debug - ") +AS_IF([test "x$plugin_debug" = "xyes" ], + [AS_ECHO("Enabled")], + [AS_ECHO("Disabled")]) +AS_ECHO_N("trafficstats - ") +AS_IF([test "x$plugin_trafficstats" = "xyes" ], + [AS_ECHO("Enabled")], + [AS_ECHO("Disabled")]) +AS_ECHO_N("pommodoro - ") +AS_IF([test "x$plugin_pommodoro" = "xyes" ], + [AS_ECHO("Enabled")], + [AS_ECHO("Disabled")]) +AS_ECHO_N("game-nexuiz - ") +AS_IF([test "x$plugin_game_nexuiz" = "xyes" ], + [AS_ECHO("Enabled")], + [AS_ECHO("Disabled")]) +AS_ECHO_N("backlight - ") +AS_IF([test "x$plugin_backlight" = "xyes" ], + [AS_ECHO("Enabled")], + [AS_ECHO("Disabled")]) +AS_ECHO_N("notify-lcd2 - ") +AS_IF([test "x$plugin_notify_lcd2" = "xyes" ], + [AS_ECHO("Enabled")], + [AS_ECHO("Disabled")]) +AS_ECHO_N("ppastats - ") +AS_IF([test "x$plugin_ppastats" = "xyes" ], + [AS_ECHO("Enabled")], + [AS_ECHO("Disabled")]) +AS_ECHO_N("nm - ") +AS_IF([test "x$plugin_nm" = "xyes" ], + [AS_ECHO("Enabled")], + [AS_ECHO("Disabled")]) +AS_ECHO_N("lens - ") +AS_IF([test "x$plugin_lens" = "xyes" ], + [AS_ECHO("Enabled")], + [AS_ECHO("Disabled")]) +AS_ECHO_N("webkitbrowser - ") +AS_IF([test "x$plugin_webkit_browser" = "xyes" ], + [AS_ECHO("Enabled")], + [AS_ECHO("Disabled")]) +AS_ECHO_N("things - ") +AS_IF([test "x$plugin_things" = "xyes" ], + [AS_ECHO("Enabled")], + [AS_ECHO("Disabled")]) +AS_ECHO_N("impulse15 - ") +AS_IF([test "x$plugin_impulse15" = "xyes" ], + [AS_ECHO("Enabled")], + [AS_ECHO("Disabled")]) +AS_ECHO("See ./configure --help for descriptions of these plugins and options to enable and disable them.") +AS_ECHO("") +AS_ECHO("Other Configuration") +AS_ECHO("-------------------") +AS_ECHO("Fixed size font name - ${FIXED_SIZE_FONT}") +AS_ECHO("udev rules path - ${UDEV_RULES_PATH}") +AS_ECHO("Device group - ${DEVICEGROUP}") +AS_ECHO("Device mode - ${DEVICEMODE}") +AS_ECHO_N("Hotplugging support - ") +AS_IF([test "x${HAVE_PYUDEV}" = "xno"], + [AS_ECHO("No")], + [AS_ECHO("Yes")]) +AS_ECHO("") +AS_ECHO("Building i18n for locales: ${ENABLED_LOCALES}") +AS_ECHO("") diff --git a/data/Makefile.am b/data/Makefile.am new file mode 100644 index 0000000..e466685 --- /dev/null +++ b/data/Makefile.am @@ -0,0 +1,11 @@ +SUBDIRS = \ + applications \ + autostart \ + dbus \ + udev \ + themes \ + images \ + ukeys \ + ui \ + icons + diff --git a/data/applications/Makefile.am b/data/applications/Makefile.am new file mode 100644 index 0000000..9374e0b --- /dev/null +++ b/data/applications/Makefile.am @@ -0,0 +1,5 @@ +appdir = $(datadir)/applications +app_DATA = g15-config.desktop + +EXTRA_DIST = \ + $(app_DATA) diff --git a/data/applications/g15-config.desktop.in b/data/applications/g15-config.desktop.in new file mode 100644 index 0000000..62516b1 --- /dev/null +++ b/data/applications/g15-config.desktop.in @@ -0,0 +1,10 @@ +[Desktop Entry] +Encoding=UTF-8 +Name=Logitech G Keyboard Configuration +Comment=Configure your Logitech G15 or G19 keyboard +Exec=g15-config +Icon=gnome15 +Terminal=false +StartupNotify=true +Type=Application +Categories=GTK;Settings;HardwareSettings; diff --git a/data/autostart/Makefile.am b/data/autostart/Makefile.am new file mode 100644 index 0000000..f7a78a1 --- /dev/null +++ b/data/autostart/Makefile.am @@ -0,0 +1,12 @@ +if ENABLE_SYSTEMTRAY + MAYBE_SYSTEMTRAY = g15-systemtray.desktop +endif +if ENABLE_INDICATOR + MAYBE_INDICATOR = g15-indicator.desktop +endif + +autostartdir = $(sysconfdir)/xdg/autostart +autostart_DATA = gnome15.desktop $(MAYBE_SYSTEMTRAY) $(MAYBE_INDICATOR) + +EXTRA_DIST = \ + gnome15.desktop g15-systemtray.desktop g15-indicator.desktop diff --git a/data/autostart/g15-indicator.desktop.in b/data/autostart/g15-indicator.desktop.in new file mode 100644 index 0000000..0c809e5 --- /dev/null +++ b/data/autostart/g15-indicator.desktop.in @@ -0,0 +1,20 @@ +[Desktop Entry] +Version=1.0 +Encoding=UTF-8 +Name=Logitech G Keyboard Indicator +Name[fr]=Indicateur Logitech Clavier G +Name[it]=Tastiera Logitech G Indicatore +Icon=gnome15 +Comment=Panel indicator allowing control and monitor of the Gnome15 desktop service for Logitech G keyboards. +Comment[fr]=Voyant panneau permettant le contrôle et le suivi du service Gnome15 bureau pour les claviers Logitech G. +Comment[it]=Pannello indicatore che consente il controllo e il monitoraggio del servizio Gnome15 desktop per tastiere Logitech G +Exec=g15-indicator +Terminal=false +Type=Application +Categories=TrayIcon;GTK +GenericName= +#X-GNOME-Autostart-Delay=0 +#AutostartCondition=GNOME /desktop/gnome/gnome15/enabled +X-GNOME-Autostart-Phase=Applications +#X-GNOME-AutoRestart=true +#X-Ubuntu-Gettext-Domain=gnome15 \ No newline at end of file diff --git a/data/autostart/g15-systemtray.desktop.in b/data/autostart/g15-systemtray.desktop.in new file mode 100644 index 0000000..7382279 --- /dev/null +++ b/data/autostart/g15-systemtray.desktop.in @@ -0,0 +1,20 @@ +[Desktop Entry] +Version=1.0 +Encoding=UTF-8 +Name=Logitech G Keyboard Tray Icon +Name[fr]=Clavier Logitech G Icône System Tray +Name[it]=Logitech G Keyboard Tray Icon di sistema +Icon=gnome15 +Comment=Tray icon allowing control and monitor of the Gnome15 desktop service for Logitech G keyboards. +Comment[fr]=Icône permet de contrôler et de surveiller le service Gnome15 bureau pour les claviers Logitech G. +Comment[it]=Tray icon che consente il controllo e il monitoraggio del servizio Gnome15 desktop per tastiere Logitech G. +Exec=g15-systemtray +Terminal=false +Type=Application +Categories=TrayIcon;GTK +GenericName= +#X-GNOME-Autostart-Delay=0 +#AutostartCondition=GNOME /desktop/gnome/gnome15/enabled +X-GNOME-Autostart-Phase=Applications +#X-GNOME-AutoRestart=true +#X-Ubuntu-Gettext-Domain=gnome15 diff --git a/data/autostart/gnome15.desktop.in b/data/autostart/gnome15.desktop.in new file mode 100644 index 0000000..a993025 --- /dev/null +++ b/data/autostart/gnome15.desktop.in @@ -0,0 +1,21 @@ +[Desktop Entry] +Version=1.0 +Encoding=UTF-8 +Name=Logitech G Keyboard Desktop Service +Name[fr]=Clavier Logitech G Desktop Service +Name[it]=Tastiera Logitech G Desktop Servizio +Icon=gnome15 +Comment=Logitech G series keyboard desktop service. +Comment[fr]=Logitech série G clavier de bureau de service. +Comment[it]=Serie G di Logitech tastiera servizio desktop. +Exec=g15-desktop-service +Terminal=false +Type=Application +Categories=System;GTK +GenericName= +#X-GNOME-Autostart-Delay=8 +X-GNOME-Autostart-Phase=WindowManager +#AutostartCondition=GNOME /desktop/gnome/gnome15/enabled +#X-GNOME-Autostart-Phase=Applications +#X-GNOME-AutoRestart=true +#X-Ubuntu-Gettext-Domain=gnome15 \ No newline at end of file diff --git a/data/dbus/Makefile.am b/data/dbus/Makefile.am new file mode 100644 index 0000000..422e155 --- /dev/null +++ b/data/dbus/Makefile.am @@ -0,0 +1,10 @@ +if ENABLE_DRIVER_KERNEL + systemdbusconfdir = $(sysconfdir)/dbus-1/system.d + systemdbusconf_DATA = g15-system-service.conf + systemdbusdir = $(datadir)/dbus-1/system-services + systemdbus_DATA = org.gnome15.SystemService.service +endif + +dbusdir = $(datadir)/dbus-1/services +dbus_DATA = org.gnome15.Gnome15.service +EXTRA_DIST = org.gnome15.Gnome15.service org.gnome15.SystemService.service g15-system-service.conf diff --git a/data/dbus/g15-system-service.conf b/data/dbus/g15-system-service.conf new file mode 100644 index 0000000..21c87d2 --- /dev/null +++ b/data/dbus/g15-system-service.conf @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/data/dbus/org.gnome15.Gnome15.service b/data/dbus/org.gnome15.Gnome15.service new file mode 100644 index 0000000..78ebb73 --- /dev/null +++ b/data/dbus/org.gnome15.Gnome15.service @@ -0,0 +1,3 @@ +[D-BUS Service] +Name=org.gnome15.Gnome15 +#Exec=g15-desktop-service diff --git a/data/dbus/org.gnome15.SystemService.service b/data/dbus/org.gnome15.SystemService.service new file mode 100644 index 0000000..26a7596 --- /dev/null +++ b/data/dbus/org.gnome15.SystemService.service @@ -0,0 +1,4 @@ +[D-BUS Service] +Name=org.gnome15.SystemService +Exec=/usr/bin/g15-system-service +User=root diff --git a/data/fonts/CyrKoi-VGA8.psf.gz b/data/fonts/CyrKoi-VGA8.psf.gz new file mode 100644 index 0000000..e150703 Binary files /dev/null and b/data/fonts/CyrKoi-VGA8.psf.gz differ diff --git a/data/fonts/tom-thumb.pcf b/data/fonts/tom-thumb.pcf new file mode 100644 index 0000000..b9f2ae7 Binary files /dev/null and b/data/fonts/tom-thumb.pcf differ diff --git a/data/icons/AwOken/Makefile.am b/data/icons/AwOken/Makefile.am new file mode 100644 index 0000000..7a187e1 --- /dev/null +++ b/data/icons/AwOken/Makefile.am @@ -0,0 +1 @@ +SUBDIRS = status apps diff --git a/data/icons/AwOken/apps/128/Makefile.am b/data/icons/AwOken/apps/128/Makefile.am new file mode 100644 index 0000000..085a971 --- /dev/null +++ b/data/icons/AwOken/apps/128/Makefile.am @@ -0,0 +1,5 @@ +imagesdir = $(datadir)/icons/AwOken/clear/128x128/apps +images_DATA = gnome15.png + +EXTRA_DIST = \ + $(images_DATA) diff --git a/data/icons/AwOken/apps/128/gnome15.png b/data/icons/AwOken/apps/128/gnome15.png new file mode 100644 index 0000000..809d87d Binary files /dev/null and b/data/icons/AwOken/apps/128/gnome15.png differ diff --git a/data/icons/AwOken/apps/16/Makefile.am b/data/icons/AwOken/apps/16/Makefile.am new file mode 100644 index 0000000..9d1f1f8 --- /dev/null +++ b/data/icons/AwOken/apps/16/Makefile.am @@ -0,0 +1,5 @@ +imagesdir = $(datadir)/icons/AwOken/clear/16x16/apps +images_DATA = gnome15.png + +EXTRA_DIST = \ + $(images_DATA) diff --git a/data/icons/AwOken/apps/16/gnome15.png b/data/icons/AwOken/apps/16/gnome15.png new file mode 100644 index 0000000..9335d44 Binary files /dev/null and b/data/icons/AwOken/apps/16/gnome15.png differ diff --git a/data/icons/AwOken/apps/22/Makefile.am b/data/icons/AwOken/apps/22/Makefile.am new file mode 100644 index 0000000..5d71517 --- /dev/null +++ b/data/icons/AwOken/apps/22/Makefile.am @@ -0,0 +1,5 @@ +imagesdir = $(datadir)/icons/AwOken/clear/22x22/apps +images_DATA = gnome15.png + +EXTRA_DIST = \ + $(images_DATA) diff --git a/data/icons/AwOken/apps/22/gnome15.png b/data/icons/AwOken/apps/22/gnome15.png new file mode 100644 index 0000000..0178d09 Binary files /dev/null and b/data/icons/AwOken/apps/22/gnome15.png differ diff --git a/data/icons/AwOken/apps/24/Makefile.am b/data/icons/AwOken/apps/24/Makefile.am new file mode 100644 index 0000000..7ced0cd --- /dev/null +++ b/data/icons/AwOken/apps/24/Makefile.am @@ -0,0 +1,5 @@ +imagesdir = $(datadir)/icons/AwOken/clear/24x24/apps +images_DATA = gnome15.png + +EXTRA_DIST = \ + $(images_DATA) diff --git a/data/icons/AwOken/apps/24/gnome15.png b/data/icons/AwOken/apps/24/gnome15.png new file mode 100644 index 0000000..cda0461 Binary files /dev/null and b/data/icons/AwOken/apps/24/gnome15.png differ diff --git a/data/icons/AwOken/apps/48/Makefile.am b/data/icons/AwOken/apps/48/Makefile.am new file mode 100644 index 0000000..b621143 --- /dev/null +++ b/data/icons/AwOken/apps/48/Makefile.am @@ -0,0 +1,5 @@ +imagesdir = $(datadir)/icons/AwOken/clear/48x48/apps +images_DATA = gnome15.png + +EXTRA_DIST = \ + $(images_DATA) diff --git a/data/icons/AwOken/apps/48/gnome15.png b/data/icons/AwOken/apps/48/gnome15.png new file mode 100644 index 0000000..0121756 Binary files /dev/null and b/data/icons/AwOken/apps/48/gnome15.png differ diff --git a/data/icons/AwOken/apps/64/Makefile.am b/data/icons/AwOken/apps/64/Makefile.am new file mode 100644 index 0000000..b7bddda --- /dev/null +++ b/data/icons/AwOken/apps/64/Makefile.am @@ -0,0 +1,5 @@ +imagesdir = $(datadir)/icons/AwOken/clear/64x64/apps +images_DATA = gnome15.png + +EXTRA_DIST = \ + $(images_DATA) diff --git a/data/icons/AwOken/apps/64/gnome15.png b/data/icons/AwOken/apps/64/gnome15.png new file mode 100644 index 0000000..a200286 Binary files /dev/null and b/data/icons/AwOken/apps/64/gnome15.png differ diff --git a/data/icons/AwOken/apps/Makefile.am b/data/icons/AwOken/apps/Makefile.am new file mode 100644 index 0000000..1dd1756 --- /dev/null +++ b/data/icons/AwOken/apps/Makefile.am @@ -0,0 +1 @@ +SUBDIRS = 16 22 24 48 64 128 diff --git a/data/icons/AwOken/status/128/Makefile.am b/data/icons/AwOken/status/128/Makefile.am new file mode 100644 index 0000000..97774a4 --- /dev/null +++ b/data/icons/AwOken/status/128/Makefile.am @@ -0,0 +1,6 @@ +imagesdir = $(datadir)/icons/AwOken/clear/128x128/status +images_DATA = logitech-g-keyboard-error-panel.png \ + logitech-g-keyboard-panel.png + +EXTRA_DIST = \ + $(images_DATA) diff --git a/data/icons/AwOken/status/128/logitech-g-keyboard-error-panel.png b/data/icons/AwOken/status/128/logitech-g-keyboard-error-panel.png new file mode 100644 index 0000000..b512675 Binary files /dev/null and b/data/icons/AwOken/status/128/logitech-g-keyboard-error-panel.png differ diff --git a/data/icons/AwOken/status/128/logitech-g-keyboard-panel.png b/data/icons/AwOken/status/128/logitech-g-keyboard-panel.png new file mode 100644 index 0000000..809d87d Binary files /dev/null and b/data/icons/AwOken/status/128/logitech-g-keyboard-panel.png differ diff --git a/data/icons/AwOken/status/16/Makefile.am b/data/icons/AwOken/status/16/Makefile.am new file mode 100644 index 0000000..ffdf9e5 --- /dev/null +++ b/data/icons/AwOken/status/16/Makefile.am @@ -0,0 +1,6 @@ +imagesdir = $(datadir)/icons/AwOken/clear/16x16/status +images_DATA = logitech-g-keyboard-error-panel.png \ + logitech-g-keyboard-panel.png + +EXTRA_DIST = \ + $(images_DATA) diff --git a/data/icons/AwOken/status/16/logitech-g-keyboard-error-panel.png b/data/icons/AwOken/status/16/logitech-g-keyboard-error-panel.png new file mode 100644 index 0000000..0b3d6d5 Binary files /dev/null and b/data/icons/AwOken/status/16/logitech-g-keyboard-error-panel.png differ diff --git a/data/icons/AwOken/status/16/logitech-g-keyboard-panel.png b/data/icons/AwOken/status/16/logitech-g-keyboard-panel.png new file mode 100644 index 0000000..9335d44 Binary files /dev/null and b/data/icons/AwOken/status/16/logitech-g-keyboard-panel.png differ diff --git a/data/icons/AwOken/status/22/Makefile.am b/data/icons/AwOken/status/22/Makefile.am new file mode 100644 index 0000000..da8e0a4 --- /dev/null +++ b/data/icons/AwOken/status/22/Makefile.am @@ -0,0 +1,6 @@ +imagesdir = $(datadir)/icons/AwOken/clear/22x22/status +images_DATA = logitech-g-keyboard-error-panel.png \ + logitech-g-keyboard-panel.png + +EXTRA_DIST = \ + $(images_DATA) diff --git a/data/icons/AwOken/status/22/logitech-g-keyboard-error-panel.png b/data/icons/AwOken/status/22/logitech-g-keyboard-error-panel.png new file mode 100644 index 0000000..fbb888a Binary files /dev/null and b/data/icons/AwOken/status/22/logitech-g-keyboard-error-panel.png differ diff --git a/data/icons/AwOken/status/22/logitech-g-keyboard-panel.png b/data/icons/AwOken/status/22/logitech-g-keyboard-panel.png new file mode 100644 index 0000000..0178d09 Binary files /dev/null and b/data/icons/AwOken/status/22/logitech-g-keyboard-panel.png differ diff --git a/data/icons/AwOken/status/24/Makefile.am b/data/icons/AwOken/status/24/Makefile.am new file mode 100644 index 0000000..99d569c --- /dev/null +++ b/data/icons/AwOken/status/24/Makefile.am @@ -0,0 +1,6 @@ +imagesdir = $(datadir)/icons/AwOken/clear/24x24/status +images_DATA = logitech-g-keyboard-error-panel.png \ + logitech-g-keyboard-panel.png + +EXTRA_DIST = \ + $(images_DATA) diff --git a/data/icons/AwOken/status/24/logitech-g-keyboard-error-panel.png b/data/icons/AwOken/status/24/logitech-g-keyboard-error-panel.png new file mode 100644 index 0000000..f8e85c9 Binary files /dev/null and b/data/icons/AwOken/status/24/logitech-g-keyboard-error-panel.png differ diff --git a/data/icons/AwOken/status/24/logitech-g-keyboard-panel.png b/data/icons/AwOken/status/24/logitech-g-keyboard-panel.png new file mode 100644 index 0000000..cda0461 Binary files /dev/null and b/data/icons/AwOken/status/24/logitech-g-keyboard-panel.png differ diff --git a/data/icons/AwOken/status/48/Makefile.am b/data/icons/AwOken/status/48/Makefile.am new file mode 100644 index 0000000..1070fbb --- /dev/null +++ b/data/icons/AwOken/status/48/Makefile.am @@ -0,0 +1,6 @@ +imagesdir = $(datadir)/icons/AwOken/clear/48x48/status +images_DATA = logitech-g-keyboard-error-panel.png \ + logitech-g-keyboard-panel.png + +EXTRA_DIST = \ + $(images_DATA) diff --git a/data/icons/AwOken/status/48/logitech-g-keyboard-error-panel.png b/data/icons/AwOken/status/48/logitech-g-keyboard-error-panel.png new file mode 100644 index 0000000..f93c83f Binary files /dev/null and b/data/icons/AwOken/status/48/logitech-g-keyboard-error-panel.png differ diff --git a/data/icons/AwOken/status/48/logitech-g-keyboard-panel.png b/data/icons/AwOken/status/48/logitech-g-keyboard-panel.png new file mode 100644 index 0000000..0121756 Binary files /dev/null and b/data/icons/AwOken/status/48/logitech-g-keyboard-panel.png differ diff --git a/data/icons/AwOken/status/64/Makefile.am b/data/icons/AwOken/status/64/Makefile.am new file mode 100644 index 0000000..976584a --- /dev/null +++ b/data/icons/AwOken/status/64/Makefile.am @@ -0,0 +1,6 @@ +imagesdir = $(datadir)/icons/AwOken/clear/64x64/status +images_DATA = logitech-g-keyboard-error-panel.png \ + logitech-g-keyboard-panel.png + +EXTRA_DIST = \ + $(images_DATA) diff --git a/data/icons/AwOken/status/64/logitech-g-keyboard-error-panel.png b/data/icons/AwOken/status/64/logitech-g-keyboard-error-panel.png new file mode 100644 index 0000000..ecba249 Binary files /dev/null and b/data/icons/AwOken/status/64/logitech-g-keyboard-error-panel.png differ diff --git a/data/icons/AwOken/status/64/logitech-g-keyboard-panel.png b/data/icons/AwOken/status/64/logitech-g-keyboard-panel.png new file mode 100644 index 0000000..a200286 Binary files /dev/null and b/data/icons/AwOken/status/64/logitech-g-keyboard-panel.png differ diff --git a/data/icons/AwOken/status/Makefile.am b/data/icons/AwOken/status/Makefile.am new file mode 100644 index 0000000..1dd1756 --- /dev/null +++ b/data/icons/AwOken/status/Makefile.am @@ -0,0 +1 @@ +SUBDIRS = 16 22 24 48 64 128 diff --git a/data/icons/Makefile.am b/data/icons/Makefile.am new file mode 100644 index 0000000..09b792b --- /dev/null +++ b/data/icons/Makefile.am @@ -0,0 +1,11 @@ +SUBDIRS = hicolor +if ENABLE_ICONS_AWOKEN +SUBDIRS += AwOken +endif +if ENABLE_ICONS_MONO +SUBDIRS += \ + ubuntu-mono-dark \ + ubuntu-mono-light +endif + +EXTRA_DIST= AwOken ubuntu-mono-dark ubuntu-mono-light diff --git a/data/icons/elementary/Makefile.am b/data/icons/elementary/Makefile.am new file mode 100644 index 0000000..d52da2b --- /dev/null +++ b/data/icons/elementary/Makefile.am @@ -0,0 +1 @@ +SUBDIRS = status diff --git a/data/icons/elementary/status/16/Makefile.am b/data/icons/elementary/status/16/Makefile.am new file mode 100644 index 0000000..76ff8d8 --- /dev/null +++ b/data/icons/elementary/status/16/Makefile.am @@ -0,0 +1,6 @@ +imagesdir = $(datadir)/icons/elementary/status/16 +images_DATA = logitech-g-keyboard-error-panel.svg \ + logitech-g-keyboard-panel.svg + +EXTRA_DIST = \ + $(images_DATA) diff --git a/data/icons/elementary/status/16/logitech-g-keyboard-error-panel.svg b/data/icons/elementary/status/16/logitech-g-keyboard-error-panel.svg new file mode 100644 index 0000000..6c35d9a --- /dev/null +++ b/data/icons/elementary/status/16/logitech-g-keyboard-error-panel.svg @@ -0,0 +1,225 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + G + + diff --git a/data/icons/elementary/status/16/logitech-g-keyboard-panel.svg b/data/icons/elementary/status/16/logitech-g-keyboard-panel.svg new file mode 100644 index 0000000..b48e4e5 --- /dev/null +++ b/data/icons/elementary/status/16/logitech-g-keyboard-panel.svg @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/data/icons/elementary/status/22/Makefile.am b/data/icons/elementary/status/22/Makefile.am new file mode 100644 index 0000000..26fe3eb --- /dev/null +++ b/data/icons/elementary/status/22/Makefile.am @@ -0,0 +1,6 @@ +imagesdir = $(datadir)/icons/elementary/status/22 +images_DATA = logitech-g-keyboard-error-panel.svg \ + logitech-g-keyboard-panel.svg + +EXTRA_DIST = \ + $(images_DATA) diff --git a/data/icons/elementary/status/22/logitech-g-keyboard-error-panel.svg b/data/icons/elementary/status/22/logitech-g-keyboard-error-panel.svg new file mode 100644 index 0000000..8d30176 --- /dev/null +++ b/data/icons/elementary/status/22/logitech-g-keyboard-error-panel.svg @@ -0,0 +1,235 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + G + + diff --git a/data/icons/elementary/status/22/logitech-g-keyboard-panel.svg b/data/icons/elementary/status/22/logitech-g-keyboard-panel.svg new file mode 100644 index 0000000..b8d215d --- /dev/null +++ b/data/icons/elementary/status/22/logitech-g-keyboard-panel.svg @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/data/icons/elementary/status/24/Makefile.am b/data/icons/elementary/status/24/Makefile.am new file mode 100644 index 0000000..05a36ad --- /dev/null +++ b/data/icons/elementary/status/24/Makefile.am @@ -0,0 +1,6 @@ +imagesdir = $(datadir)/icons/elementary/status/24 +images_DATA = logitech-g-keyboard-error-panel.svg \ + logitech-g-keyboard-panel.svg + +EXTRA_DIST = \ + $(images_DATA) diff --git a/data/icons/elementary/status/24/logitech-g-keyboard-error-panel.svg b/data/icons/elementary/status/24/logitech-g-keyboard-error-panel.svg new file mode 100644 index 0000000..9753175 --- /dev/null +++ b/data/icons/elementary/status/24/logitech-g-keyboard-error-panel.svg @@ -0,0 +1,440 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + G + + + + + + + + diff --git a/data/icons/elementary/status/24/logitech-g-keyboard-panel.svg b/data/icons/elementary/status/24/logitech-g-keyboard-panel.svg new file mode 100644 index 0000000..21e0008 --- /dev/null +++ b/data/icons/elementary/status/24/logitech-g-keyboard-panel.svg @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/data/icons/elementary/status/Makefile.am b/data/icons/elementary/status/Makefile.am new file mode 100644 index 0000000..fe7d9a1 --- /dev/null +++ b/data/icons/elementary/status/Makefile.am @@ -0,0 +1 @@ +SUBDIRS = 16 22 24 diff --git a/data/icons/hicolor/16x16/Makefile.am b/data/icons/hicolor/16x16/Makefile.am new file mode 100644 index 0000000..d52da2b --- /dev/null +++ b/data/icons/hicolor/16x16/Makefile.am @@ -0,0 +1 @@ +SUBDIRS = status diff --git a/data/icons/hicolor/16x16/status/Makefile.am b/data/icons/hicolor/16x16/status/Makefile.am new file mode 100644 index 0000000..d38627c --- /dev/null +++ b/data/icons/hicolor/16x16/status/Makefile.am @@ -0,0 +1,6 @@ +imagesdir = $(datadir)/icons/hicolor/16x16/status +images_DATA = logitech-g-keyboard-error-applet.png \ + logitech-g-keyboard-applet.png + +EXTRA_DIST = \ + $(images_DATA) diff --git a/data/icons/hicolor/16x16/status/logitech-g-keyboard-applet.png b/data/icons/hicolor/16x16/status/logitech-g-keyboard-applet.png new file mode 100644 index 0000000..9cd42c6 Binary files /dev/null and b/data/icons/hicolor/16x16/status/logitech-g-keyboard-applet.png differ diff --git a/data/icons/hicolor/16x16/status/logitech-g-keyboard-error-applet.png b/data/icons/hicolor/16x16/status/logitech-g-keyboard-error-applet.png new file mode 100644 index 0000000..b27ff2c Binary files /dev/null and b/data/icons/hicolor/16x16/status/logitech-g-keyboard-error-applet.png differ diff --git a/data/icons/hicolor/22x22/Makefile.am b/data/icons/hicolor/22x22/Makefile.am new file mode 100644 index 0000000..1eae4a6 --- /dev/null +++ b/data/icons/hicolor/22x22/Makefile.am @@ -0,0 +1 @@ +SUBDIRS = apps status diff --git a/data/icons/hicolor/22x22/apps/Makefile.am b/data/icons/hicolor/22x22/apps/Makefile.am new file mode 100644 index 0000000..354ff38 --- /dev/null +++ b/data/icons/hicolor/22x22/apps/Makefile.am @@ -0,0 +1,5 @@ +imagesdir = $(datadir)/icons/hicolor/22x22/apps +images_DATA = gnome15.png + +EXTRA_DIST = \ + $(images_DATA) diff --git a/data/icons/hicolor/22x22/apps/gnome15.png b/data/icons/hicolor/22x22/apps/gnome15.png new file mode 100644 index 0000000..c2ab623 Binary files /dev/null and b/data/icons/hicolor/22x22/apps/gnome15.png differ diff --git a/data/icons/hicolor/22x22/status/Makefile.am b/data/icons/hicolor/22x22/status/Makefile.am new file mode 100644 index 0000000..29f90ad --- /dev/null +++ b/data/icons/hicolor/22x22/status/Makefile.am @@ -0,0 +1,6 @@ +imagesdir = $(datadir)/icons/hicolor/22x24/status +images_DATA = logitech-g-keyboard-error-applet.png \ + logitech-g-keyboard-applet.png + +EXTRA_DIST = \ + $(images_DATA) diff --git a/data/icons/hicolor/22x22/status/logitech-g-keyboard-applet.png b/data/icons/hicolor/22x22/status/logitech-g-keyboard-applet.png new file mode 100644 index 0000000..c34418b Binary files /dev/null and b/data/icons/hicolor/22x22/status/logitech-g-keyboard-applet.png differ diff --git a/data/icons/hicolor/22x22/status/logitech-g-keyboard-error-applet.png b/data/icons/hicolor/22x22/status/logitech-g-keyboard-error-applet.png new file mode 100644 index 0000000..1259e22 Binary files /dev/null and b/data/icons/hicolor/22x22/status/logitech-g-keyboard-error-applet.png differ diff --git a/data/icons/hicolor/24x24/Makefile.am b/data/icons/hicolor/24x24/Makefile.am new file mode 100644 index 0000000..1eae4a6 --- /dev/null +++ b/data/icons/hicolor/24x24/Makefile.am @@ -0,0 +1 @@ +SUBDIRS = apps status diff --git a/data/icons/hicolor/24x24/apps/Makefile.am b/data/icons/hicolor/24x24/apps/Makefile.am new file mode 100644 index 0000000..8087ce8 --- /dev/null +++ b/data/icons/hicolor/24x24/apps/Makefile.am @@ -0,0 +1,5 @@ +imagesdir = $(datadir)/icons/hicolor/24x24/apps +images_DATA = gnome15.png + +EXTRA_DIST = \ + $(images_DATA) diff --git a/data/icons/hicolor/24x24/apps/gnome15.png b/data/icons/hicolor/24x24/apps/gnome15.png new file mode 100644 index 0000000..c2ab623 Binary files /dev/null and b/data/icons/hicolor/24x24/apps/gnome15.png differ diff --git a/data/icons/hicolor/24x24/status/Makefile.am b/data/icons/hicolor/24x24/status/Makefile.am new file mode 100644 index 0000000..fd3ae81 --- /dev/null +++ b/data/icons/hicolor/24x24/status/Makefile.am @@ -0,0 +1,6 @@ +imagesdir = $(datadir)/icons/hicolor/24x24/status +images_DATA = logitech-g-keyboard-error-applet.png \ + logitech-g-keyboard-applet.png + +EXTRA_DIST = \ + $(images_DATA) diff --git a/data/icons/hicolor/24x24/status/logitech-g-keyboard-applet.png b/data/icons/hicolor/24x24/status/logitech-g-keyboard-applet.png new file mode 100644 index 0000000..ef12b98 Binary files /dev/null and b/data/icons/hicolor/24x24/status/logitech-g-keyboard-applet.png differ diff --git a/data/icons/hicolor/24x24/status/logitech-g-keyboard-error-applet.png b/data/icons/hicolor/24x24/status/logitech-g-keyboard-error-applet.png new file mode 100644 index 0000000..fca25e2 Binary files /dev/null and b/data/icons/hicolor/24x24/status/logitech-g-keyboard-error-applet.png differ diff --git a/data/icons/hicolor/48x48/Makefile.am b/data/icons/hicolor/48x48/Makefile.am new file mode 100644 index 0000000..ebbd145 --- /dev/null +++ b/data/icons/hicolor/48x48/Makefile.am @@ -0,0 +1 @@ +SUBDIRS = apps diff --git a/data/icons/hicolor/48x48/apps/Makefile.am b/data/icons/hicolor/48x48/apps/Makefile.am new file mode 100644 index 0000000..aef69a6 --- /dev/null +++ b/data/icons/hicolor/48x48/apps/Makefile.am @@ -0,0 +1,5 @@ +imagesdir = $(datadir)/icons/hicolor/48x48/apps +images_DATA = gnome15.png + +EXTRA_DIST = \ + $(images_DATA) diff --git a/data/icons/hicolor/48x48/apps/gnome15.png b/data/icons/hicolor/48x48/apps/gnome15.png new file mode 100644 index 0000000..3c0f909 Binary files /dev/null and b/data/icons/hicolor/48x48/apps/gnome15.png differ diff --git a/data/icons/hicolor/64x64/Makefile.am b/data/icons/hicolor/64x64/Makefile.am new file mode 100644 index 0000000..ebbd145 --- /dev/null +++ b/data/icons/hicolor/64x64/Makefile.am @@ -0,0 +1 @@ +SUBDIRS = apps diff --git a/data/icons/hicolor/64x64/apps/Makefile.am b/data/icons/hicolor/64x64/apps/Makefile.am new file mode 100644 index 0000000..d404298 --- /dev/null +++ b/data/icons/hicolor/64x64/apps/Makefile.am @@ -0,0 +1,5 @@ +imagesdir = $(datadir)/icons/hicolor/64x64/apps +images_DATA = gnome15.png + +EXTRA_DIST = \ + $(images_DATA) diff --git a/data/icons/hicolor/64x64/apps/gnome15.png b/data/icons/hicolor/64x64/apps/gnome15.png new file mode 100644 index 0000000..8e11b54 Binary files /dev/null and b/data/icons/hicolor/64x64/apps/gnome15.png differ diff --git a/data/icons/hicolor/Makefile.am b/data/icons/hicolor/Makefile.am new file mode 100644 index 0000000..01eff3c --- /dev/null +++ b/data/icons/hicolor/Makefile.am @@ -0,0 +1 @@ +SUBDIRS = 16x16 22x22 24x24 48x48 64x64 scalable diff --git a/data/icons/hicolor/scalable/Makefile.am b/data/icons/hicolor/scalable/Makefile.am new file mode 100644 index 0000000..dbbb83c --- /dev/null +++ b/data/icons/hicolor/scalable/Makefile.am @@ -0,0 +1 @@ +SUBDIRS = apps status devices diff --git a/data/icons/hicolor/scalable/apps/Makefile.am b/data/icons/hicolor/scalable/apps/Makefile.am new file mode 100644 index 0000000..a9adaed --- /dev/null +++ b/data/icons/hicolor/scalable/apps/Makefile.am @@ -0,0 +1,5 @@ +imagesdir = $(datadir)/icons/hicolor/scalable/apps +images_DATA = gnome15.svg + +EXTRA_DIST = \ + $(images_DATA) diff --git a/data/icons/hicolor/scalable/apps/gnome15.svg b/data/icons/hicolor/scalable/apps/gnome15.svg new file mode 100644 index 0000000..df59f82 --- /dev/null +++ b/data/icons/hicolor/scalable/apps/gnome15.svg @@ -0,0 +1,3076 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + G + G + G + G + G + diff --git a/data/icons/hicolor/scalable/devices/Makefile.am b/data/icons/hicolor/scalable/devices/Makefile.am new file mode 100644 index 0000000..82eb519 --- /dev/null +++ b/data/icons/hicolor/scalable/devices/Makefile.am @@ -0,0 +1,5 @@ +imagesdir = $(datadir)/icons/hicolor/scalable/devices +images_DATA = g110.png g15v1.png g15v2.png g19.png g510.png g930.png z10.png g13.png + +EXTRA_DIST = \ + $(images_DATA) diff --git a/data/icons/hicolor/scalable/devices/g11.png b/data/icons/hicolor/scalable/devices/g11.png new file mode 100644 index 0000000..fbe6d3d Binary files /dev/null and b/data/icons/hicolor/scalable/devices/g11.png differ diff --git a/data/icons/hicolor/scalable/devices/g110.png b/data/icons/hicolor/scalable/devices/g110.png new file mode 100644 index 0000000..9575b02 Binary files /dev/null and b/data/icons/hicolor/scalable/devices/g110.png differ diff --git a/data/icons/hicolor/scalable/devices/g13.png b/data/icons/hicolor/scalable/devices/g13.png new file mode 100644 index 0000000..9fe0ef9 Binary files /dev/null and b/data/icons/hicolor/scalable/devices/g13.png differ diff --git a/data/icons/hicolor/scalable/devices/g15v1.png b/data/icons/hicolor/scalable/devices/g15v1.png new file mode 100644 index 0000000..91e7ae7 Binary files /dev/null and b/data/icons/hicolor/scalable/devices/g15v1.png differ diff --git a/data/icons/hicolor/scalable/devices/g15v2.png b/data/icons/hicolor/scalable/devices/g15v2.png new file mode 100644 index 0000000..5d7660d Binary files /dev/null and b/data/icons/hicolor/scalable/devices/g15v2.png differ diff --git a/data/icons/hicolor/scalable/devices/g19.png b/data/icons/hicolor/scalable/devices/g19.png new file mode 100644 index 0000000..911e716 Binary files /dev/null and b/data/icons/hicolor/scalable/devices/g19.png differ diff --git a/data/icons/hicolor/scalable/devices/g35.png b/data/icons/hicolor/scalable/devices/g35.png new file mode 100644 index 0000000..cc073c2 Binary files /dev/null and b/data/icons/hicolor/scalable/devices/g35.png differ diff --git a/data/icons/hicolor/scalable/devices/g510.png b/data/icons/hicolor/scalable/devices/g510.png new file mode 100644 index 0000000..5361267 Binary files /dev/null and b/data/icons/hicolor/scalable/devices/g510.png differ diff --git a/data/icons/hicolor/scalable/devices/g930.png b/data/icons/hicolor/scalable/devices/g930.png new file mode 100644 index 0000000..06c0560 Binary files /dev/null and b/data/icons/hicolor/scalable/devices/g930.png differ diff --git a/data/icons/hicolor/scalable/devices/mx5500.png b/data/icons/hicolor/scalable/devices/mx5500.png new file mode 100644 index 0000000..c8c87e9 Binary files /dev/null and b/data/icons/hicolor/scalable/devices/mx5500.png differ diff --git a/data/icons/hicolor/scalable/devices/z10.png b/data/icons/hicolor/scalable/devices/z10.png new file mode 100644 index 0000000..dfb9dc5 Binary files /dev/null and b/data/icons/hicolor/scalable/devices/z10.png differ diff --git a/data/icons/hicolor/scalable/status/Makefile.am b/data/icons/hicolor/scalable/status/Makefile.am new file mode 100644 index 0000000..3f0ae9f --- /dev/null +++ b/data/icons/hicolor/scalable/status/Makefile.am @@ -0,0 +1,6 @@ +imagesdir = $(datadir)/icons/hicolor/scalable/status +images_DATA = logitech-g-keyboard-error-panel.svg \ + logitech-g-keyboard-panel.svg + +EXTRA_DIST = \ + $(images_DATA) diff --git a/data/icons/hicolor/scalable/status/logitech-g-keyboard-error-panel.svg b/data/icons/hicolor/scalable/status/logitech-g-keyboard-error-panel.svg new file mode 100644 index 0000000..1524705 --- /dev/null +++ b/data/icons/hicolor/scalable/status/logitech-g-keyboard-error-panel.svg @@ -0,0 +1,3076 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + G + G + G + G + G + diff --git a/data/icons/hicolor/scalable/status/logitech-g-keyboard-panel.svg b/data/icons/hicolor/scalable/status/logitech-g-keyboard-panel.svg new file mode 100644 index 0000000..df59f82 --- /dev/null +++ b/data/icons/hicolor/scalable/status/logitech-g-keyboard-panel.svg @@ -0,0 +1,3076 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + G + G + G + G + G + diff --git a/data/icons/ubuntu-mono-dark/Makefile.am b/data/icons/ubuntu-mono-dark/Makefile.am new file mode 100644 index 0000000..d52da2b --- /dev/null +++ b/data/icons/ubuntu-mono-dark/Makefile.am @@ -0,0 +1 @@ +SUBDIRS = status diff --git a/data/icons/ubuntu-mono-dark/status/16/Makefile.am b/data/icons/ubuntu-mono-dark/status/16/Makefile.am new file mode 100644 index 0000000..7dd3c2b --- /dev/null +++ b/data/icons/ubuntu-mono-dark/status/16/Makefile.am @@ -0,0 +1,6 @@ +imagesdir = $(datadir)/icons/ubuntu-mono-dark/status/16 +images_DATA = logitech-g-keyboard-error-panel.svg \ + logitech-g-keyboard-panel.svg + +EXTRA_DIST = \ + $(images_DATA) diff --git a/data/icons/ubuntu-mono-dark/status/16/logitech-g-keyboard-error-panel.svg b/data/icons/ubuntu-mono-dark/status/16/logitech-g-keyboard-error-panel.svg new file mode 100644 index 0000000..d698552 --- /dev/null +++ b/data/icons/ubuntu-mono-dark/status/16/logitech-g-keyboard-error-panel.svg @@ -0,0 +1,279 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + G + + + + + + + diff --git a/data/icons/ubuntu-mono-dark/status/16/logitech-g-keyboard-panel.svg b/data/icons/ubuntu-mono-dark/status/16/logitech-g-keyboard-panel.svg new file mode 100644 index 0000000..c607eed --- /dev/null +++ b/data/icons/ubuntu-mono-dark/status/16/logitech-g-keyboard-panel.svg @@ -0,0 +1,129 @@ + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/data/icons/ubuntu-mono-dark/status/22/Makefile.am b/data/icons/ubuntu-mono-dark/status/22/Makefile.am new file mode 100644 index 0000000..7df1dd9 --- /dev/null +++ b/data/icons/ubuntu-mono-dark/status/22/Makefile.am @@ -0,0 +1,6 @@ +imagesdir = $(datadir)/icons/ubuntu-mono-dark/status/22 +images_DATA = logitech-g-keyboard-error-panel.svg \ + logitech-g-keyboard-panel.svg + +EXTRA_DIST = \ + $(images_DATA) diff --git a/data/icons/ubuntu-mono-dark/status/22/logitech-g-keyboard-error-panel.svg b/data/icons/ubuntu-mono-dark/status/22/logitech-g-keyboard-error-panel.svg new file mode 100644 index 0000000..c464548 --- /dev/null +++ b/data/icons/ubuntu-mono-dark/status/22/logitech-g-keyboard-error-panel.svg @@ -0,0 +1,129 @@ + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/data/icons/ubuntu-mono-dark/status/22/logitech-g-keyboard-panel.svg b/data/icons/ubuntu-mono-dark/status/22/logitech-g-keyboard-panel.svg new file mode 100644 index 0000000..2043e54 --- /dev/null +++ b/data/icons/ubuntu-mono-dark/status/22/logitech-g-keyboard-panel.svg @@ -0,0 +1,129 @@ + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/data/icons/ubuntu-mono-dark/status/24/Makefile.am b/data/icons/ubuntu-mono-dark/status/24/Makefile.am new file mode 100644 index 0000000..885ae38 --- /dev/null +++ b/data/icons/ubuntu-mono-dark/status/24/Makefile.am @@ -0,0 +1,6 @@ +imagesdir = $(datadir)/icons/ubuntu-mono-dark/status/24 +images_DATA = logitech-g-keyboard-error-panel.svg \ + logitech-g-keyboard-panel.svg + +EXTRA_DIST = \ + $(images_DATA) diff --git a/data/icons/ubuntu-mono-dark/status/24/logitech-g-keyboard-error-panel.svg b/data/icons/ubuntu-mono-dark/status/24/logitech-g-keyboard-error-panel.svg new file mode 100644 index 0000000..9c94f98 --- /dev/null +++ b/data/icons/ubuntu-mono-dark/status/24/logitech-g-keyboard-error-panel.svg @@ -0,0 +1,134 @@ + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/data/icons/ubuntu-mono-dark/status/24/logitech-g-keyboard-panel.svg b/data/icons/ubuntu-mono-dark/status/24/logitech-g-keyboard-panel.svg new file mode 100644 index 0000000..c58d208 --- /dev/null +++ b/data/icons/ubuntu-mono-dark/status/24/logitech-g-keyboard-panel.svg @@ -0,0 +1,129 @@ + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/data/icons/ubuntu-mono-dark/status/Makefile.am b/data/icons/ubuntu-mono-dark/status/Makefile.am new file mode 100644 index 0000000..fe7d9a1 --- /dev/null +++ b/data/icons/ubuntu-mono-dark/status/Makefile.am @@ -0,0 +1 @@ +SUBDIRS = 16 22 24 diff --git a/data/icons/ubuntu-mono-light/Makefile.am b/data/icons/ubuntu-mono-light/Makefile.am new file mode 100644 index 0000000..d52da2b --- /dev/null +++ b/data/icons/ubuntu-mono-light/Makefile.am @@ -0,0 +1 @@ +SUBDIRS = status diff --git a/data/icons/ubuntu-mono-light/status/16/Makefile.am b/data/icons/ubuntu-mono-light/status/16/Makefile.am new file mode 100644 index 0000000..9b24ffd --- /dev/null +++ b/data/icons/ubuntu-mono-light/status/16/Makefile.am @@ -0,0 +1,6 @@ +imagesdir = $(datadir)/icons/ubuntu-mono-light/status/16 +images_DATA = logitech-g-keyboard-error-panel.svg \ + logitech-g-keyboard-panel.svg + +EXTRA_DIST = \ + $(images_DATA) diff --git a/data/icons/ubuntu-mono-light/status/16/logitech-g-keyboard-error-panel.svg b/data/icons/ubuntu-mono-light/status/16/logitech-g-keyboard-error-panel.svg new file mode 100644 index 0000000..16362b6 --- /dev/null +++ b/data/icons/ubuntu-mono-light/status/16/logitech-g-keyboard-error-panel.svg @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + G + + diff --git a/data/icons/ubuntu-mono-light/status/16/logitech-g-keyboard-panel.svg b/data/icons/ubuntu-mono-light/status/16/logitech-g-keyboard-panel.svg new file mode 100644 index 0000000..7e4df24 --- /dev/null +++ b/data/icons/ubuntu-mono-light/status/16/logitech-g-keyboard-panel.svg @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + G + + diff --git a/data/icons/ubuntu-mono-light/status/22/Makefile.am b/data/icons/ubuntu-mono-light/status/22/Makefile.am new file mode 100644 index 0000000..ba851a9 --- /dev/null +++ b/data/icons/ubuntu-mono-light/status/22/Makefile.am @@ -0,0 +1,6 @@ +imagesdir = $(datadir)/icons/ubuntu-mono-light/status/22 +images_DATA = logitech-g-keyboard-error-panel.svg \ + logitech-g-keyboard-panel.svg + +EXTRA_DIST = \ + $(images_DATA) diff --git a/data/icons/ubuntu-mono-light/status/22/logitech-g-keyboard-error-panel.svg b/data/icons/ubuntu-mono-light/status/22/logitech-g-keyboard-error-panel.svg new file mode 100644 index 0000000..1582e3b --- /dev/null +++ b/data/icons/ubuntu-mono-light/status/22/logitech-g-keyboard-error-panel.svg @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + G + + diff --git a/data/icons/ubuntu-mono-light/status/22/logitech-g-keyboard-panel.svg b/data/icons/ubuntu-mono-light/status/22/logitech-g-keyboard-panel.svg new file mode 100644 index 0000000..d09282b --- /dev/null +++ b/data/icons/ubuntu-mono-light/status/22/logitech-g-keyboard-panel.svg @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + G + + diff --git a/data/icons/ubuntu-mono-light/status/24/Makefile.am b/data/icons/ubuntu-mono-light/status/24/Makefile.am new file mode 100644 index 0000000..4242660 --- /dev/null +++ b/data/icons/ubuntu-mono-light/status/24/Makefile.am @@ -0,0 +1,6 @@ +imagesdir = $(datadir)/icons/ubuntu-mono-light/status/24 +images_DATA = logitech-g-keyboard-error-panel.svg \ + logitech-g-keyboard-panel.svg + +EXTRA_DIST = \ + $(images_DATA) diff --git a/data/icons/ubuntu-mono-light/status/24/logitech-g-keyboard-error-panel.svg b/data/icons/ubuntu-mono-light/status/24/logitech-g-keyboard-error-panel.svg new file mode 100644 index 0000000..1f7b262 --- /dev/null +++ b/data/icons/ubuntu-mono-light/status/24/logitech-g-keyboard-error-panel.svg @@ -0,0 +1,136 @@ + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + G + + + diff --git a/data/icons/ubuntu-mono-light/status/24/logitech-g-keyboard-panel.svg b/data/icons/ubuntu-mono-light/status/24/logitech-g-keyboard-panel.svg new file mode 100644 index 0000000..76cc1ce --- /dev/null +++ b/data/icons/ubuntu-mono-light/status/24/logitech-g-keyboard-panel.svg @@ -0,0 +1,136 @@ + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + G + + + diff --git a/data/icons/ubuntu-mono-light/status/24/old_logitech-g-keyboard-panel.svg b/data/icons/ubuntu-mono-light/status/24/old_logitech-g-keyboard-panel.svg new file mode 100644 index 0000000..187cfa1 --- /dev/null +++ b/data/icons/ubuntu-mono-light/status/24/old_logitech-g-keyboard-panel.svg @@ -0,0 +1,70 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + diff --git a/data/icons/ubuntu-mono-light/status/Makefile.am b/data/icons/ubuntu-mono-light/status/Makefile.am new file mode 100644 index 0000000..fe7d9a1 --- /dev/null +++ b/data/icons/ubuntu-mono-light/status/Makefile.am @@ -0,0 +1 @@ +SUBDIRS = 16 22 24 diff --git a/data/images/Makefile.am b/data/images/Makefile.am new file mode 100644 index 0000000..4a619b4 --- /dev/null +++ b/data/images/Makefile.am @@ -0,0 +1,57 @@ +imagesdir = $(datadir)/gnome15/images +images_DATA = g15key.png \ + g15key-error.png \ + g19-background.svg \ + mx5500-background.svg \ + default-background.svg \ + locked.png \ + key-g1.png \ + key-g2.png \ + key-g3.png \ + key-g4.png \ + key-g5.png \ + key-g6.png \ + key-g7.png \ + key-g8.png \ + key-g9.png \ + key-g10.png \ + key-g11.png \ + key-g12.png \ + key-g13.png \ + key-g14.png \ + key-g15.png \ + key-g16.png \ + key-g17.png \ + key-g18.png \ + key-g19.png \ + key-g20.png \ + key-g21.png \ + key-g22.png \ + key-l1.png \ + key-l2.png \ + key-l3.png \ + key-l4.png \ + key-l5.png \ + key-menu.png \ + key-left.png \ + key-right.png \ + key-up.png \ + key-down.png \ + key-ok.png \ + key-back.png \ + key-settings.png \ + key-light.png \ + key-m1.png \ + key-m2.png \ + key-m3.png \ + key-mr.png \ + key-vol-up.png \ + key-vol-down.png \ + key-mute.png \ + key-next.png \ + key-prev.png \ + key-play.png \ + key-stop.png + +EXTRA_DIST = \ + $(images_DATA) diff --git a/data/images/default-background.svg b/data/images/default-background.svg new file mode 100644 index 0000000..7d7ab42 --- /dev/null +++ b/data/images/default-background.svg @@ -0,0 +1,535 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + Gnome15 + Linux Desktop integration for the Logitech G Series + + Version ${version} + + + diff --git a/data/images/g15key-error.png b/data/images/g15key-error.png new file mode 100644 index 0000000..35e6b91 Binary files /dev/null and b/data/images/g15key-error.png differ diff --git a/data/images/g15key.png b/data/images/g15key.png new file mode 100644 index 0000000..96cc316 Binary files /dev/null and b/data/images/g15key.png differ diff --git a/data/images/g19-background.svg b/data/images/g19-background.svg new file mode 100644 index 0000000..08141a2 --- /dev/null +++ b/data/images/g19-background.svg @@ -0,0 +1,574 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + Gnome15 + Linux Desktop integration for the Logitech G-Series + + Version ${version} + + ${text} + + diff --git a/data/images/key-back.png b/data/images/key-back.png new file mode 100644 index 0000000..01b92d2 Binary files /dev/null and b/data/images/key-back.png differ diff --git a/data/images/key-down.png b/data/images/key-down.png new file mode 100644 index 0000000..b6c6eee Binary files /dev/null and b/data/images/key-down.png differ diff --git a/data/images/key-g1.png b/data/images/key-g1.png new file mode 100644 index 0000000..9b006e1 Binary files /dev/null and b/data/images/key-g1.png differ diff --git a/data/images/key-g10.png b/data/images/key-g10.png new file mode 100644 index 0000000..660251f Binary files /dev/null and b/data/images/key-g10.png differ diff --git a/data/images/key-g11.png b/data/images/key-g11.png new file mode 100644 index 0000000..7f40ef7 Binary files /dev/null and b/data/images/key-g11.png differ diff --git a/data/images/key-g12.png b/data/images/key-g12.png new file mode 100644 index 0000000..530aa1c Binary files /dev/null and b/data/images/key-g12.png differ diff --git a/data/images/key-g13.png b/data/images/key-g13.png new file mode 100644 index 0000000..9900e87 Binary files /dev/null and b/data/images/key-g13.png differ diff --git a/data/images/key-g14.png b/data/images/key-g14.png new file mode 100644 index 0000000..6e90464 Binary files /dev/null and b/data/images/key-g14.png differ diff --git a/data/images/key-g15.png b/data/images/key-g15.png new file mode 100644 index 0000000..3ac9244 Binary files /dev/null and b/data/images/key-g15.png differ diff --git a/data/images/key-g16.png b/data/images/key-g16.png new file mode 100644 index 0000000..0e50153 Binary files /dev/null and b/data/images/key-g16.png differ diff --git a/data/images/key-g17.png b/data/images/key-g17.png new file mode 100644 index 0000000..5a58eba Binary files /dev/null and b/data/images/key-g17.png differ diff --git a/data/images/key-g18.png b/data/images/key-g18.png new file mode 100644 index 0000000..86c4fb3 Binary files /dev/null and b/data/images/key-g18.png differ diff --git a/data/images/key-g19.png b/data/images/key-g19.png new file mode 100644 index 0000000..51d7d55 Binary files /dev/null and b/data/images/key-g19.png differ diff --git a/data/images/key-g2.png b/data/images/key-g2.png new file mode 100644 index 0000000..2fd7606 Binary files /dev/null and b/data/images/key-g2.png differ diff --git a/data/images/key-g20.png b/data/images/key-g20.png new file mode 100644 index 0000000..3400fa6 Binary files /dev/null and b/data/images/key-g20.png differ diff --git a/data/images/key-g21.png b/data/images/key-g21.png new file mode 100644 index 0000000..7f59e72 Binary files /dev/null and b/data/images/key-g21.png differ diff --git a/data/images/key-g22.png b/data/images/key-g22.png new file mode 100644 index 0000000..84f3bae Binary files /dev/null and b/data/images/key-g22.png differ diff --git a/data/images/key-g3.png b/data/images/key-g3.png new file mode 100644 index 0000000..6938628 Binary files /dev/null and b/data/images/key-g3.png differ diff --git a/data/images/key-g4.png b/data/images/key-g4.png new file mode 100644 index 0000000..a524e3f Binary files /dev/null and b/data/images/key-g4.png differ diff --git a/data/images/key-g5.png b/data/images/key-g5.png new file mode 100644 index 0000000..b27eb27 Binary files /dev/null and b/data/images/key-g5.png differ diff --git a/data/images/key-g6.png b/data/images/key-g6.png new file mode 100644 index 0000000..7f9ff10 Binary files /dev/null and b/data/images/key-g6.png differ diff --git a/data/images/key-g7.png b/data/images/key-g7.png new file mode 100644 index 0000000..c0b0fda Binary files /dev/null and b/data/images/key-g7.png differ diff --git a/data/images/key-g8.png b/data/images/key-g8.png new file mode 100644 index 0000000..73855f2 Binary files /dev/null and b/data/images/key-g8.png differ diff --git a/data/images/key-g9.png b/data/images/key-g9.png new file mode 100644 index 0000000..0c955a2 Binary files /dev/null and b/data/images/key-g9.png differ diff --git a/data/images/key-l1.png b/data/images/key-l1.png new file mode 100644 index 0000000..4be33d6 Binary files /dev/null and b/data/images/key-l1.png differ diff --git a/data/images/key-l2.png b/data/images/key-l2.png new file mode 100644 index 0000000..678c95b Binary files /dev/null and b/data/images/key-l2.png differ diff --git a/data/images/key-l3.png b/data/images/key-l3.png new file mode 100644 index 0000000..c90825b Binary files /dev/null and b/data/images/key-l3.png differ diff --git a/data/images/key-l4.png b/data/images/key-l4.png new file mode 100644 index 0000000..2587a7f Binary files /dev/null and b/data/images/key-l4.png differ diff --git a/data/images/key-l5.png b/data/images/key-l5.png new file mode 100644 index 0000000..45519f3 Binary files /dev/null and b/data/images/key-l5.png differ diff --git a/data/images/key-left.png b/data/images/key-left.png new file mode 100644 index 0000000..78adfe7 Binary files /dev/null and b/data/images/key-left.png differ diff --git a/data/images/key-light.png b/data/images/key-light.png new file mode 100644 index 0000000..6dd0449 Binary files /dev/null and b/data/images/key-light.png differ diff --git a/data/images/key-m1.png b/data/images/key-m1.png new file mode 100644 index 0000000..c79f6ba Binary files /dev/null and b/data/images/key-m1.png differ diff --git a/data/images/key-m2.png b/data/images/key-m2.png new file mode 100644 index 0000000..77d8722 Binary files /dev/null and b/data/images/key-m2.png differ diff --git a/data/images/key-m3.png b/data/images/key-m3.png new file mode 100644 index 0000000..ca18b76 Binary files /dev/null and b/data/images/key-m3.png differ diff --git a/data/images/key-menu.png b/data/images/key-menu.png new file mode 100644 index 0000000..684d01e Binary files /dev/null and b/data/images/key-menu.png differ diff --git a/data/images/key-mr.png b/data/images/key-mr.png new file mode 100644 index 0000000..77e432d Binary files /dev/null and b/data/images/key-mr.png differ diff --git a/data/images/key-mute.png b/data/images/key-mute.png new file mode 100644 index 0000000..5e139d0 Binary files /dev/null and b/data/images/key-mute.png differ diff --git a/data/images/key-next.png b/data/images/key-next.png new file mode 100644 index 0000000..8e8d854 Binary files /dev/null and b/data/images/key-next.png differ diff --git a/data/images/key-ok.png b/data/images/key-ok.png new file mode 100644 index 0000000..a143c10 Binary files /dev/null and b/data/images/key-ok.png differ diff --git a/data/images/key-play.png b/data/images/key-play.png new file mode 100644 index 0000000..914184f Binary files /dev/null and b/data/images/key-play.png differ diff --git a/data/images/key-prev.png b/data/images/key-prev.png new file mode 100644 index 0000000..69bffa7 Binary files /dev/null and b/data/images/key-prev.png differ diff --git a/data/images/key-right.png b/data/images/key-right.png new file mode 100644 index 0000000..3c99ebc Binary files /dev/null and b/data/images/key-right.png differ diff --git a/data/images/key-settings.png b/data/images/key-settings.png new file mode 100644 index 0000000..7341858 Binary files /dev/null and b/data/images/key-settings.png differ diff --git a/data/images/key-stop.png b/data/images/key-stop.png new file mode 100644 index 0000000..c00968f Binary files /dev/null and b/data/images/key-stop.png differ diff --git a/data/images/key-up.png b/data/images/key-up.png new file mode 100644 index 0000000..02fd26f Binary files /dev/null and b/data/images/key-up.png differ diff --git a/data/images/key-vol-down.png b/data/images/key-vol-down.png new file mode 100644 index 0000000..97d0c60 Binary files /dev/null and b/data/images/key-vol-down.png differ diff --git a/data/images/key-vol-up.png b/data/images/key-vol-up.png new file mode 100644 index 0000000..4c1789c Binary files /dev/null and b/data/images/key-vol-up.png differ diff --git a/data/images/locked.png b/data/images/locked.png new file mode 100644 index 0000000..6538bea Binary files /dev/null and b/data/images/locked.png differ diff --git a/data/images/mx5500-background.svg b/data/images/mx5500-background.svg new file mode 100644 index 0000000..84bf297 --- /dev/null +++ b/data/images/mx5500-background.svg @@ -0,0 +1,532 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + Gnome15 + Logitech on Linux + + Version ${version} + + + diff --git a/data/themes/Makefile.am b/data/themes/Makefile.am new file mode 100644 index 0000000..4977d17 --- /dev/null +++ b/data/themes/Makefile.am @@ -0,0 +1 @@ +SUBDIRS = default \ No newline at end of file diff --git a/data/themes/default/Makefile.am b/data/themes/default/Makefile.am new file mode 100644 index 0000000..3f1d30b --- /dev/null +++ b/data/themes/default/Makefile.am @@ -0,0 +1,22 @@ +themedir = $(datadir)/gnome15/themes/default +theme_DATA = default-menu-screen.svg \ + default-menu-entry.svg \ + default-menu-child-entry.svg \ + default-menu-separator.svg \ + default-confirmation-screen.svg \ + default-error-screen.svg \ + mx5500-menu-screen.svg \ + mx5500-menu-entry.svg \ + mx5500-menu-child-entry.svg \ + mx5500-menu-separator.svg \ + mx5500-confirmation-screen.svg \ + mx5500-error-screen.svg \ + g19-menu-screen.svg \ + g19-menu-entry.svg \ + g19-menu-child-entry.svg \ + g19-menu-separator.svg \ + g19-error-screen.svg \ + g19-confirmation-screen.svg + +EXTRA_DIST = \ + $(theme_DATA) diff --git a/data/themes/default/default-confirmation-screen.svg b/data/themes/default/default-confirmation-screen.svg new file mode 100644 index 0000000..b644c9c --- /dev/null +++ b/data/themes/default/default-confirmation-screen.svg @@ -0,0 +1,162 @@ + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + ${text} + ${title} + + L3 - No + L4 - Yes + + diff --git a/data/themes/default/default-error-screen.svg b/data/themes/default/default-error-screen.svg new file mode 100644 index 0000000..beecf00 --- /dev/null +++ b/data/themes/default/default-error-screen.svg @@ -0,0 +1,150 @@ + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + ${text} + ${title} + + L5 - Close + + diff --git a/data/themes/default/default-menu-child-entry.svg b/data/themes/default/default-menu-child-entry.svg new file mode 100644 index 0000000..9a9d8a8 --- /dev/null +++ b/data/themes/default/default-menu-child-entry.svg @@ -0,0 +1,223 @@ + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + ${item_name} + ${item_alt} + + + + + + + + + ${item_name} + ${item_alt} + + + + + + + diff --git a/data/themes/default/default-menu-entry.svg b/data/themes/default/default-menu-entry.svg new file mode 100644 index 0000000..0731610 --- /dev/null +++ b/data/themes/default/default-menu-entry.svg @@ -0,0 +1,221 @@ + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + ${item_name} + ${item_alt} + + + + + + + + + ${item_name} + ${item_alt} + + + + + + + diff --git a/data/themes/default/default-menu-screen.svg b/data/themes/default/default-menu-screen.svg new file mode 100644 index 0000000..a6262ba --- /dev/null +++ b/data/themes/default/default-menu-screen.svg @@ -0,0 +1,67 @@ + + + + + + + + + + + + + ${title} + + + + + + + ${alt_title} + + diff --git a/data/themes/default/default-menu-separator.svg b/data/themes/default/default-menu-separator.svg new file mode 100644 index 0000000..e533f80 --- /dev/null +++ b/data/themes/default/default-menu-separator.svg @@ -0,0 +1,122 @@ + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + diff --git a/data/themes/default/g19-confirmation-screen.svg b/data/themes/default/g19-confirmation-screen.svg new file mode 100644 index 0000000..6913b30 --- /dev/null +++ b/data/themes/default/g19-confirmation-screen.svg @@ -0,0 +1,263 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + ${title} + + Yes + No + + + + + + ${text} + + diff --git a/data/themes/default/g19-error-screen.svg b/data/themes/default/g19-error-screen.svg new file mode 100644 index 0000000..3364eb1 --- /dev/null +++ b/data/themes/default/g19-error-screen.svg @@ -0,0 +1,233 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + ${title} + + ${text} + Close + + Ok + + diff --git a/data/themes/default/g19-menu-child-entry.svg b/data/themes/default/g19-menu-child-entry.svg new file mode 100644 index 0000000..6366146 --- /dev/null +++ b/data/themes/default/g19-menu-child-entry.svg @@ -0,0 +1,338 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + ${item_name} + + ${item_alt} + + + + + + + + + ${item_name} + ${item_alt} + + + + + + diff --git a/data/themes/default/g19-menu-entry.svg b/data/themes/default/g19-menu-entry.svg new file mode 100644 index 0000000..e52a959 --- /dev/null +++ b/data/themes/default/g19-menu-entry.svg @@ -0,0 +1,407 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + ${item_name} + ${item_alt} + + + + + + + + + + ${item_name} + + + + + + + ${item_alt} + + diff --git a/data/themes/default/g19-menu-screen.svg b/data/themes/default/g19-menu-screen.svg new file mode 100644 index 0000000..8a1b8f7 --- /dev/null +++ b/data/themes/default/g19-menu-screen.svg @@ -0,0 +1,262 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + ${title} + + + ${alt_title} + + + + + + + diff --git a/data/themes/default/g19-menu-separator.svg b/data/themes/default/g19-menu-separator.svg new file mode 100644 index 0000000..d820d13 --- /dev/null +++ b/data/themes/default/g19-menu-separator.svg @@ -0,0 +1,210 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + diff --git a/data/themes/default/mx5500-confirmation-screen.svg b/data/themes/default/mx5500-confirmation-screen.svg new file mode 100644 index 0000000..5a41194 --- /dev/null +++ b/data/themes/default/mx5500-confirmation-screen.svg @@ -0,0 +1,177 @@ + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + ${text} + ${title} + + No + Yes + + + + diff --git a/data/themes/default/mx5500-error-screen.svg b/data/themes/default/mx5500-error-screen.svg new file mode 100644 index 0000000..390ae42 --- /dev/null +++ b/data/themes/default/mx5500-error-screen.svg @@ -0,0 +1,171 @@ + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + ${text} + ${title} + + Hold + + Close + + diff --git a/data/themes/default/mx5500-menu-child-entry.svg b/data/themes/default/mx5500-menu-child-entry.svg new file mode 100644 index 0000000..9f76490 --- /dev/null +++ b/data/themes/default/mx5500-menu-child-entry.svg @@ -0,0 +1,181 @@ + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + ${item_name} + ${item_alt} + + + + ${item_name} + ${item_alt} + + diff --git a/data/themes/default/mx5500-menu-entry.svg b/data/themes/default/mx5500-menu-entry.svg new file mode 100644 index 0000000..9b4a30b --- /dev/null +++ b/data/themes/default/mx5500-menu-entry.svg @@ -0,0 +1,206 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + ${item_name} + ${item_alt} + + + + ${item_name} + ${item_alt} + + diff --git a/data/themes/default/mx5500-menu-screen.svg b/data/themes/default/mx5500-menu-screen.svg new file mode 100644 index 0000000..82429a5 --- /dev/null +++ b/data/themes/default/mx5500-menu-screen.svg @@ -0,0 +1,146 @@ + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + ${title} + + + + + + + ${alt_title} + + diff --git a/data/themes/default/mx5500-menu-separator.svg b/data/themes/default/mx5500-menu-separator.svg new file mode 100644 index 0000000..50784d9 --- /dev/null +++ b/data/themes/default/mx5500-menu-separator.svg @@ -0,0 +1,122 @@ + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + diff --git a/data/udev/98-gnome15.rules.in b/data/udev/98-gnome15.rules.in new file mode 100644 index 0000000..01fa9b9 --- /dev/null +++ b/data/udev/98-gnome15.rules.in @@ -0,0 +1,8 @@ +# This file maintains permissions for Gnome15 input devices +# + +# Make uinput load +SUBSYSTEM=="input", RUN+="/sbin/modprobe uinput" + +# Set permissions for uinput devices +KERNEL=="event*|uinput", GROUP="@DEVICEGROUP@", MODE="@DEVICEMODE@" \ No newline at end of file diff --git a/data/udev/99-gnome15-g15direct.rules.in b/data/udev/99-gnome15-g15direct.rules.in new file mode 100644 index 0000000..65a58d7 --- /dev/null +++ b/data/udev/99-gnome15-g15direct.rules.in @@ -0,0 +1,30 @@ +# This file maintains permissions for Gnome15's "G15 Direct" driver. These are the +# defaults, you might want to tighten them up a bit. +# +# See udev(7) for syntax. +# + +# Logitech G15 Gaming Keyboard +SUBSYSTEM=="usb", ACTION=="add", ATTRS{idVendor}=="046d", ATTRS{idProduct}=="c222", MODE="@DEVICEMODE@", GROUP="@DEVICEGROUP@" + +# Logitech G15 Gaming Keyboard (version 2) +SUBSYSTEM=="usb", ACTION=="add", ATTRS{idVendor}=="046d", ATTRS{idProduct}=="c227", MODE="@DEVICEMODE@", GROUP="@DEVICEGROUP@" + +# Logitech G13 Advanced Gameboard +SUBSYSTEM=="usb", ACTION=="add", ATTRS{idVendor}=="046d", ATTRS{idProduct}=="c21c", MODE="@DEVICEMODE@", GROUP="@DEVICEGROUP@" + +# Logitech Z10 Speakers +SUBSYSTEM=="usb", ACTION=="add", ATTRS{idVendor}=="046d", ATTRS{idProduct}=="0a07", MODE="@DEVICEMODE@", GROUP="@DEVICEGROUP@" + +# Logitech G11 Keyboard (no LCD) +SUBSYSTEM=="usb", ACTION=="add", ATTRS{idVendor}=="046d", ATTRS{idProduct}=="c225", MODE="@DEVICEMODE@", GROUP="@DEVICEGROUP@" + +# Logitech G110 Keyboard (no LCD) +SUBSYSTEM=="usb", ACTION=="add", ATTRS{idVendor}=="046d", ATTRS{idProduct}=="c22b", MODE="@DEVICEMODE@", GROUP="@DEVICEGROUP@" + +# Logitech Game Panel (Dell XPS 1730?) +SUBSYSTEM=="usb", ACTION=="add", ATTRS{idVendor}=="046d", ATTRS{idProduct}=="c251", MODE="@DEVICEMODE@", GROUP="@DEVICEGROUP@" + +# Logitech G510 +SUBSYSTEM=="usb", ACTION=="add", ATTRS{idVendor}=="046d", ATTRS{idProduct}=="c22d", MODE="@DEVICEMODE@", GROUP="@DEVICEGROUP@" +SUBSYSTEM=="usb", ACTION=="add", ATTRS{idVendor}=="046d", ATTRS{idProduct}=="c22e", MODE="@DEVICEMODE@", GROUP="@DEVICEGROUP@" diff --git a/data/udev/99-gnome15-g19direct.rules.in b/data/udev/99-gnome15-g19direct.rules.in new file mode 100644 index 0000000..d1ebeb4 --- /dev/null +++ b/data/udev/99-gnome15-g19direct.rules.in @@ -0,0 +1,8 @@ +# This file maintains permissions for Gnome15's "G19 Direct" driver. These are the +# defaults, you might want to tighten them up a bit. +# +# See udev(7) for syntax. +# + +# Logitech G19 Gaming Keyboard +SUBSYSTEM=="usb", ACTION=="add", ATTRS{idVendor}=="046d", ATTRS{idProduct}=="c229", MODE="@DEVICEMODE@", GROUP="@DEVICEGROUP@" diff --git a/data/udev/99-gnome15-g930.rules.in b/data/udev/99-gnome15-g930.rules.in new file mode 100644 index 0000000..6e319df --- /dev/null +++ b/data/udev/99-gnome15-g930.rules.in @@ -0,0 +1,6 @@ +# This file maintains permissions for Logitech G keyboard devices +# See udev(7) for syntax. +# + +# Logitech G930 Headset +SUBSYSTEM=="input", ACTION=="add", KERNEL=="event*", ATTRS{idVendor}=="046d", ATTRS{idProduct}=="0xa1f", MODE="@DEVICEMODE@", GROUP="@DEVICEGROUP@", SYMLINK+="g930-extra-keys" diff --git a/data/udev/99-gnome15-kernel.rules.in b/data/udev/99-gnome15-kernel.rules.in new file mode 100644 index 0000000..3a70cd7 --- /dev/null +++ b/data/udev/99-gnome15-kernel.rules.in @@ -0,0 +1,39 @@ +# This file maintains permissions for Logitech G keyboard devices +# See udev(7) for syntax. +# + +# Logitech G19 Gaming Keyboard +SUBSYSTEM=="input", ACTION=="add", KERNEL=="event*", ATTRS{idVendor}=="046d", ATTRS{idProduct}=="c229", MODE="@DEVICEMODE@", GROUP="@DEVICEGROUP@", SYMLINK+="g19-extra-keys" +SUBSYSTEMS=="usb", ACTION=="add", KERNEL=="fb[0-9]*", ATTRS{idVendor}=="046d", ATTRS{idProduct}=="c229", MODE="@DEVICEMODE@", GROUP="@DEVICEGROUP@", SYMLINK+="g19-lcd" + +# Logitech G15 Gaming Keyboard (version 1) +SUBSYSTEM=="input", ACTION=="add", KERNEL=="event*", ATTRS{idVendor}=="046d", ATTRS{idProduct}=="c222", MODE="@DEVICEMODE@", GROUP="@DEVICEGROUP@", SYMLINK+="g15v1-extra-keys" +SUBSYSTEMS=="usb", ACTION=="add", KERNEL=="fb[0-9]*", ATTRS{idVendor}=="046d", ATTRS{idProduct}=="c222", MODE="@DEVICEMODE@", GROUP="@DEVICEGROUP@", SYMLINK+="g15v1-lcd" + +# Logitech G15 Gaming Keyboard (version 2) +SUBSYSTEM=="input", ACTION=="add", KERNEL=="event*", ATTRS{idVendor}=="046d", ATTRS{idProduct}=="c227", MODE="@DEVICEMODE@", GROUP="@DEVICEGROUP@", SYMLINK+="g15v2-extra-keys" +SUBSYSTEMS=="usb", ACTION=="add", KERNEL=="fb[0-9]*", ATTRS{idVendor}=="046d", ATTRS{idProduct}=="c227", MODE="@DEVICEMODE@", GROUP="@DEVICEGROUP@", SYMLINK+="g15v2-lcd" + +# Logitech G13 Advanced Gameboard +SUBSYSTEM=="input", ACTION=="add", KERNEL=="event*", ATTRS{idVendor}=="046d", ATTRS{idProduct}=="c21c", MODE="@DEVICEMODE@", GROUP="@DEVICEGROUP@", SYMLINK+="g13-extra-keys" +SUBSYSTEMS=="usb", ACTION=="add", KERNEL=="fb[0-9]*", ATTRS{idVendor}=="046d", ATTRS{idProduct}=="c21c", MODE="@DEVICEMODE@", GROUP="@DEVICEGROUP@", SYMLINK+="g13-lcd" + +# Logitech G510 Keyboard +SUBSYSTEM=="input", ACTION=="add", KERNEL=="event*", ATTRS{idVendor}=="046d", ATTRS{idProduct}=="c22d", MODE="@DEVICEMODE@", GROUP="@DEVICEGROUP@", SYMLINK+="g510-extra-keys" +SUBSYSTEMS=="usb", ACTION=="add", KERNEL=="fb[0-9]*", ATTRS{idVendor}=="046d", ATTRS{idProduct}=="c22d", MODE="@DEVICEMODE@", GROUP="@DEVICEGROUP@", SYMLINK+="g510-lcd" +SUBSYSTEM=="input", ACTION=="add", KERNEL=="event*", ATTRS{idVendor}=="046d", ATTRS{idProduct}=="c22e", MODE="@DEVICEMODE@", GROUP="@DEVICEGROUP@", SYMLINK+="g510-extra-keys" +SUBSYSTEMS=="usb", ACTION=="add", KERNEL=="fb[0-9]*", ATTRS{idVendor}=="046d", ATTRS{idProduct}=="c22e", MODE="@DEVICEMODE@", GROUP="@DEVICEGROUP@", SYMLINK+="g510-lcd" + +# Logitech Z10 Speakers +SUBSYSTEM=="input", ACTION=="add", KERNEL=="event*", ATTRS{idVendor}=="046d", ATTRS{idProduct}=="0a07", MODE="@DEVICEMODE@", GROUP="@DEVICEGROUP@", SYMLINK+="z10-extra-keys" +SUBSYSTEMS=="usb", ACTION=="add", KERNEL=="fb[0-9]*", ATTRS{idVendor}=="046d", ATTRS{idProduct}=="0a07", MODE="@DEVICEMODE@", GROUP="@DEVICEGROUP@", SYMLINK+="z10-lcd" + +# Logitech G11 Keyboard (no LCD) +SUBSYSTEM=="input", ACTION=="add", KERNEL=="event*", ATTRS{idVendor}=="046d", ATTRS{idProduct}=="c225", MODE="@DEVICEMODE@", GROUP="@DEVICEGROUP@", SYMLINK+="g11-extra-keys" + +# Logitech G110 Keyboard (no LCD) +SUBSYSTEM=="input", ACTION=="add", KERNEL=="event*", ATTRS{idVendor}=="046d", ATTRS{idProduct}=="c22b", MODE="@DEVICEMODE@", GROUP="@DEVICEGROUP@", SYMLINK+="g110-extra-keys" + +# Logitech Game Panel (Dell XPS 1730?) +SUBSYSTEM=="input", ACTION=="add", KERNEL=="event*", ATTRS{idVendor}=="046d", ATTRS{idProduct}=="c251", MODE="@DEVICEMODE@", GROUP="@DEVICEGROUP@", SYMLINK+="gamepanel-extra-keys" +SUBSYSTEMS=="usb", ACTION=="add", KERNEL=="fb[0-9]*", ATTRS{idVendor}=="046d", ATTRS{idProduct}=="c251", MODE="@DEVICEMODE@", GROUP="@DEVICEGROUP@", SYMLINK+="gamepanel-lcd" diff --git a/data/udev/Makefile.am b/data/udev/Makefile.am new file mode 100644 index 0000000..1aed22f --- /dev/null +++ b/data/udev/Makefile.am @@ -0,0 +1,20 @@ +if ENABLE_DRIVER_KERNEL + MAYBE_KERNEL = 99-gnome15-kernel.rules +endif + +if ENABLE_DRIVER_G19DIRECT + MAYBE_G19DIRECT = 99-gnome15-g19direct.rules +endif + +if ENABLE_DRIVER_G15DIRECT + MAYBE_G15DIRECT = 99-gnome15-g15direct.rules +endif + +if ENABLE_DRIVER_G930 + MAYBE_G930 = 99-gnome15-g930.rules +endif + +udevdir = @UDEV_RULES_PATH@ +udev_DATA = 98-gnome15.rules $(MAYBE_KERNEL) $(MAYBE_G19DIRECT) $(MAYBE_G15DIRECT) $(MAYBE_G930) + +EXTRA_DIST = 98-gnome15.rules 99-gnome15-g19direct.rules 99-gnome15-kernel.rules 99-gnome15-g15direct.rules 99-gnome15-g930.rules \ No newline at end of file diff --git a/data/ui/Makefile.am b/data/ui/Makefile.am new file mode 100644 index 0000000..0dbfc30 --- /dev/null +++ b/data/ui/Makefile.am @@ -0,0 +1,16 @@ +uidir = $(datadir)/gnome15/ui +ui_DATA = driver_gtk.ui \ + driver_kernel.ui \ + driver_g19direct.ui \ + driver_g15direct.ui \ + driver_g930.ui \ + g15-config.ui \ + password.ui \ + macro-editor.ui \ + script-editor.ui \ + colorpicker.ui \ + accounts.ui \ + redblue.png + +EXTRA_DIST = \ + $(ui_DATA) diff --git a/data/ui/accounts.ui b/data/ui/accounts.ui new file mode 100644 index 0000000..391913f --- /dev/null +++ b/data/ui/accounts.ui @@ -0,0 +1,409 @@ + + + + + + + + + + + + + + False + 5 + Accounts + False + True + center-on-parent + dialog + + + True + False + 2 + + + True + False + end + + + gtk-close + True + True + True + True + + + False + False + 0 + + + + + False + True + end + 0 + + + + + True + False + + + True + True + + + True + False + + + True + False + 0 + none + + + True + False + 12 + + + True + False + + + True + False + + + True + False + toolbutton1 + True + gtk-add + + + False + True + + + + + True + False + toolbutton2 + True + gtk-remove + + + False + True + + + + + False + False + 0 + + + + + True + True + automatic + automatic + in + + + 200 + 150 + True + True + AccountModel + False + False + 0 + + + URL + + + + 1 + 0 + + + + + + + + + True + True + 1 + + + + + + + + + True + False + <b>Accounts</b> + True + + + + + True + True + 0 + + + + + False + True + + + + + 300 + True + False + 4 + + + True + False + 4 + + + True + False + Type: + + + False + False + 0 + + + + + True + False + TypeModel + + + + 0 + + + + + True + True + 1 + + + + + False + True + 0 + + + + + 200 + True + False + 0 + none + + + True + False + 12 + + + True + False + + + + + + + + + + True + False + <b>Account Options</b> + True + + + + + True + True + 1 + + + + + True + True + + + + + True + True + 0 + + + + + True + False + 0 + none + + + True + False + 12 + + + True + False + 4 + + + True + False + + + True + False + 0 + Check every + + + True + True + 0 + + + + + True + True + + True + False + False + True + True + UpdateAdjustment + + + True + True + 1 + + + + + True + False + minutes + + + True + True + 2 + + + + + True + True + 0 + + + + + + + + + + + + True + False + <b>Options</b> + True + + + + + True + True + 1 + + + + + True + True + 1 + + + + + + button9 + + + + + + + + + + + + pop3 + POP3 + + + imap + IMAP + + + + + 1 + 9999 + 1 + 1 + 1 + + diff --git a/data/ui/colorpicker.ui b/data/ui/colorpicker.ui new file mode 100644 index 0000000..67eb8d2 --- /dev/null +++ b/data/ui/colorpicker.ui @@ -0,0 +1,232 @@ + + + + + + 255 + 1 + 10 + + + 255 + 1 + 10 + + + False + 5 + Pick Colour + True + dialog + + + True + False + 2 + + + True + False + end + + + gtk-close + True + True + True + True + + + False + False + 0 + + + + + False + True + end + 0 + + + + + True + False + + + True + False + 8 + + + True + False + + + True + False + redblue.png + + + + + + + True + True + 0 + + + + + True + False + 2 + 2 + 34 + 8 + + + True + False + 0 + Red: + + + + + + + + True + False + 0 + Blue: + + + 1 + 2 + + + + + + True + True + + False + False + True + True + RAdjustment + + + 1 + 2 + + + + + + True + True + + False + False + True + True + BAdjustment + + + 1 + 2 + 1 + 2 + + + + + + False + False + 1 + + + + + False + False + 1 + + + + + + + + + button1 + + + + False + 5 + Pick Colour + True + dialog + + + True + False + 2 + + + True + False + end + + + gtk-close + True + True + True + True + + + False + False + 0 + + + + + False + True + end + 0 + + + + + True + False + #000000000000 + + + True + True + 1 + + + + + + + + + button2 + + + diff --git a/data/ui/driver_g15.ui b/data/ui/driver_g15.ui new file mode 100644 index 0000000..1a64c20 --- /dev/null +++ b/data/ui/driver_g15.ui @@ -0,0 +1,61 @@ + + + + + + 65535 + 1 + 10 + + + + True + False + + + True + False + + + 128 + True + False + 0 + Port + + + False + False + 0 + + + + + True + True + + True + False + False + True + True + PortAdjustment + 0.029999999999999999 + + + True + True + 4 + 1 + + + + + False + False + 4 + 0 + + + + diff --git a/data/ui/driver_g15direct.ui b/data/ui/driver_g15direct.ui new file mode 100644 index 0000000..7977df5 --- /dev/null +++ b/data/ui/driver_g15direct.ui @@ -0,0 +1,234 @@ + + + + + + + + + + + + + + macro + Emit macro keys + + + joystick + Analogue Joystick + + + mouse + Mouse + + + digital-joystick + Digital Joystick + + + + + 100 + 1 + 10 + + + 1000000 + 1 + 10 + + + 1000000 + 1 + 10 + + + False + 5 + Driver Settings + dialog + + + True + False + 2 + + + True + False + end + + + + + + gtk-close + True + True + True + True + + + False + False + 1 + + + + + False + True + end + 0 + + + + + True + False + 3 + 3 + 4 + 4 + + + + + + True + True + + True + False + False + True + True + TimeoutAdjustment + 0.029999999999999999 + + + 1 + 2 + + + + + 48 + True + False + 0 + 8 + ms + + + 2 + 3 + + + + + 128 + True + False + 0 + Timeout + + + + + True + False + 0 + Joystick mode + + + 1 + 2 + + + + + True + False + JoyModeModel + + + + 1 + + + + + 1 + 2 + 1 + 2 + + + + + True + False + 0 + Center Offset + + + 2 + 3 + + + + + True + True + + True + False + False + True + True + OffsetAdjustment + + + 1 + 2 + 2 + 3 + + + + + True + False + 0 + 0 + 8 + 8 + How far off-center the joystick must +be before registering movement. +Too low, and the joystick will "stick" +in one direction. Too high and you +lose accuracy. + + + 2 + 3 + 2 + 3 + + + + + False + True + 1 + + + + + + button1 + + + diff --git a/data/ui/driver_g19.ui b/data/ui/driver_g19.ui new file mode 100644 index 0000000..1a64c20 --- /dev/null +++ b/data/ui/driver_g19.ui @@ -0,0 +1,61 @@ + + + + + + 65535 + 1 + 10 + + + + True + False + + + True + False + + + 128 + True + False + 0 + Port + + + False + False + 0 + + + + + True + True + + True + False + False + True + True + PortAdjustment + 0.029999999999999999 + + + True + True + 4 + 1 + + + + + False + False + 4 + 0 + + + + diff --git a/data/ui/driver_g19direct.ui b/data/ui/driver_g19direct.ui new file mode 100644 index 0000000..cd2851d --- /dev/null +++ b/data/ui/driver_g19direct.ui @@ -0,0 +1,205 @@ + + + + + + False + 5 + Driver Settings + dialog + + + True + False + 2 + + + True + False + end + + + + + + gtk-close + True + True + True + True + + + False + False + 1 + + + + + False + True + end + 0 + + + + + True + False + + + True + False + + + 128 + True + False + 0 + Timeout + + + False + False + 0 + + + + + True + True + + True + False + False + True + True + TimeoutAdjustment + 0.029999999999999999 + + + True + True + 4 + 1 + + + + + 48 + True + False + 0.10000000149011612 + ms + + + False + False + 2 + + + + + False + False + 4 + 0 + + + + + True + False + + + 128 + True + False + then wait + + + True + True + 0 + + + + + True + True + + True + False + False + True + True + ResetWaitAdjustment + + + True + True + 1 + + + + + 48 + True + False + 0.10000000149011612 + ms + + + False + False + 2 + + + + + False + False + 1 + + + + + Reset device before use + True + True + False + True + + + False + False + 2 + + + + + True + True + 1 + + + + + + + + + button1 + + + + 1000000 + 1 + 10 + + + 1000000 + 1 + 10 + + diff --git a/data/ui/driver_g930.ui b/data/ui/driver_g930.ui new file mode 100644 index 0000000..638064c --- /dev/null +++ b/data/ui/driver_g930.ui @@ -0,0 +1,80 @@ + + + + + + + False + 5 + Driver Settings + dialog + + + True + False + 2 + + + True + False + end + + + + + + gtk-close + True + True + True + True + + + False + False + 1 + + + + + False + True + end + 0 + + + + + True + False + + + Grab multimedia keys + True + True + False + True + + + False + True + 0 + + + + + True + True + 1 + + + + + + + + + button1 + + + diff --git a/data/ui/driver_gtk.ui b/data/ui/driver_gtk.ui new file mode 100644 index 0000000..c5099ba --- /dev/null +++ b/data/ui/driver_gtk.ui @@ -0,0 +1,118 @@ + + + + + + False + 5 + Driver Settings + dialog + + + True + False + 2 + + + True + False + end + + + + + + gtk-close + True + True + True + True + + + False + False + 1 + + + + + False + True + end + 0 + + + + + True + False + + + 128 + True + False + 0 + Mode: + + + False + False + 0 + + + + + True + False + ModeModel + + + + 0 + + + + + True + True + 1 + + + + + False + False + 1 + + + + + + + + + button1 + + + + + + + + + + g19 + + + g15v1 + + + g15v2 + + + g13 + + + + + diff --git a/data/ui/driver_kernel.ui b/data/ui/driver_kernel.ui new file mode 100644 index 0000000..3745f6c --- /dev/null +++ b/data/ui/driver_kernel.ui @@ -0,0 +1,206 @@ + + + + + + + + + + + + g19 + + + g15v1 + + + g15v2 + + + g13 + + + auto + + + + + + + + + + + + + macro + Emit Macro Keys + + + joystick + Analogue Joystick + + + mouse + Mouse + + + digital-joystick + Digital Joystick + + + + + + False + 5 + Driver Settings + dialog + + + True + False + 2 + + + True + False + end + + + + + + gtk-close + True + True + True + True + + + False + False + 1 + + + + + False + True + end + 0 + + + + + True + False + 3 + 3 + 4 + 4 + + + 128 + True + False + 0 + Device: + + + + + True + False + DeviceModel + + + + 0 + + + + + 1 + 3 + + + + + Calibrate + True + True + True + + + 2 + 3 + 1 + 2 + + + + + + Grab multimedia keys + True + True + False + True + + + 3 + 2 + 3 + + + + + 128 + True + False + 0 + Joystick mode: + + + 1 + 2 + + + + + True + False + JoyModeModel + + + + 1 + + + + + 1 + 2 + 1 + 2 + + + + + False + True + 1 + + + + + + + + + button1 + + + diff --git a/data/ui/g15-config.ui b/data/ui/g15-config.ui new file mode 100644 index 0000000..b6c7af4 --- /dev/null +++ b/data/ui/g15-config.ui @@ -0,0 +1,3393 @@ + + + + + + False + 5 + About Plugin + center + dialog + + + True + False + 2 + + + True + False + end + + + gtk-close + True + True + True + True + + + False + False + 0 + + + + + False + False + end + 0 + + + + + True + False + 4 + + + True + False + + + True + False + 0 + Author: + + + + + + False + False + 0 + + + + + True + False + 0 + A. Author <a_user@company.com> + + + False + False + 1 + + + + + True + True + 0 + + + + + True + False + + + True + False + 0 + Copyright: + + + + + + False + False + 0 + + + + + True + False + 0 + Copyright © 2006 A. Author + + + False + False + 1 + + + + + True + True + 1 + + + + + True + False + + + True + False + 0 + Site: + + + + + + False + False + 0 + + + + + True + True + True + none + 0 + http://glade.gnome.org + + + False + False + 1 + + + + + True + True + 2 + + + + + False + True + 1 + + + + + + CancelMacroButton + + + + False + 5 + True + center-on-parent + normal + True + MainWindow + question + Remove Macro + Are you sure you wish to remove this macro? + True + + + True + False + 2 + + + True + False + end + + + gtk-remove + True + True + True + True + + + False + False + 0 + + + + + gtk-close + True + True + True + True + + + False + False + 1 + + + + + False + True + end + 0 + + + + + + button10 + button11 + + + + False + 5 + True + center-on-parent + normal + True + MainWindow + question + Remove Profile + Are you sure you wish to remove this profile? + True + + + True + False + 2 + + + True + False + end + + + gtk-remove + True + True + True + True + + + False + False + 0 + + + + + gtk-close + True + True + True + True + + + False + False + 1 + + + + + False + True + end + 0 + + + + + + button6 + button7 + + + + False + 5 + Copy Existing Profile + True + center-on-parent + 320 + 98 + normal + MainWindow + + + True + False + 2 + + + True + False + end + + + gtk-ok + True + True + True + True + True + True + + + False + False + 0 + + + + + gtk-cancel + True + True + True + True + + + False + False + 1 + + + + + False + True + end + 0 + + + + + True + False + + + True + False + 0 + Copied Profile Name: + + + False + False + 0 + + + + + True + True + + True + True + False + False + True + True + + + True + True + 1 + + + + + False + False + 1 + + + + + + button21 + button41 + + + + 100 + 1 + 10 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 540 + 440 + False + 5 + Global Options + center + dialog + + + True + False + 2 + + + True + False + end + + + gtk-close + True + True + True + True + + + False + False + 0 + + + + + False + False + end + 0 + + + + + True + False + 4 + + + Only Show Indicator On Error + True + True + False + 0.54000002145767212 + True + + + False + False + 0 + + + + + Start Desktop Service On Login + True + True + False + True + + + False + False + 1 + + + + + Start Indicator On Login + True + True + False + True + + + False + False + 2 + + + + + Start System Tray Icon On Login + True + True + False + True + + + False + False + 3 + + + + + Enable GNOME Shell extension (must restart GNOME Shell) + True + True + False + True + + + False + False + 4 + + + + + True + False + 0 + none + + + True + False + 12 + + + True + False + 8 + + + 220 + True + True + + + True + True + GlobalPluginModel + + + Enabled + + + + 1 + 0 + + + + + + + Name + + + + 1 + 2 + + + + + + + + + False + False + 0 + + + + + True + False + + + True + False + 0 + 0 + Plugin Name + + + + + + False + True + 4 + 0 + + + + + True + False + + + True + False + 0 + Description: + + + + + + False + False + 0 + + + + + True + True + + + True + False + + + True + False + 8 + 8 + 8 + 8 + + + True + False + 0 + 0 + Description of plugin. <b>bold</b> + True + True + word-char + 60 + + + + + + + + + True + True + 4 + 1 + + + + + True + True + 1 + + + + + True + False + 8 + + + gtk-preferences + True + True + True + True + + + False + False + 0 + + + + + gtk-about + True + True + True + True + + + False + False + 1 + + + + + False + False + 2 + + + + + True + True + 1 + + + + + + + + + True + False + 4 + <b>Global Plugins</b> + True + + + + + True + True + 5 + + + + + True + True + 1 + + + + + + CancelMacroButton1 + + + + False + 5 + True + center-on-parent + normal + True + MainWindow + error + Error importing profile + Error message to display + True + + + True + False + 2 + + + True + False + end + + + gtk-close + True + True + True + True + + + False + False + 0 + + + + + False + True + end + 0 + + + + + + ImportProfileErrorCloseButton + + + + + + + + + + + + + + + + + + + + False + 5 + Logitech G Keyboard Configuration + center + normal + + + True + False + 2 + + + True + False + end + + + Stop Service + True + True + True + + + False + False + 0 + True + + + + + gtk-close + True + True + True + True + True + True + + + False + False + 1 + + + + + Configure + True + True + True + + + False + False + 2 + True + + + + + False + True + end + 0 + + + + + True + True + + + True + True + never + automatic + in + + + True + True + DeviceModel + 1 + 0 + + + + 0 + + + + + + 4 + 1 + 3 + 2 + + + + + + + False + True + + + + + True + False + 4 + + + True + False + + + True + False + + + True + False + + + 0 + 0 + True + False + 0 + Logitech Keyboard Device + + + + + + True + True + 0 + + + + + Enabled + True + True + False + True + + + False + False + 1 + + + + + False + False + 8 + 0 + + + + + True + True + + + True + False + 4 + + + True + False + 18 + + + True + False + 0 + none + + + True + False + 12 + + + True + False + + + + + + + + + + True + False + <b>Controls</b> + True + + + + + True + True + 0 + + + + + True + False + 0 + none + + + True + False + 0 + 24 + + + True + False + + + + + + + + + + True + False + <b>Switches</b> + True + + + + + True + True + 1 + + + + + False + False + 0 + + + + + True + False + 0 + none + + + True + False + 8 + 12 + + + True + False + 4 + + + Cycle screens + True + True + False + True + + + False + False + 0 + + + + + True + False + 32 + + + True + False + + + True + False + 0 + Every + + + False + False + 4 + 0 + + + + + True + True + + True + False + False + True + True + CycleAdjustment + + + False + False + 1 + + + + + True + False + seconds + + + False + False + 4 + 2 + + + + + + + False + False + 4 + 1 + + + + + + + + + True + False + <b>Options</b> + True + + + + + False + False + 1 + + + + + True + False + 0 + none + + + True + False + 12 + + + True + False + + + 240 + True + False + DriverModel + + + + 1 + + + + + False + True + 0 + + + + + Options + True + True + True + + + False + True + 4 + 1 + + + + + + + + + True + False + <b>Driver</b> + True + + + + + False + True + 2 + + + + + + + True + False + Keyboard + + + False + + + + + True + False + + + True + True + + + True + False + 0 + none + + + True + False + 12 + + + True + False + 4 + + + True + False + 4 + + + gtk-new + True + True + True + True + + + False + False + 0 + + + + + Import + True + True + True + + + True + True + 4 + 1 + + + + + False + True + 4 + 0 + + + + + True + True + automatic + automatic + in + + + 300 + True + True + ProfileModel + False + False + 0 + + + column + + + + 5 + + + + + + + Name + True + + + + 4 + 3 + 0 + 1 + + + + + + + Id + + + + + + + True + True + 1 + + + + + + + + + True + False + <b>Profiles</b> + True + + + + + False + True + + + + + True + True + 3 + + + True + False + 2 + 4 + + + True + False + + + True + False + toolbutton2 + True + gtk-new + + + False + True + + + + + True + False + toolbutton1 + True + gtk-delete + + + False + True + + + + + True + False + toolbutton2 + True + gtk-properties + + + False + True + + + + + False + False + 0 + + + + + True + True + automatic + automatic + + + True + True + MacroModel + False + 1 + + + On + + + + 5 + + + + + + + 0 + Key + True + True + True + 2 + + + + 0 + + + + + + + Name + + + + 3 + 1 + + + + + + + + + True + True + 1 + + + + + True + False + 0 + none + + + True + False + 12 + + + True + False + + + True + False + 4 + + + M1 + True + True + False + True + True + + + False + False + 0 + + + + + M2 + True + True + False + True + M1 + + + False + False + 1 + + + + + M3 + True + True + False + True + M1 + + + False + False + 2 + + + + + True + True + 0 + + + + + + + + + + + + True + False + <b>Memory Bank</b> + True + + + + + False + False + 2 + + + + + + + True + False + Macros + + + False + + + + + True + False + 4 + 4 + 4 + + + True + False + + + Activate this profile when no others are +active + True + True + False + 0 + True + + + False + False + 4 + 0 + + + + + Activate this profile when a window with +the following title has focus + True + True + False + 0 + True + + + False + False + 4 + 1 + + + + + 120 + True + False + 1 + + + 120 + True + False + 0 + 30 + Window + + + False + False + 0 + + + + + True + True + + False + False + True + True + + + True + True + 1 + + + + + 24 + True + True + True + Select a window from those active + WindowSelectImage + + + True + True + 2 + + + + + False + True + 4 + 2 + + + + + Activate when a command launched through +g15-launch matches the following pattern + True + True + False + True + + + False + False + 3 + + + + + 120 + True + False + 1 + + + 120 + True + False + 0 + 30 + Pattern + + + False + False + 0 + + + + + True + False + True + + True + False + False + True + True + + + True + True + 1 + + + + + False + False + 4 + 4 + + + + + True + False + 0 + 4 + 4 + Use macros from another profile when they are not set in this one + True + True + + + False + False + 5 + + + + + 120 + True + False + 1 + + + 120 + True + False + 0 + 30 + Profile + + + False + False + 0 + + + + + True + False + ParentProfileModel + + + + 1 + + + + + True + True + 1 + + + + + False + False + 8 + 6 + + + + + + + + + + 1 + + + + + True + False + Activation + + + 1 + False + + + + + True + False + 4 + 4 + 4 + 4 + + + True + False + 4 + + + Send delays with keystrokes + True + True + False + 0 + True + + + False + False + 0 + + + + + Use fixed delay + True + True + False + 0 + True + + + False + True + 4 + 1 + + + + + True + False + + + 150 + True + False + 0 + Press for + + + True + False + 0 + + + + + True + True + + True + False + False + True + True + PressDelayAdjustment + 2 + + + True + True + 1 + + + + + False + False + 4 + 2 + + + + + True + False + + + 150 + True + False + 0 + Release, then wait + + + True + False + 0 + + + + + True + True + + True + False + False + True + True + ReleaseDelayAdjustment + 2 + + + True + True + 1 + + + + + False + False + 4 + 3 + + + + + + + 2 + + + + + True + False + Delays + + + 2 + False + + + + + True + False + 4 + 4 + 4 + 4 + + + True + False + + + True + False + 2 + 2 + + + + + + + + + True + False + 0 + Author + + + + + True + True + + True + False + False + True + True + + + 1 + 2 + + + + + False + False + 4 + 0 + + + + + 120 + True + False + 0 + none + + + 120 + True + False + 12 + + + True + False + 4 + + + True + False + 2 + 3 + 8 + 4 + + + True + True + True + + + 48 + 48 + True + False + 1 + gtk-missing-image + + + + + 1 + 2 + + + + + + + True + True + True + + + 48 + 48 + True + False + 1 + gtk-missing-image + + + + + 1 + 2 + 1 + 2 + + + + + + + Clear + True + True + True + 0 + + + 2 + 3 + + + + + + + Clear + True + True + True + 0 + + + 2 + 3 + 1 + 2 + + + + + + + True + False + 0 + Icon + + + GTK_FILL + + + + + + True + False + 0 + Background + + + 1 + 2 + GTK_FILL + + + + + + False + True + 0 + + + + + + + + + True + False + 24 + <b>Images</b> + True + center + + + + + True + True + 1 + + + + + True + False + 4 + + + True + False + 8 + + + Get more profiles or upload yours + True + True + True + none + http://www.russo79.com/forum/gnome-15/macro-profiles + + + True + True + 0 + + + + + True + True + 0 + + + + + Export + True + True + True + + + False + False + 1 + + + + + False + False + 4 + 2 + + + + + + + 3 + + + + + True + False + Information + + + 3 + False + + + + + True + False + 8 + + + True + False + + + True + False + ProfilePluginsModeModel + + + + 1 + + + + + False + False + 4 + 0 + + + + + True + True + automatic + automatic + in + + + True + True + EnabledProfilePluginsModel + False + False + 0 + + + Enabled + + + + 0 + + + + + + + Plugin + + + + 1 + + + + + + + + + True + True + 1 + + + + + True + True + 0 + + + + + 4 + + + + + True + False + Plugins + + + 4 + False + + + + + + + + + + + True + True + + + + + True + True + 4 + 0 + + + + + 1 + + + + + True + False + Macros + + + 1 + False + + + + + True + False + + + 200 + True + True + 1 + in + + + True + True + PluginModel + False + False + 0 + + + Enabled + + + + 0 + + + + + + + Plugin + + + + 1 + + + + + + + False + Id + + + + 2 + + + + + + + + + False + False + 0 + + + + + True + False + 8 + 8 + 8 + + + True + False + + + True + False + 0 + 0 + Plugin Name + + + + + + False + False + 10 + 0 + + + + + True + False + 4 + + + False + True + + + True + False + 0 + Supported Models: + + + + + + False + False + 0 + + + + + True + False + 0 + 0 + Models supported. + True + True + word-char + + + True + True + 1 + + + + + False + False + 0 + + + + + True + False + + + True + False + 0 + Description: + + + + + + False + False + 0 + + + + + True + True + + + True + False + + + True + False + 8 + 8 + 8 + 8 + + + True + False + 0 + 0 + Description of plugin. <b>bold</b> + True + True + word-char + 60 + + + + + + + + + True + True + 4 + 1 + + + + + True + True + 1 + + + + + True + False + 0 + none + + + True + False + 12 + + + True + False + 3 + 4 + + + True + False + gtk-save-as + + + 1 + 2 + + + + + + True + False + 0 + label + + + 2 + 3 + + + + + True + False + label + + + + + + + + + + + + True + False + <b>Keys</b> + True + + + + + False + False + 2 + + + + + True + False + 8 + + + gtk-preferences + True + True + False + True + + + False + False + 0 + + + + + gtk-about + True + True + True + True + + + False + False + 1 + + + + + True + False + 1 + Theme: + + + True + True + 2 + + + + + True + False + ThemeModel + + + + 1 + + + + + True + True + 3 + + + + + False + False + 3 + + + + + True + True + 1 + + + + + + + True + True + 1 + + + + + 2 + + + + + True + False + Plugins + + + 2 + False + + + + + True + True + 1 + + + + + False + True + There is no appropriate driver for the device <b>DeviceName</b>. +Do you have all the required packages installed? + + True + center + + + True + True + 2 + + + + + True + True + 0 + + + + + False + True + No device selected. + True + center + + + True + True + end + 1 + + + + + True + True + 0 + + + + + True + True + + + + + True + True + 1 + + + + + + StopServiceButton + button1 + GlobalOptionsButton + + + + False + 5 + New Profile + True + center-on-parent + 320 + 98 + normal + MainWindow + + + True + False + 2 + + + True + False + end + + + gtk-ok + True + True + True + True + True + True + + + False + False + 0 + + + + + gtk-cancel + True + True + True + True + + + False + False + 1 + + + + + False + True + end + 0 + + + + + True + False + + + True + False + 0 + Profile Name: + + + False + False + 0 + + + + + True + True + + True + False + False + True + True + + + True + True + 1 + + + + + False + False + 1 + + + + + + button2 + button4 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 100 + 0.050000000000000003 + 1 + + + True + False + + + gtk-remove + True + False + True + True + + + + + Export + True + False + Export this profile as an .mzip archive. + image2 + False + + + + + Duplicate + True + False + image3 + False + + + + + True + False + + + + + True + False + Make this profile the currently active one + Activate + True + + + + + True + False + Lock + True + + + + + True + False + Unlock + True + + + + + False + 5 + True + center-on-parent + normal + True + MainWindow + error + Profile already exists + The profile name you have supplied already exists, please +choose another name. + True + + + True + False + 2 + + + True + False + end + + + gtk-close + True + True + True + True + + + False + False + 0 + + + + + False + True + end + 0 + + + + + + button5 + + + + + + + + + + + + all + Enable All Available Plugins + + + none + Disable All Available Plugins + + + selected + Plugins Selected Below + + + + + 100 + 0.050000000000000003 + 1 + + + False + 5 + Select Window + True + center-on-parent + 320 + 98 + normal + MainWindow + + + True + False + 2 + + + True + False + end + + + gtk-close + True + True + True + True + right + + + False + False + 0 + + + + + False + True + end + 0 + + + + + True + False + + + 120 + True + False + Window + + + False + False + 0 + + + + + True + False + WindowModel + + + + 0 + + + + + True + True + 1 + + + + + False + False + 1 + + + + + + button8 + + + + + + + + + + + + True + False + gtk-zoom-in + + + True + False + gtk-zoom-in + + + True + False + gtk-save-as + + + True + False + gtk-copy + + diff --git a/data/ui/macro-editor.ui b/data/ui/macro-editor.ui new file mode 100644 index 0000000..58886bf --- /dev/null +++ b/data/ui/macro-editor.ui @@ -0,0 +1,901 @@ + + + + + + + + + + + + + + + + + + + + + + 0 + When Released + + + 1 + When Pressed + + + 2 + When Held + + + + + True + False + gtk-clear + + + + + + + + + + + mouse + Mouse Button + + + keyboard + Keyboard Key + + + joystick + Joystick Button + + + digital-joystick + Digital Joystick + + + command + Run Command + + + simple + Simple Macro + + + script + Macro Script + + + action + Action + + + + + + + + + + + + + 580 + False + 5 + Edit Macro + center-on-parent + normal + + + True + False + 2 + + + True + False + + + + + + False + True + 4 + 0 + + + + + True + False + end + + + gtk-close + True + True + True + True + True + True + + + False + False + 0 + + + + + False + True + end + 0 + + + + + True + False + 8 + + + True + False + + + True + False + 0 + none + + + True + False + 12 + + + + + + + + True + False + <b>Keys Pressed</b> + True + + + + + False + False + 0 + + + + + Allow combination of keys + True + True + False + True + + + + False + False + 4 + 1 + + + + + True + True + 0 + + + + + True + False + 4 + + + True + False + 4 + 2 + 16 + 8 + + + True + False + 0 + <b>Memory Bank:</b> + True + + + GTK_FILL + + + + + True + False + 0 + <b>Name:</b> + True + + + 1 + 2 + GTK_FILL + + + + + True + True + True + + 20 + False + False + True + True + + + + 1 + 2 + 1 + 2 + + + + + True + False + 0 + label + + + 1 + 2 + GTK_FILL + + + + + True + False + MapTypeModel + + + + + 1 + + + + + 1 + 2 + 2 + 3 + + + + + True + False + 0 + <b>Target:</b> + True + + + 2 + 3 + GTK_FILL + + + + + True + False + 0 + <b>Activation:</b> + True + + + 3 + 4 + + + + + True + False + ActivateOnModel + + + + + 1 + + + + + 1 + 2 + 3 + 4 + + + + + False + False + 0 + + + + + True + False + + + False + True + 1 + + + + + True + False + + + True + False + + + True + False + Filter: + + + False + False + 0 + + + + + True + True + + False + False + True + True + + + + True + True + 1 + + + + + True + True + True + ClearImage + + + + False + False + 2 + + + + + False + False + 4 + 0 + + + + + True + True + automatic + automatic + in + + + True + True + MappedKeyModel + False + False + 0 + + + + column + + + + 0 + + + + + + + + + True + True + 1 + + + + + True + True + 2 + + + + + True + False + + + True + False + 8 + + + True + True + + True + False + False + True + True + + + + True + True + 0 + + + + + _Browse + True + True + True + True + + + + False + False + 1 + + + + + False + False + 0 + + + + + Run command in background + True + True + False + True + + + + False + True + 4 + 1 + + + + + True + True + 4 + 3 + + + + + True + False + + + True + True + + True + False + False + True + True + + + + False + False + 0 + + + + + True + False + 0 + \r for Return, \e for escape, \b for backspace, \t for tab +\p for pause and \\ for backslash + True + word-char + + + + + + False + False + 1 + + + + + False + False + 4 + 4 + + + + + True + False + + + True + True + etched-in + + + 128 + True + True + + + + + True + True + 0 + + + + + True + False + + + + + + Editor + True + True + True + + + + False + False + 1 + + + + + False + False + 4 + 1 + + + + + True + True + 5 + + + + + True + False + + + True + True + automatic + automatic + in + + + True + True + ActionModel + False + False + 0 + + + + column + + + + 1 + + + + + + + + + True + True + 0 + + + + + True + True + 4 + 6 + + + + + True + False + 0 + none + + + True + False + 12 + + + True + False + 8 + + + True + False + 4 + + + True + False + 0 + Mode: + + + False + False + 0 + + + + + True + False + RepeatModel + + + + + 1 + + + + + True + True + 1 + + + + + False + False + 0 + + + + + Override default repeat delay + True + True + False + True + + + + True + True + 1 + + + + + True + False + 4 + + + True + False + Delay between repeats + + + True + True + 0 + + + + + True + True + + False + False + True + True + TurboAdjustment + 2 + + + + True + True + 1 + + + + + False + False + 2 + + + + + + + + + True + False + <b>Repetition</b> + True + + + + + False + False + 4 + 7 + + + + + False + True + 1 + + + + + True + True + 1 + + + + + + MacroEditCloseButton + + + + + + + + + + + + none + None + + + toggle + Toggle + + + held + When held + + + + + 0.050000000000000003 + 30 + 0.02 + 1 + + + + + + + + + + + BTN_TEST + 100 + + + KEY_TEST + 101 + + + + diff --git a/data/ui/password.ui b/data/ui/password.ui new file mode 100644 index 0000000..a333a01 --- /dev/null +++ b/data/ui/password.ui @@ -0,0 +1,161 @@ + + + + + + False + 5 + Password Required + True + center + dialog-password + normal + True + + + True + False + 2 + + + True + False + end + + + Ok + True + True + True + True + True + True + True + + + False + False + 0 + + + + + Cancel + True + True + True + + + False + False + 1 + + + + + False + True + end + 0 + + + + + True + False + 8 + + + True + False + 64 + dialog-password + 6 + + + True + True + 0 + + + + + True + False + 8 + + + True + False + Password Text + True + + + False + False + 0 + + + + + True + False + + + True + False + Password + + + True + False + 0 + + + + + True + True + False + + True + False + False + True + True + + + True + False + 1 + + + + + True + True + 1 + + + + + False + False + 1 + + + + + True + True + 1 + + + + + + togglebutton1 + button1 + + + diff --git a/data/ui/redblue.png b/data/ui/redblue.png new file mode 100644 index 0000000..bd9be8f Binary files /dev/null and b/data/ui/redblue.png differ diff --git a/data/ui/script-editor.ui b/data/ui/script-editor.ui new file mode 100644 index 0000000..7576935 --- /dev/null +++ b/data/ui/script-editor.ui @@ -0,0 +1,1508 @@ + + + + + + False + 5 + dialog + True + question + Add Delay + Provide the number of milliseconds to wait +for on this delay. + + + True + False + 2 + + + True + False + end + + + gtk-save + True + True + True + True + True + True + + + False + False + 0 + + + + + gtk-cancel + True + True + True + True + + + False + False + 1 + + + + + False + True + end + 0 + + + + + True + False + + + True + False + Delay + + + True + True + 0 + + + + + True + True + + False + False + True + True + DelayAdjustment + + + True + True + 1 + + + + + True + False + ms + + + False + False + 4 + 2 + + + + + True + True + 2 + + + + + + button6 + button7 + + + + False + 5 + dialog + True + question + Execute Command + Enter the name or path of a command to +execute. + + + True + False + 2 + + + True + False + end + + + gtk-save + True + True + True + True + True + True + + + False + False + 0 + + + + + gtk-cancel + True + True + True + True + + + False + False + 1 + + + + + False + True + end + 0 + + + + + True + False + 4 + + + True + False + Command + + + True + True + 0 + + + + + True + True + + False + False + True + True + + + True + True + 1 + + + + + Browse + True + True + True + + + + True + True + 2 + + + + + True + True + 2 + + + + + + button8 + button9 + + + + False + 5 + dialog + True + question + Add Goto + Choose the label to jump to. The label must +already exist. + + + True + False + 2 + + + True + False + end + + + gtk-save + True + True + True + True + True + True + + + False + False + 0 + + + + + gtk-cancel + True + True + True + True + + + False + False + 1 + + + + + False + True + end + 0 + + + + + True + False + 4 + + + True + False + Goto + + + True + True + 0 + + + + + True + False + GotoLabelModel + + + + 0 + + + + + True + True + 1 + + + + + True + True + 2 + + + + + + button10 + button13 + + + + False + 5 + dialog + True + question + Add Label + Enter the name of the label in this script. +You may then use Goto operations to +jump to this label. + + + True + False + 2 + + + True + False + end + + + gtk-save + True + True + True + True + True + True + + + False + False + 0 + + + + + gtk-cancel + True + True + True + True + + + False + False + 1 + + + + + False + True + end + 0 + + + + + True + False + 4 + + + True + False + Label + + + True + True + 0 + + + + + True + True + + True + False + False + True + True + + + True + True + 1 + + + + + True + True + 2 + + + + + + button11 + button12 + + + + False + 5 + dialog + True + question + Add Wait + You can halt the execution of the script either until all the +keys that trigger this macro are held, or until all the keys +the trigger this macro are released (depending on the +trigger type) + +This allows you to have up to 3 stages in your macro when +activated by press. The 2nd stage would start when the +key is held, and the 3rd and final stage when the keys are +released. + + + True + False + 2 + + + True + False + end + + + gtk-save + True + True + True + True + True + True + + + False + False + 0 + + + + + gtk-cancel + True + True + True + True + + + False + False + 1 + + + + + False + True + end + 0 + + + + + True + False + 4 + + + True + False + Wait + + + True + True + 0 + + + + + True + False + WaitModel + + + + 1 + + + + + True + True + 1 + + + + + True + True + 2 + + + + + + button16 + button17 + + + + 99999 + 1 + 10 + + + 480 + 540 + False + 5 + Script Editor + dialog + + + True + False + 2 + + + True + False + + + True + False + _Edit + True + + + True + False + + + gtk-cut + True + False + True + True + + + + + + gtk-copy + True + False + True + True + + + + + + gtk-paste + True + False + True + True + + + + + + gtk-delete + True + False + True + True + + + + + + True + False + + + + + True + False + Select All + True + + + + + + True + False + Select All Delays + True + + + + + + True + False + Select All Key Operations + True + + + + + + True + False + Select All Key Presses + True + + + + + + True + False + Select All Key Releases + True + + + + + + True + False + Select All Commands + True + + + + + + True + False + Deselect All + True + + + + + + True + False + + + + + True + False + Edit Selected Values + True + + + + + + + + + + True + False + _Help + True + + + True + False + + + gtk-help + True + False + True + True + + + + + + + + + False + True + 0 + + + + + True + False + end + + + gtk-save + True + True + True + True + True + True + + + False + False + 0 + + + + + gtk-cancel + True + True + True + True + + + False + False + 1 + + + + + False + True + end + 0 + + + + + True + False + + + True + False + Record keystrokes and insert them into the macro script + Record + True + gtk-media-record + + + + False + True + + + + + True + False + + + False + + + + + True + False + Remove selected macro operations + Remove + True + gtk-delete + + + + False + True + + + + + True + False + + + False + + + + + True + False + Delay + Delay + True + gtk-media-pause + + + + False + True + + + + + True + False + Execute command + Command + True + gtk-execute + + + + False + True + + + + + True + False + Add a script label + Label + True + gtk-underline + + + + False + True + + + + + True + False + Goto a script label + Goto + True + gtk-media-previous + + + + False + True + + + + + True + False + Wait + True + gtk-stop + + + + False + True + + + + + False + True + 2 + + + + + True + True + automatic + automatic + in + + + True + True + ScriptModel + True + + + + + + 16 + + + + 0 + + + + + + + Op + + + + 2 + + + + + + + Value + + + + + + 3 + 1 + + + + + + + + + True + True + 3 + + + + + True + False + + + + + + False + False + 4 + + + + + + + + + SaveButton + button1 + + + + + + + + + + + + + + + + True + False + gtk-media-record + 6 + + + 420 + False + 5 + dialog + True + other + Record + Record a key sequence and insert it into +this macro. + RecordImage + + + True + False + 2 + + + True + False + end + + + gtk-save + True + True + True + True + True + True + + + False + False + 0 + + + + + gtk-cancel + True + True + True + True + + + False + False + 1 + + + + + False + True + end + 0 + + + + + True + False + + + True + False + 0 + none + + + True + False + 12 + + + True + False + + + Output delays + True + True + False + True + + + + True + True + 0 + + + + + Emit UInput codes instead of X + True + True + False + True + + + + True + True + 1 + + + + + + + + + True + False + <b>Output</b> + True + + + + + True + True + 0 + + + + + True + False + 0 + none + + + True + False + 12 + + + True + False + 8 + + + True + False + 8 + + + gtk-media-record + True + True + True + True + + + + True + False + 0 + + + + + gtk-media-stop + True + True + True + True + + + + True + True + 1 + + + + + False + False + 0 + + + + + True + False + 0 + Recording status text ... + + + True + True + 1 + + + + + + + + + True + False + <b>Recording</b> + True + + + + + True + True + 1 + + + + + + + + True + True + 2 + + + + + + button14 + button15 + + + + False + 5 + dialog + True + question + Remove Macro Operations + Are you sure you wish to remove the selected macro +operations? + + + True + False + 2 + + + True + False + end + + + gtk-remove + True + True + True + True + + + False + False + 0 + + + + + gtk-close + True + True + True + True + + + False + False + 1 + + + + + False + True + end + 0 + + + + + + + + + button4 + button5 + + + + True + False + + + gtk-cut + True + False + True + True + + + + + + gtk-copy + True + False + True + True + + + + + + gtk-paste + True + False + True + True + + + + + + gtk-delete + True + False + True + True + + + + + + + + + + + + + + + + + + + False + 5 + dialog + True + Set Value + Set the value on all selected operations + + + True + False + 2 + + + True + False + end + + + gtk-save + True + True + True + True + True + True + + + False + False + 0 + + + + + gtk-cancel + True + True + True + True + + + False + False + 1 + + + + + False + True + end + 0 + + + + + True + False + + + True + False + Value + + + True + True + 0 + + + + + True + True + + False + False + True + True + + + True + True + 1 + + + + + True + True + 2 + + + + + + button2 + button3 + + + + + + + + + + + + Hold + Until All Keys Held + + + Release + Until All Keys Released + + + + diff --git a/data/ui/turbo.png b/data/ui/turbo.png new file mode 100644 index 0000000..834668a Binary files /dev/null and b/data/ui/turbo.png differ diff --git a/data/ukeys/Makefile.am b/data/ukeys/Makefile.am new file mode 100644 index 0000000..9d17153 --- /dev/null +++ b/data/ukeys/Makefile.am @@ -0,0 +1,9 @@ +ukeysdir = $(datadir)/gnome15/ukeys +ukeys_DATA = joystick.keys \ + digital-joystick.keys \ + mouse.keys \ + keyboard.keys \ + keysym-to-uinput + +EXTRA_DIST = \ + $(ukeys_DATA) diff --git a/data/ukeys/digital-joystick.keys b/data/ukeys/digital-joystick.keys new file mode 100644 index 0000000..a55cc68 --- /dev/null +++ b/data/ukeys/digital-joystick.keys @@ -0,0 +1,44 @@ +X_LEFT +X_RIGHT +Y_UP +Y_DOWN + +BTN_X +BTN_Y +BTN_Z +BTN_TL +BTN_TR +BTN_TL2 +BTN_TR2 +BTN_SELECT + +#BTN_0 +#BTN_1 +#BTN_2 +#BTN_3 +#BTN_4 +#BTN_5 +#BTN_6 +#BTN_7 +#BTN_8 +#BTN_9 +#BTN_TRIGGER +#BTN_THUMB +#BTN_THUMB2 +#BTN_TOP +#BTN_TOP2 +#BTN_PINKIE +#BTN_BASE +#BTN_BASE2 +#BTN_BASE3 +#BTN_BASE4 +#BTN_BASE5 +#BTN_BASE6 +#BTN_DEAD +#BTN_A +#BTN_B +#BTN_C +#BTN_START +#BTN_MODE +#BTN_THUMBL +#BTN_THUMBR \ No newline at end of file diff --git a/data/ukeys/joystick.keys b/data/ukeys/joystick.keys new file mode 100644 index 0000000..a55cc68 --- /dev/null +++ b/data/ukeys/joystick.keys @@ -0,0 +1,44 @@ +X_LEFT +X_RIGHT +Y_UP +Y_DOWN + +BTN_X +BTN_Y +BTN_Z +BTN_TL +BTN_TR +BTN_TL2 +BTN_TR2 +BTN_SELECT + +#BTN_0 +#BTN_1 +#BTN_2 +#BTN_3 +#BTN_4 +#BTN_5 +#BTN_6 +#BTN_7 +#BTN_8 +#BTN_9 +#BTN_TRIGGER +#BTN_THUMB +#BTN_THUMB2 +#BTN_TOP +#BTN_TOP2 +#BTN_PINKIE +#BTN_BASE +#BTN_BASE2 +#BTN_BASE3 +#BTN_BASE4 +#BTN_BASE5 +#BTN_BASE6 +#BTN_DEAD +#BTN_A +#BTN_B +#BTN_C +#BTN_START +#BTN_MODE +#BTN_THUMBL +#BTN_THUMBR \ No newline at end of file diff --git a/data/ukeys/keyboard.keys b/data/ukeys/keyboard.keys new file mode 100644 index 0000000..ec68f23 --- /dev/null +++ b/data/ukeys/keyboard.keys @@ -0,0 +1,378 @@ +KEY_0 +KEY_1 +KEY_2 +KEY_3 +KEY_4 +KEY_5 +KEY_6 +KEY_7 +KEY_8 +KEY_9 + +KEY_A +KEY_B +KEY_C +KEY_D +KEY_E +KEY_F +KEY_G +KEY_H +KEY_I +KEY_J +KEY_K +KEY_L +KEY_M +KEY_N +KEY_O +KEY_P +KEY_Q +KEY_R +KEY_S +KEY_T +KEY_U +KEY_V +KEY_W +KEY_X +KEY_Y +KEY_Z + +KEY_F1 +KEY_F2 +KEY_F3 +KEY_F4 +KEY_F5 +KEY_F6 +KEY_F7 +KEY_F8 +KEY_F9 +KEY_F10 +KEY_F11 +KEY_F12 +KEY_F13 +KEY_F14 +KEY_F15 +KEY_F16 +KEY_F17 +KEY_F18 +KEY_F19 +KEY_F20 +KEY_F21 +KEY_F22 +KEY_F23 +KEY_F24 + + +KEY_KP0 +KEY_KP1 +KEY_KP2 +KEY_KP3 +KEY_KP4 +KEY_KP5 +KEY_KP6 +KEY_KP7 +KEY_KP8 +KEY_KP9 +KEY_KPASTERISK +KEY_KPCOMMA +KEY_KPDOT +KEY_KPENTER +KEY_KPEQUAL +KEY_KPJPCOMMA +KEY_KPLEFTPAREN +KEY_KPMINUS +KEY_KPPLUS +KEY_KPPLUSMINUS +KEY_KPRIGHTPAREN +KEY_KPSLASH + +KEY_AB +KEY_ADDRESSBOOK +KEY_AGAIN +KEY_ALTERASE +KEY_ANGLE +KEY_APOSTROPHE +KEY_ARCHIVE +KEY_AUDIO +KEY_AUX +KEY_BACK +KEY_BACKSLASH +KEY_BACKSPACE +KEY_BASSBOOST +KEY_BATTERY +KEY_BLUE +KEY_BLUETOOTH +KEY_BOOKMARKS +KEY_BREAK +KEY_BRIGHTNESS_CYCLE +KEY_BRIGHTNESSDOWN +KEY_BRIGHTNESSUP +KEY_BRIGHTNESS_ZERO +KEY_BRL_DOT1 +KEY_BRL_DOT10 +KEY_BRL_DOT2 +KEY_BRL_DOT3 +KEY_BRL_DOT4 +KEY_BRL_DOT5 +KEY_BRL_DOT6 +KEY_BRL_DOT7 +KEY_BRL_DOT8 +KEY_BRL_DOT9 +KEY_CALC +KEY_CALENDAR +KEY_CANCEL +KEY_CAPSLOCK +KEY_CD +KEY_CHANNEL +KEY_CHANNELDOWN +KEY_CHANNELUP +KEY_CHAT +KEY_CLEAR +KEY_CLOSE +KEY_CLOSECD +KEY_COFFEE +KEY_COMMA +KEY_COMPOSE +KEY_COMPUTER +KEY_CONFIG +KEY_CONNECT +KEY_CONTEXT_MENU +KEY_COPY +KEY_CUT +KEY_CYCLEWINDOWS +KEY_DASHBOARD +KEY_DATABASE +KEY_DEL_EOL +KEY_DEL_EOS +KEY_DELETE +KEY_DELETEFILE +KEY_DEL_LINE +KEY_DIGITS +KEY_DIRECTION +KEY_DIRECTORY +KEY_DISPLAY_OFF +KEY_DISPLAYTOGGLE +KEY_DOCUMENTS +KEY_DOLLAR +KEY_DOT +KEY_DOWN +KEY_DVD +KEY_EDIT +KEY_EDITOR +KEY_EJECTCD +KEY_EJECTCLOSECD +KEY_EMAIL +KEY_END +KEY_ENTER +KEY_EPG +KEY_EQUAL +KEY_ESC +KEY_EURO +KEY_EXIT +KEY_FASTFORWARD +KEY_FAVORITES +KEY_FILE +KEY_FINANCE +KEY_FIND +KEY_FIRST +KEY_FN +KEY_FN_1 +KEY_FN_2 +KEY_FN_B +KEY_FN_D +KEY_FN_E +KEY_FN_ESC +KEY_FN_F +KEY_FN_F1 +KEY_FN_F2 +KEY_FN_F3 +KEY_FN_F4 +KEY_FN_F5 +KEY_FN_F6 +KEY_FN_F7 +KEY_FN_F8 +KEY_FN_F9 +KEY_FN_F10 +KEY_FN_F11 +KEY_FN_F12 +KEY_FN_S +KEY_FORWARD +KEY_FORWARDMAIL +KEY_FRAMEBACK +KEY_FRAMEFORWARD +KEY_FRONT +KEY_GAMES +KEY_GOTO +KEY_GRAPHICSEDITOR +KEY_GRAVE +KEY_GREEN +KEY_HANJA +KEY_HELP +KEY_HENKAN +KEY_HIRAGANA +KEY_HOME +KEY_HOMEPAGE +KEY_HP +KEY_INFO +KEY_INSERT +KEY_INS_LINE +KEY_ISO +KEY_KATAKANA +KEY_KATAKANAHIRAGANA +KEY_KBDILLUMDOWN +KEY_KBDILLUMTOGGLE +KEY_KBDILLUMUP +KEY_LANGUAGE +KEY_LAST +KEY_LEFT +KEY_LEFTALT +KEY_LEFTBRACE +KEY_LEFTCTRL +KEY_LEFTMETA +KEY_LEFTSHIFT +KEY_LINEFEED +KEY_LIST +KEY_LOGOFF +KEY_MACRO +KEY_MAIL +KEY_MAX +KEY_MEDIA +KEY_MEDIA_REPEAT +KEY_MEMO +KEY_MENU +KEY_MESSENGER +KEY_MHP +KEY_MINUS +KEY_MODE +KEY_MOVE +KEY_MP3 +KEY_MSDOS +KEY_MUHENKAN +KEY_MUTE +KEY_NEW +KEY_NEWS +KEY_NEXT +KEY_NEXTSONG +KEY_NUMERIC_0 +KEY_NUMERIC_1 +KEY_NUMERIC_2 +KEY_NUMERIC_3 +KEY_NUMERIC_4 +KEY_NUMERIC_5 +KEY_NUMERIC_6 +KEY_NUMERIC_7 +KEY_NUMERIC_8 +KEY_NUMERIC_9 +KEY_NUMERIC_POUND +KEY_NUMERIC_STAR +KEY_NUMLOCK +KEY_OK +KEY_OPEN +KEY_OPTION +KEY_PAGEDOWN +KEY_PAGEUP +KEY_PASTE +KEY_PAUSE +KEY_PAUSECD +KEY_PC +KEY_PHONE +KEY_PLAY +KEY_PLAYCD +KEY_PLAYER +KEY_PLAYPAUSE +KEY_POWER +KEY_POWER2 +KEY_PRESENTATION +KEY_PREVIOUS +KEY_PREVIOUSSONG +KEY_PRINT +KEY_PROG1 +KEY_PROG2 +KEY_PROG3 +KEY_PROG4 +KEY_PROGRAM +KEY_PROPS +KEY_PVR +KEY_QUESTION +KEY_RADIO +KEY_RECORD +KEY_RED +KEY_REDO +KEY_REFRESH +KEY_REPLY +KEY_RESERVED +KEY_RESTART +KEY_REWIND +KEY_RIGHT +KEY_RIGHTALT +KEY_RIGHTBRACE +KEY_RIGHTCTRL +KEY_RIGHTMETA +KEY_RIGHTSHIFT +KEY_RO +KEY_SAT +KEY_SAT2 +KEY_SAVE +KEY_SCALE +KEY_SCREEN +KEY_SCROLLDOWN +KEY_SCROLLLOCK +KEY_SCROLLUP +KEY_SEARCH +KEY_SELECT +KEY_SEMICOLON +KEY_SEND +KEY_SENDFILE +KEY_SETUP +KEY_SHOP +KEY_SHUFFLE +KEY_SLASH +KEY_SLEEP +KEY_SLOW +KEY_SOUND +KEY_SPACE +KEY_SPELLCHECK +KEY_SPORT +KEY_SPREADSHEET +KEY_STOP +KEY_STOPCD +KEY_SUBTITLE +KEY_SUSPEND +KEY_SWITCHVIDEOMODE +KEY_SYSRQ +KEY_TAB +KEY_TAPE +KEY_TEEN +KEY_TEXT +KEY_TIME +KEY_TITLE +KEY_TUNER +KEY_TV +KEY_TV2 +KEY_TWEN +KEY_UNDO +KEY_UNKNOWN +KEY_UP +KEY_UWB +KEY_VCR +KEY_VCR2 +KEY_VENDOR +KEY_VIDEO +KEY_VIDEO_NEXT +KEY_VIDEOPHONE +KEY_VIDEO_PREV +KEY_VOICEMAIL +KEY_VOLUMEDOWN +KEY_VOLUMEUP +KEY_WAKEUP +KEY_WIMAX +KEY_WLAN +KEY_WORDPROCESSOR +KEY_WWW +KEY_XFER +KEY_YELLOW +KEY_YEN +KEY_ZENKAKUHANKAKU +KEY_ZOOM +KEY_ZOOMIN +KEY_ZOOMOUT +KEY_ZOOMRESET diff --git a/data/ukeys/keysym-to-uinput b/data/ukeys/keysym-to-uinput new file mode 100644 index 0000000..aeaed48 --- /dev/null +++ b/data/ukeys/keysym-to-uinput @@ -0,0 +1,289 @@ +# This file maps X Keysyms to UInput codes. Note, not every single +# key is mapped, as this is only currently used for macro recording, +# which doesn't emit single syms for keys that are normally shifted + +[DEFAULT] +Control_a=KEY_LEFTCTRL,KEY_A +Control_b=KEY_LEFTCTRL,KEY_B +Control_c=KEY_LEFTCTRL,KEY_C +Control_d=KEY_LEFTCTRL,KEY_D +Control_e=KEY_LEFTCTRL,KEY_E +Control_f=KEY_LEFTCTRL,KEY_F +Control_g=KEY_LEFTCTRL,KEY_G +BackSpace=KEY_BACKSPACE +Control_h=KEY_BACKSPACE +Tab=KEY_TAB +Control_i=KEY_TAB +Linefeed=KEY_LINEFEED +Select=KEY_SELECT +End=KEY_END +Prior=KEY_PAGEUP +PageUp=KEY_PAGEUP +Next=KEY_PAGEDOWN +PageDown=KEY_PAGEDOWN +Control_j=KEY_LINEFEED +Control_k=KEY_LEFTCTRL,KEY_K +Control_l=KEY_LEFTCTRL,KEY_L +Control_m=KEY_LEFTCTRL,KEY_M +Control_n=KEY_LEFTCTRL,KEY_N +Control_o=KEY_LEFTCTRL,KEY_O +Control_p=KEY_LEFTCTRL,KEY_P +Control_q=KEY_LEFTCTRL,KEY_Q +Control_r=KEY_LEFTCTRL,KEY_R +Control_s=KEY_LEFTCTRL,KEY_S +Control_t=KEY_LEFTCTRL,KEY_T +Control_u=KEY_LEFTCTRL,KEY_U +Control_v=KEY_LEFTCTRL,KEY_V +Control_w=KEY_LEFTCTRL,KEY_W +Control_x=KEY_LEFTCTRL,KEY_X +Control_y=KEY_LEFTCTRL,KEY_Y +Control_z=KEY_LEFTCTRL,KEY_Z +Escape=KEY_ESC +Control_backslash=KEY_LEFTCTRL,KEY_BACKSLASH +Control_bracketright=KEY_LEFTCTRL,KEY_RIGHTBRACE +Control_asciicircum=KEY_LEFTCTRL,KEY_LEFTSHIFT,KEY_6 +Control_underscore=KEY_LEFTCTRL,KEY_LEFTSHIFT,KEY_MINUS +space=KEY_SPACE +dollar=KEY_DOLLAR +apostrophe=KEY_APOSTROPHE +comma=KEY_COMMA +minus=KEY_KP_MINUS +period=KEY_DOT +slash=KEY_SLASH +zero=KEY_0 +one=KEY_1 +two=KEY_2 +three=KEY_3 +four=KEY_4 +five=KEY_5 +six=KEY_6 +seven=KEY_7 +eight=KEY_8 +nine=KEY_9 +semicolon=KEY_SEMICOLON +equal=KEY_EQUAL +backslash=KEY_BACKSLASH +grave=KEY_GRAVE +a=KEY_A +b=KEY_B +c=KEY_C +d=KEY_D +e=KEY_E +f=KEY_F +g=KEY_G +h=KEY_H +i=KEY_I +j=KEY_J +k=KEY_K +l=KEY_L +m=KEY_M +n=KEY_N +o=KEY_O +p=KEY_P +q=KEY_Q +r=KEY_R +s=KEY_S +t=KEY_T +u=KEY_U +v=KEY_V +w=KEY_W +x=KEY_X +y=KEY_Y +z=KEY_Z +braceleft=KEY_LEFTBRACE +braceright=KEY_RIGHTBRACE +Delete=KEY_DELETE +F1=KEY_F1 +F2=KEY_F2 +F3=KEY_F3 +F4=KEY_F4 +F5=KEY_F5 +F6=KEY_F6 +F7=KEY_F7 +F8=KEY_F8 +F9=KEY_F9 +F10=KEY_F10 +F11=KEY_F11 +F12=KEY_F12 +F13=KEY_F13 +F14=KEY_F14 +F15=KEY_F15 +F16=KEY_F16 +F17=KEY_F17 +F18=KEY_F18 +F19=KEY_F19 +F20=KEY_F20 +F21=KEY_F21 +F22=KEY_F22 +F23=KEY_F23 +F24=KEY_F24 +Find=KEY_FIND +Home=KEY_HOME +Insert=KEY_INSERT +Next=KEY_NEXT +Help=KEY_HELP +Pause=KEY_PAUSE +Return=KEY_ENTER +Break=KEY_BREAK +Caps_Lock=KEY_CAPSLOCK +Num_Lock=KEY_NUMLOCK +Scroll_Lock=KEY_SCROLLLOCK +Scroll_Forward=KEY_SCROLLUP +Scroll_Backward=KEY_SCROLLDOWN +Compose=KEY_COMPOSE +KP_0=KEY_KP0 +KP_1=KEY_KP1 +KP_2=KEY_KP2 +KP_3=KEY_KP3 +KP_4=KEY_KP4 +KP_5=KEY_KP5 +KP_6=KEY_KP6 +KP_7=KEY_KP7 +KP_8=KEY_KP8 +KP_9=KEY_KP9 +KP_Add=KEY_KPPLUS +KP_Subtract=KEY_KPMINUS +KP_Multiply=KEY_ASTERISK +KP_Enter=KEY_KPENTER +KP_Period=KEY_DOT +KP_Comma=KEY_KPCOMMA +KP_Divide=KEY_KPSLASH +KP_MinPlus=KEY_KPPLUSMINUS +Down=KEY_DOWN +Left=KEY_LEFT +Right=KEY_RIGHT +Up=KEY_UP +Shift=KEY_LEFTSHIFT +AltGr=KEY_RIGHTALT +AltGr_L=KEY_LEFTALT +AltGr_R=KEY_RIGHTALT +AltL=KEY_LEFTALT +AltR=KEY_RIGHTALT +Alt_L=KEY_LEFTALT +Alt_R=KEY_RIGHTALT +Control=KEY_LEFTCTRL +Alt=KEY_LEFTALT +AltL=KEY_LEFTALT +Shift_L=KEY_LEFTSHIFT +Shift_R=KEY_RIGHTSHIFT +Control_L=KEY_LEFTCTRL +Control_R=KEY_RIGHTCTRL +ShiftL=KEY_LEFTSHIFT +ShiftR=KEY_RIGHTSHIFT +CtrlL=KEY_LEFTCTRL +CtrlR=KEY_LEFTCTRL +CapsShift=KEY_LEFTSHIFT +Meta_Control_a=KEY_LEFTMETA,KEY_LEFTCTRL,KEY_A +Meta_Control_b=KEY_LEFTMETA,KEY_LEFTCTRL,KEY_B +Meta_Control_c=KEY_LEFTMETA,KEY_LEFTCTRL,KEY_C +Meta_Control_d=KEY_LEFTMETA,KEY_LEFTCTRL,KEY_D +Meta_Control_e=KEY_LEFTMETA,KEY_LEFTCTRL,KEY_E +Meta_Control_f=KEY_LEFTMETA,KEY_LEFTCTRL,KEY_F +Meta_Control_g=KEY_LEFTMETA,KEY_LEFTCTRL,KEY_G +Meta_BackSpace=KEY_LEFTMETA,KEY_BACKSPACE +Meta_Tab=KEY_LEFTMETA,KEY_TAB +Meta_Linefeed=KEY_LEFTMETA,KEY_LINFEED +Meta_Control_k=KEY_LEFTMETA,KEY_LEFTCTRL,KEY_K +Meta_Control_l=KEY_LEFTMETA,KEY_LEFTCTRL,KEY_L +Meta_Control_m=KEY_LEFTMETA,KEY_LEFTCTRL,KEY_M +Meta_Control_n=KEY_LEFTMETA,KEY_LEFTCTRL,KEY_N +Meta_Control_o=KEY_LEFTMETA,KEY_LEFTCTRL,KEY_O +Meta_Control_p=KEY_LEFTMETA,KEY_LEFTCTRL,KEY_P +Meta_Control_q=KEY_LEFTMETA,KEY_LEFTCTRL,KEY_Q +Meta_Control_r=KEY_LEFTMETA,KEY_LEFTCTRL,KEY_R +Meta_Control_s=KEY_LEFTMETA,KEY_LEFTCTRL,KEY_S +Meta_Control_t=KEY_LEFTMETA,KEY_LEFTCTRL,KEY_T +Meta_Control_u=KEY_LEFTMETA,KEY_LEFTCTRL,KEY_U +Meta_Control_v=KEY_LEFTMETA,KEY_LEFTCTRL,KEY_V +Meta_Control_w=KEY_LEFTMETA,KEY_LEFTCTRL,KEY_W +Meta_Control_x=KEY_LEFTMETA,KEY_LEFTCTRL,KEY_X +Meta_Control_y=KEY_LEFTMETA,KEY_LEFTCTRL,KEY_Y +Meta_Control_z=KEY_LEFTMETA,KEY_LEFTCTRL,KEY_Z +Meta_Escape=KEY_LEFTMETA,KEY_ESC +Meta_Escape=KEY_LEFTMETA,KEY_LEFTCTRL,KEY_BACKSLASH +Meta_space=KEY_LEFTMETA,KEY_SPACE +Meta_apostrophe=KEY_LEFTMETA,KEY_APOSTROPHE +Meta_comma=KEY_LEFTMETA,KEY_COMMA +Meta_minus=KEY_LEFTMETA,KEY_COMMA +Meta_period=KEY_LEFTMETA,KEY_DOT +Meta_slash=KEY_LEFTMETA,KEY_SLASH +Meta_zero=KEY_LEFTMETA,KEY_0 +Meta_one=KEY_LEFTMETA,KEY_1 +Meta_two=KEY_LEFTMETA,KEY_2 +Meta_three=KEY_LEFTMETA,KEY_3 +Meta_four=KEY_LEFTMETA,KEY_4 +Meta_five=KEY_LEFTMETA,KEY_5 +Meta_six=KEY_LEFTMETA,KEY_6 +Meta_seven=KEY_LEFTMETA,KEY_7 +Meta_eight=KEY_LEFTMETA,KEY_8 +Meta_nine=KEY_LEFTMETA,KEY_9 +Meta_semicolon=KEY_LEFTMETA,KEY_SEMICOLON +Meta_equal=KEY_LEFTMETA,KEY_EQUAL +Meta_question=KEY_LEFTMETA,KEY_QUESTION +Meta_braceleft=KEY_LEFTMETA,KEY_LEFTBRACE +Meta_braceright=KEY_LEFTMETA,KEY_RIGHTBRACE +Meta_A=KEY_LEFTMETA,KEY_LEFTSHIFT,KEY_A +Meta_B=KEY_LEFTMETA,KEY_LEFTSHIFT,KEY_B +Meta_C=KEY_LEFTMETA,KEY_LEFTSHIFT,KEY_C +Meta_D=KEY_LEFTMETA,KEY_LEFTSHIFT,KEY_D +Meta_E=KEY_LEFTMETA,KEY_LEFTSHIFT,KEY_E +Meta_F=KEY_LEFTMETA,KEY_LEFTSHIFT,KEY_F +Meta_G=KEY_LEFTMETA,KEY_LEFTSHIFT,KEY_G +Meta_H=KEY_LEFTMETA,KEY_LEFTSHIFT,KEY_H +Meta_I=KEY_LEFTMETA,KEY_LEFTSHIFT,KEY_I +Meta_J=KEY_LEFTMETA,KEY_LEFTSHIFT,KEY_J +Meta_K=KEY_LEFTMETA,KEY_LEFTSHIFT,KEY_K +Meta_L=KEY_LEFTMETA,KEY_LEFTSHIFT,KEY_L +Meta_M=KEY_LEFTMETA,KEY_LEFTSHIFT,KEY_M +Meta_N=KEY_LEFTMETA,KEY_LEFTSHIFT,KEY_M +Meta_O=KEY_LEFTMETA,KEY_LEFTSHIFT,KEY_O +Meta_P=KEY_LEFTMETA,KEY_LEFTSHIFT,KEY_P +Meta_Q=KEY_LEFTMETA,KEY_LEFTSHIFT,KEY_Q +Meta_R=KEY_LEFTMETA,KEY_LEFTSHIFT,KEY_R +Meta_S=KEY_LEFTMETA,KEY_LEFTSHIFT,KEY_S +Meta_T=KEY_LEFTMETA,KEY_LEFTSHIFT,KEY_T +Meta_U=KEY_LEFTMETA,KEY_LEFTSHIFT,KEY_U +Meta_V=KEY_LEFTMETA,KEY_LEFTSHIFT,KEY_V +Meta_W=KEY_LEFTMETA,KEY_LEFTSHIFT,KEY_W +Meta_X=KEY_LEFTMETA,KEY_LEFTSHIFT,KEY_X +Meta_Y=KEY_LEFTMETA,KEY_LEFTSHIFT,KEY_Y +Meta_Z=KEY_LEFTMETA,KEY_LEFTSHIFT,KEY_Z +Meta_backslash=KEY_LEFTMETA,KEY_BACKSLASH +Meta_grave=KEY_LEFTMETA,KEY_GRAVE +Meta_a=KEY_LEFTMETA,KEY_A +Meta_b=KEY_LEFTMETA,KEY_B +Meta_c=KEY_LEFTMETA,KEY_C +Meta_d=KEY_LEFTMETA,KEY_D +Meta_e=KEY_LEFTMETA,KEY_E +Meta_f=KEY_LEFTMETA,KEY_F +Meta_g=KEY_LEFTMETA,KEY_G +Meta_h=KEY_LEFTMETA,KEY_H +Meta_i=KEY_LEFTMETA,KEY_I +Meta_j=KEY_LEFTMETA,KEY_J +Meta_k=KEY_LEFTMETA,KEY_K +Meta_l=KEY_LEFTMETA,KEY_L +Meta_m=KEY_LEFTMETA,KEY_M +Meta_n=KEY_LEFTMETA,KEY_M +Meta_o=KEY_LEFTMETA,KEY_O +Meta_p=KEY_LEFTMETA,KEY_P +Meta_q=KEY_LEFTMETA,KEY_Q +Meta_r=KEY_LEFTMETA,KEY_R +Meta_s=KEY_LEFTMETA,KEY_S +Meta_t=KEY_LEFTMETA,KEY_T +Meta_u=KEY_LEFTMETA,KEY_U +Meta_v=KEY_LEFTMETA,KEY_V +Meta_w=KEY_LEFTMETA,KEY_W +Meta_x=KEY_LEFTMETA,KEY_X +Meta_y=KEY_LEFTMETA,KEY_Y +Meta_z=KEY_LEFTMETA,KEY_Z +Meta_Delete=KEY_LEFTMETA,KEY_DELETE +Brl_dot1=KEY_BRL_DOT1 +Brl_dot2=KEY_BRL_DOT2 +Brl_dot3=KEY_BRL_DOT3 +Brl_dot4=KEY_BRL_DOT4 +Brl_dot5=KEY_BRL_DOT5 +Brl_dot6=KEY_BRL_DOT6 +Brl_dot7=KEY_BRL_DOT7 +Brl_dot8=KEY_BRL_DOT8 +Brl_dot9=KEY_BRL_DOT9 +Brl_dot10=KEY_BRL_DOT10 \ No newline at end of file diff --git a/data/ukeys/mouse.keys b/data/ukeys/mouse.keys new file mode 100644 index 0000000..6b83d33 --- /dev/null +++ b/data/ukeys/mouse.keys @@ -0,0 +1,18 @@ +BTN_0 +BTN_1 +BTN_2 +BTN_3 +BTN_4 +BTN_5 +BTN_6 +BTN_7 +BTN_8 +BTN_9 +BTN_LEFT +BTN_RIGHT +BTN_MIDDLE +BTN_SIDE +BTN_EXTRA +BTN_FORWARD +BTN_BACK +BTN_TASK \ No newline at end of file diff --git a/data/xcf/AwOkenIcon.xcf b/data/xcf/AwOkenIcon.xcf new file mode 100644 index 0000000..4f9e5fb Binary files /dev/null and b/data/xcf/AwOkenIcon.xcf differ diff --git a/debian/changelog b/debian/changelog new file mode 100644 index 0000000..bbf6ff7 --- /dev/null +++ b/debian/changelog @@ -0,0 +1,5 @@ +gnome15 (0~git-1) unstable; urgency=medium + + * Initial debian package (git commit abfdec9016768e1c31beda29b25568eed6f7dfe3) + + -- Dmitry Yu Okunev Sun, 15 Jan 2017 20:25:19 +0300 diff --git a/debian/compat b/debian/compat new file mode 100644 index 0000000..ec63514 --- /dev/null +++ b/debian/compat @@ -0,0 +1 @@ +9 diff --git a/debian/control b/debian/control new file mode 100644 index 0000000..0e1674c --- /dev/null +++ b/debian/control @@ -0,0 +1,22 @@ +Source: gnome15 +Section: extra +Priority: optional +Maintainer: Dmitry Yu Okunev +Build-Depends: debhelper (>=9), python-dev, python-gtk2-dev, python-keyring, + python-virtkey, python-pyinotify, python-usb, python-gconf, python-rsvg, + python-uinput, python-xlib +Standards-Version: 3.9.8 +Homepage: https://gnome15.org/ +#Vcs-Git: git://anonscm.debian.org/collab-maint/gnome15.git +#Vcs-Browser: https://anonscm.debian.org/cgit/collab-maint/gnome15.git + +Package: gnome15 +Architecture: any +Depends: ${shlibs:Depends}, ${misc:Depends}, python-wnck +Description: Tools and libs for Logithech G series keyboards + Gnome15 provides a panel indicator (or applet), configuration tool, + macro system and plugin framework for the Logitech G series keyboards + and headsets, including the G15, G19, G13, G930, G35, G510, G11, G110 + and the Z-10 speakers. The intention is to provide the best + integration with the Linux desktop possible, using the standard + protocols and libraries where appropriate. diff --git a/debian/copyright b/debian/copyright new file mode 100644 index 0000000..2b37e16 --- /dev/null +++ b/debian/copyright @@ -0,0 +1,56 @@ +Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ +Upstream-Name: gnome15 +Source: https://github.com/Huskynarr/gnome15 + +Files: * +Copyright: 2010-2016 Brett Smith + 2010-2016 Nuno Araujo + 2010-2016 NoXPhasma + 2010-2016 Huskynarr +License: GPL-3+ + 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 package; if not, write to the Free + Software Foundation, Inc., 51 Franklin St, Fifth Floor, + Boston, MA 02110-1301 USA + . + On Debian systems, the full text of the GNU General Public + License version 3 can be found in the file + `/usr/share/common-licenses/GPL-3'. + +# If you want to use GPL v2 or later for the /debian/* files use +# the following clauses, or change it to suit. Delete these two lines +Files: debian/* +Copyright: 2017 Dmitry Yu Okunev +License: GPL-2+ + This package is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + . + This package 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 + . + On Debian systems, the complete text of the GNU General + Public License version 2 can be found in "/usr/share/common-licenses/GPL-2". + +# Please also look if there are files or directories which have a +# different copyright/license attached and list them here. +# Please avoid picking licenses with terms that are more restrictive than the +# packaged work, as it may make Debian's contributions unacceptable upstream. diff --git a/debian/rules b/debian/rules new file mode 100755 index 0000000..31f06c9 --- /dev/null +++ b/debian/rules @@ -0,0 +1,25 @@ +#!/usr/bin/make -f +# See debhelper(7) (uncomment to enable) +# output every command that modifies files on the build system. +#export DH_VERBOSE = 1 + + +# see FEATURE AREAS in dpkg-buildflags(1) +#export DEB_BUILD_MAINT_OPTIONS = hardening=+all + +# see ENVIRONMENT in dpkg-buildflags(1) +# package maintainers to append CFLAGS +#export DEB_CFLAGS_MAINT_APPEND = -Wall -pedantic +# package maintainers to append LDFLAGS +#export DEB_LDFLAGS_MAINT_APPEND = -Wl,--as-needed + + +%: + dh $@ --with autoreconf + + +# dh_make generated override targets +# This is example for Cmake (See https://bugs.debian.org/641051 ) +#override_dh_auto_configure: +# dh_auto_configure -- # -DCMAKE_LIBRARY_PATH=$(DEB_HOST_MULTIARCH) + diff --git a/debian/source/format b/debian/source/format new file mode 100644 index 0000000..163aaf8 --- /dev/null +++ b/debian/source/format @@ -0,0 +1 @@ +3.0 (quilt) diff --git a/debug-g15-config.sh b/debug-g15-config.sh new file mode 100755 index 0000000..09fb01c --- /dev/null +++ b/debug-g15-config.sh @@ -0,0 +1,5 @@ +#!/bin/bash +DIR=$(dirname $0) +cd "${DIR}" +#export G15_PLUGINS=./src/plugins +src/scripts/g15-config diff --git a/debug-g15-systemtray.sh b/debug-g15-systemtray.sh new file mode 100755 index 0000000..e25d21a --- /dev/null +++ b/debug-g15-systemtray.sh @@ -0,0 +1,5 @@ +#!/bin/bash +DIR=$(dirname $0) +cd "${DIR}" +#export G15_PLUGINS=./src/plugins +src/scripts/g15-systemtray diff --git a/debug.sh b/debug.sh new file mode 100755 index 0000000..e622179 --- /dev/null +++ b/debug.sh @@ -0,0 +1,10 @@ +#!/bin/bash +DIR=$(dirname $0) +cd "${DIR}" +#export G15_PLUGINS=./src/plugins +if [ $# -eq 0 ]; then + args=restart +else + args=$@ +fi +src/scripts/g15-desktop-service -f -l INFO $args diff --git a/docs/Gnome15.dia b/docs/Gnome15.dia new file mode 100644 index 0000000..0e75569 Binary files /dev/null and b/docs/Gnome15.dia differ diff --git a/docs/style_guide.md b/docs/style_guide.md new file mode 100644 index 0000000..f377b86 --- /dev/null +++ b/docs/style_guide.md @@ -0,0 +1,84 @@ +# Style Guide + +The style guide can be summed up as 'clang-format with the Google style set'. +In addition, the [Google Style Guide](https://google.github.io/styleguide/cppguide.html) +is followed and cpplint is the source of truth. When in doubt, defer to what +code in the project already does. + +Base rules: + +* 80 column line length max +* LF (Unix-style) line endings +* 2-space soft tabs, no TABs! +* [Google Style Guide](https://google.github.io/styleguide/cppguide.html) for naming/casing/etc +* Sort includes according to the [style guide rules](https://google.github.io/styleguide/cppguide.html#Names_and_Order_of_Includes) +* Comments are properly punctuated (that means capitalization and periods, etc) +* TODO's must be attributed like `// TODO(yourgithubname): foo.` + +Code that really breaks from the formatting rules will not be accepted, as then +no one else can use clang-format on the code without also touching all your +lines. + +### Why? + +To quote the [Google Style Guide](https://google.github.io/styleguide/cppguide.html): + +``` +One way in which we keep the code base manageable is by enforcing consistency. +It is very important that any programmer be able to look at another's code and +quickly understand it. Maintaining a uniform style and following conventions +means that we can more easily use "pattern-matching" to infer what various +symbols are and what invariants are true about them. Creating common, required +idioms and patterns makes code much easier to understand. In some cases there +might be good arguments for changing certain style rules, but we nonetheless +keep things as they are in order to preserve consistency. +``` + +## Buildbot Verification + +The buildbot runs `xb lint --all` on the master branch, and will run +`xb lint --origin` on pull requests. Run `xb format` before you commit each +local change so that you are consistently clean, otherwise you may have to +rebase. If you forget, run `xb format --origin` and rebase your changes (so you +don't end up with 5 changes and then a 6th 'whoops' one - that's nasty). + +The buildbot is running LLVM 3.8.0. If you are noticing style differences +between your local lint/format and the buildbot, ensure you are running that +version. + +## Tools + +### clang-format + +clang-format with the Google style is used to format all files. I recommend +installing/wiring it up to your editor of choice so that you don't even have to +think about tabs and wrapping and such. + +#### Command Line + +To use the `xb format` auto-formatter, you need to have a `clang-format` on your +PATH. If you're on Windows you can do this by installing an LLVM binary package +from [the LLVM downloads page](http://llvm.org/releases/download.html). If you +install it to the default location the `xb format` command will find it +automatically even if you don't choose to put all of LLVM onto your PATH. + +#### Visual Studio + +Grab the official [experimental Visual Studio plugin](http://llvm.org/builds/). +To switch to the Google style go Tools -> Options -> LLVM/Clang -> ClangFormat +and set Style to Google. Then use ctrl-r/ctrl-f to trigger the formatting. +Unfortunately it only does the cursor by default, so you'll have to select the +whole doc and invoke it to get it all done. + +If you have a better option, let me know! + +#### Xcode + +Install [Alcatraz](http://alcatraz.io/) to get the [ClangFormat](https://github.com/travisjeffery/ClangFormat-Xcode) +package. Set it to use the Google style and format on save. Never think about +tabs or linefeeds or whatever again. + +### cpplint + +TODO: write a cool script to do this/editor plugins. +In the future, the linter will run as a git commit hook and on travis. diff --git a/example.svg b/example.svg new file mode 100644 index 0000000..7cbb55a --- /dev/null +++ b/example.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/i18n/Makefile.am b/i18n/Makefile.am new file mode 100644 index 0000000..da1475b --- /dev/null +++ b/i18n/Makefile.am @@ -0,0 +1,30 @@ +EXTRA_DIST = colorpicker.en_GB.po \ + gnome15.en_GB.po \ + gnome15-drivers.en_GB.po \ + driver_g15.en_GB.po \ + driver_g15direct.en_GB.po \ + driver_g19.en_GB.po \ + driver_g930.en_GB.po \ + driver_g19direct.en_GB.po \ + driver_gtk.en_GB.po \ + driver_kernel.en_GB.po \ + g15-config.en_GB.po \ + macro-editor.en_GB.po + +all-local: + for M_LOCALE in @ENABLED_LOCALES@; do \ + mkdir -p $$M_LOCALE/LC_MESSAGES ; \ + for M_PO in "$(abs_srcdir)"/*.po; do \ + BN=`basename $$M_PO .po`; \ + LL=`basename $$BN .$$M_LOCALE`.mo; \ + echo "$$M_PO -> $$LL"; \ + msgfmt $$M_PO --output-file $$M_LOCALE/LC_MESSAGES/$$LL; \ + done; \ + done + +install-exec-hook: + for M_LOCALE in @ENABLED_LOCALES@; do \ + mkdir -p $(DESTDIR)$(datadir)/gnome15/i18n; \ + cp -pR $$M_LOCALE $(DESTDIR)$(datadir)/gnome15/i18n; \ + done + \ No newline at end of file diff --git a/i18n/build-po.sh b/i18n/build-po.sh new file mode 100755 index 0000000..1f45291 --- /dev/null +++ b/i18n/build-po.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +echo -e "Locale: \c" +read locale +if [ -n "${locale}" ]; then + for i in *.pot; do + bn=$(basename $i .pot).${locale}.po + msginit --input=${i} --output=${bn} --locale=${locale} + done +fi \ No newline at end of file diff --git a/i18n/build-pot.sh b/i18n/build-pot.sh new file mode 100755 index 0000000..a665e88 --- /dev/null +++ b/i18n/build-pot.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +cd $(dirname $0) + +# Python +xgettext --from-code=UTF-8 --language=Python --keyword=N_ --keyword=_ --output=gnome15-drivers.pot ../src/gnome15/util/*.py +xgettext --from-code=UTF-8 --language=Python --keyword=N_ --keyword=_ --output=gnome15-drivers.pot ../src/gnome15/drivers/*.py +xgettext --from-code=UTF-8 --language=Python --keyword=N_ --keyword=_ --output=gnome15.pot ../src/gnome15/*.py + +# .ui files +for i in ../data/ui/*.ui; do + intltool-extract --type=gettext/glade $i + mv ${i}.h . +done +for i in *.h; do + bn=$(basename ${i} .h) + bn=$(basename ${bn} .ui).pot + xgettext --from-code=UTF-8 --language=Python --keyword=N_ --keyword=_ --output=${bn} ${i} +done diff --git a/i18n/colorpicker.en_GB.po b/i18n/colorpicker.en_GB.po new file mode 100644 index 0000000..671a95f --- /dev/null +++ b/i18n/colorpicker.en_GB.po @@ -0,0 +1,30 @@ +# English translations for gnome15 package. +# Copyright (C) 2011 THE gnome15'S COPYRIGHT HOLDER +# This file is distributed under the same license as the gnome15 package. +# Brett Smith , 2011. +# +msgid "" +msgstr "" +"Project-Id-Version: gnome15 0.8.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2011-10-09 14:52+0100\n" +"PO-Revision-Date: 2011-10-09 14:54+0100\n" +"Last-Translator: Brett Smith \n" +"Language-Team: English (British)\n" +"Language: en_GB\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=ASCII\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: colorpicker.glade.h:1 +msgid "Blue:" +msgstr "Blue:" + +#: colorpicker.glade.h:2 +msgid "Pick Colour" +msgstr "Pick Colour" + +#: colorpicker.glade.h:3 +msgid "Red:" +msgstr "Red:" diff --git a/i18n/colorpicker.glade.h b/i18n/colorpicker.glade.h new file mode 100644 index 0000000..7274581 --- /dev/null +++ b/i18n/colorpicker.glade.h @@ -0,0 +1,3 @@ +char *s = N_("Blue:"); +char *s = N_("Pick Colour"); +char *s = N_("Red:"); diff --git a/i18n/colorpicker.pot b/i18n/colorpicker.pot new file mode 100644 index 0000000..71766bf --- /dev/null +++ b/i18n/colorpicker.pot @@ -0,0 +1,30 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2011-10-09 14:52+0100\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: colorpicker.glade.h:1 +msgid "Blue:" +msgstr "" + +#: colorpicker.glade.h:2 +msgid "Pick Colour" +msgstr "" + +#: colorpicker.glade.h:3 +msgid "Red:" +msgstr "" diff --git a/i18n/driver_g15.en_GB.po b/i18n/driver_g15.en_GB.po new file mode 100644 index 0000000..7d8cec3 --- /dev/null +++ b/i18n/driver_g15.en_GB.po @@ -0,0 +1,22 @@ +# English translations for gnome15 package. +# Copyright (C) 2011 THE gnome15'S COPYRIGHT HOLDER +# This file is distributed under the same license as the gnome15 package. +# Brett Smith , 2011. +# +msgid "" +msgstr "" +"Project-Id-Version: gnome15 0.8.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2011-10-09 14:52+0100\n" +"PO-Revision-Date: 2011-10-09 14:54+0100\n" +"Last-Translator: Brett Smith \n" +"Language-Team: English (British)\n" +"Language: en_GB\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=ASCII\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: driver_g15.glade.h:1 +msgid "Port" +msgstr "Port" diff --git a/i18n/driver_g15.glade.h b/i18n/driver_g15.glade.h new file mode 100644 index 0000000..1cac7b9 --- /dev/null +++ b/i18n/driver_g15.glade.h @@ -0,0 +1 @@ +char *s = N_("Port"); diff --git a/i18n/driver_g15.pot b/i18n/driver_g15.pot new file mode 100644 index 0000000..35a3ad1 --- /dev/null +++ b/i18n/driver_g15.pot @@ -0,0 +1,22 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2011-10-09 14:52+0100\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: driver_g15.glade.h:1 +msgid "Port" +msgstr "" diff --git a/i18n/driver_g15direct.en_GB.po b/i18n/driver_g15direct.en_GB.po new file mode 100644 index 0000000..ed48411 --- /dev/null +++ b/i18n/driver_g15direct.en_GB.po @@ -0,0 +1,42 @@ +# English translations for gnome15 package. +# Copyright (C) 2011 THE gnome15'S COPYRIGHT HOLDER +# This file is distributed under the same license as the gnome15 package. +# Brett Smith , 2011. +# +msgid "" +msgstr "" +"Project-Id-Version: gnome15 0.8.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2011-10-09 14:52+0100\n" +"PO-Revision-Date: 2011-10-09 14:54+0100\n" +"Last-Translator: Brett Smith \n" +"Language-Team: English (British)\n" +"Language: en_GB\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=ASCII\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: driver_g15direct.glade.h:1 +msgid "Analogue Joystick" +msgstr "Analogue Joystick" + +#: driver_g15direct.glade.h:2 +msgid "Emit macro keys" +msgstr "Emit macro keys" + +#: driver_g15direct.glade.h:3 +msgid "Joystick mode" +msgstr "Joystick mode" + +#: driver_g15direct.glade.h:4 +msgid "Mouse" +msgstr "Mouse" + +#: driver_g15direct.glade.h:5 +msgid "Timeout" +msgstr "Timeout" + +#: driver_g15direct.glade.h:6 +msgid "ms" +msgstr "ms" diff --git a/i18n/driver_g15direct.glade.h b/i18n/driver_g15direct.glade.h new file mode 100644 index 0000000..9778364 --- /dev/null +++ b/i18n/driver_g15direct.glade.h @@ -0,0 +1,6 @@ +char *s = N_("Analogue Joystick"); +char *s = N_("Emit macro keys"); +char *s = N_("Joystick mode"); +char *s = N_("Mouse"); +char *s = N_("Timeout"); +char *s = N_("ms"); diff --git a/i18n/driver_g15direct.pot b/i18n/driver_g15direct.pot new file mode 100644 index 0000000..1f9faa6 --- /dev/null +++ b/i18n/driver_g15direct.pot @@ -0,0 +1,42 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2011-10-09 14:52+0100\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: driver_g15direct.glade.h:1 +msgid "Analogue Joystick" +msgstr "" + +#: driver_g15direct.glade.h:2 +msgid "Emit macro keys" +msgstr "" + +#: driver_g15direct.glade.h:3 +msgid "Joystick mode" +msgstr "" + +#: driver_g15direct.glade.h:4 +msgid "Mouse" +msgstr "" + +#: driver_g15direct.glade.h:5 +msgid "Timeout" +msgstr "" + +#: driver_g15direct.glade.h:6 +msgid "ms" +msgstr "" diff --git a/i18n/driver_g19.en_GB.po b/i18n/driver_g19.en_GB.po new file mode 100644 index 0000000..8f7de2e --- /dev/null +++ b/i18n/driver_g19.en_GB.po @@ -0,0 +1,22 @@ +# English translations for gnome15 package. +# Copyright (C) 2011 THE gnome15'S COPYRIGHT HOLDER +# This file is distributed under the same license as the gnome15 package. +# Brett Smith , 2011. +# +msgid "" +msgstr "" +"Project-Id-Version: gnome15 0.8.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2011-10-09 14:52+0100\n" +"PO-Revision-Date: 2011-10-09 14:54+0100\n" +"Last-Translator: Brett Smith \n" +"Language-Team: English (British)\n" +"Language: en_GB\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=ASCII\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: driver_g19.glade.h:1 +msgid "Port" +msgstr "Port" diff --git a/i18n/driver_g19.glade.h b/i18n/driver_g19.glade.h new file mode 100644 index 0000000..1cac7b9 --- /dev/null +++ b/i18n/driver_g19.glade.h @@ -0,0 +1 @@ +char *s = N_("Port"); diff --git a/i18n/driver_g19.pot b/i18n/driver_g19.pot new file mode 100644 index 0000000..888c241 --- /dev/null +++ b/i18n/driver_g19.pot @@ -0,0 +1,22 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2011-10-09 14:52+0100\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: driver_g19.glade.h:1 +msgid "Port" +msgstr "" diff --git a/i18n/driver_g19direct.en_GB.po b/i18n/driver_g19direct.en_GB.po new file mode 100644 index 0000000..03d7ecf --- /dev/null +++ b/i18n/driver_g19direct.en_GB.po @@ -0,0 +1,34 @@ +# English translations for gnome15 package. +# Copyright (C) 2011 THE gnome15'S COPYRIGHT HOLDER +# This file is distributed under the same license as the gnome15 package. +# Brett Smith , 2011. +# +msgid "" +msgstr "" +"Project-Id-Version: gnome15 0.8.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2011-10-09 14:52+0100\n" +"PO-Revision-Date: 2011-10-09 14:54+0100\n" +"Last-Translator: Brett Smith \n" +"Language-Team: English (British)\n" +"Language: en_GB\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=ASCII\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: driver_g19direct.glade.h:1 +msgid "Reset device before use" +msgstr "Reset device before use" + +#: driver_g19direct.glade.h:2 +msgid "Timeout" +msgstr "Timeout" + +#: driver_g19direct.glade.h:3 +msgid "ms" +msgstr "ms" + +#: driver_g19direct.glade.h:4 +msgid "then wait" +msgstr "then wait" diff --git a/i18n/driver_g19direct.glade.h b/i18n/driver_g19direct.glade.h new file mode 100644 index 0000000..619ce16 --- /dev/null +++ b/i18n/driver_g19direct.glade.h @@ -0,0 +1,4 @@ +char *s = N_("Reset device before use"); +char *s = N_("Timeout"); +char *s = N_("ms"); +char *s = N_("then wait"); diff --git a/i18n/driver_g19direct.pot b/i18n/driver_g19direct.pot new file mode 100644 index 0000000..d4e87b0 --- /dev/null +++ b/i18n/driver_g19direct.pot @@ -0,0 +1,34 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2011-10-09 14:52+0100\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: driver_g19direct.glade.h:1 +msgid "Reset device before use" +msgstr "" + +#: driver_g19direct.glade.h:2 +msgid "Timeout" +msgstr "" + +#: driver_g19direct.glade.h:3 +msgid "ms" +msgstr "" + +#: driver_g19direct.glade.h:4 +msgid "then wait" +msgstr "" diff --git a/i18n/driver_g930.en_GB.po b/i18n/driver_g930.en_GB.po new file mode 100644 index 0000000..98ed86a --- /dev/null +++ b/i18n/driver_g930.en_GB.po @@ -0,0 +1,22 @@ +# English translations for gnome15 package. +# Copyright (C) 2011 THE gnome15'S COPYRIGHT HOLDER +# This file is distributed under the same license as the gnome15 package. +# Brett Smith , 2011. +# +msgid "" +msgstr "" +"Project-Id-Version: gnome15 0.8.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2011-10-09 14:52+0100\n" +"PO-Revision-Date: 2011-10-09 14:54+0100\n" +"Last-Translator: Brett Smith \n" +"Language-Team: English (British)\n" +"Language: en_GB\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=ASCII\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: driver_g930.glade.h:1 +msgid "Emit Macro Keys" +msgstr "Emit Macro Keys" diff --git a/i18n/driver_g930.glade.h b/i18n/driver_g930.glade.h new file mode 100644 index 0000000..9e095dd --- /dev/null +++ b/i18n/driver_g930.glade.h @@ -0,0 +1 @@ +char *s = N_("Emit Macro Keys"); diff --git a/i18n/driver_g930.pot b/i18n/driver_g930.pot new file mode 100644 index 0000000..11f884a --- /dev/null +++ b/i18n/driver_g930.pot @@ -0,0 +1,22 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2011-10-09 14:52+0100\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: driver_kernel.glade.h:1 +msgid "Emit Macro Keys" +msgstr "" diff --git a/i18n/driver_gtk.en_GB.po b/i18n/driver_gtk.en_GB.po new file mode 100644 index 0000000..1d30ebb --- /dev/null +++ b/i18n/driver_gtk.en_GB.po @@ -0,0 +1,22 @@ +# English translations for gnome15 package. +# Copyright (C) 2011 THE gnome15'S COPYRIGHT HOLDER +# This file is distributed under the same license as the gnome15 package. +# Brett Smith , 2011. +# +msgid "" +msgstr "" +"Project-Id-Version: gnome15 0.8.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2011-10-09 14:52+0100\n" +"PO-Revision-Date: 2011-10-09 14:54+0100\n" +"Last-Translator: Brett Smith \n" +"Language-Team: English (British)\n" +"Language: en_GB\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=ASCII\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: driver_gtk.glade.h:1 +msgid "Mode:" +msgstr "Mode:" diff --git a/i18n/driver_gtk.glade.h b/i18n/driver_gtk.glade.h new file mode 100644 index 0000000..6d8497a --- /dev/null +++ b/i18n/driver_gtk.glade.h @@ -0,0 +1 @@ +char *s = N_("Mode:"); diff --git a/i18n/driver_gtk.pot b/i18n/driver_gtk.pot new file mode 100644 index 0000000..fa0a94b --- /dev/null +++ b/i18n/driver_gtk.pot @@ -0,0 +1,22 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2011-10-09 14:52+0100\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: driver_gtk.glade.h:1 +msgid "Mode:" +msgstr "" diff --git a/i18n/driver_kernel.en_GB.po b/i18n/driver_kernel.en_GB.po new file mode 100644 index 0000000..436ea9c --- /dev/null +++ b/i18n/driver_kernel.en_GB.po @@ -0,0 +1,62 @@ +# English translations for gnome15 package. +# Copyright (C) 2011 THE gnome15'S COPYRIGHT HOLDER +# This file is distributed under the same license as the gnome15 package. +# Brett Smith , 2011. +# +msgid "" +msgstr "" +"Project-Id-Version: gnome15 0.8.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2011-10-09 14:52+0100\n" +"PO-Revision-Date: 2011-10-09 14:54+0100\n" +"Last-Translator: Brett Smith \n" +"Language-Team: English (British)\n" +"Language: en_GB\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=ASCII\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: driver_kernel.glade.h:1 +msgid "Analogue Joystick" +msgstr "Analogue Joystick" + +#: driver_kernel.glade.h:2 +msgid "Device:" +msgstr "Device:" + +#: driver_kernel.glade.h:3 +msgid "Digital Joystick" +msgstr "Digital Joystick" + +#: driver_kernel.glade.h:4 +msgid "Emit Macro Keys" +msgstr "Emit Macro Keys" + +#: driver_kernel.glade.h:5 +msgid "Joystick mode:" +msgstr "Joystick mode:" + +#: driver_kernel.glade.h:6 +msgid "Mouse" +msgstr "Mouse" + +#: driver_kernel.glade.h:7 +msgid "auto" +msgstr "auto" + +#: driver_kernel.glade.h:8 +msgid "g13" +msgstr "g13" + +#: driver_kernel.glade.h:9 +msgid "g15v1" +msgstr "g15v1" + +#: driver_kernel.glade.h:10 +msgid "g15v2" +msgstr "g15v2" + +#: driver_kernel.glade.h:11 +msgid "g19" +msgstr "g19" diff --git a/i18n/driver_kernel.glade.h b/i18n/driver_kernel.glade.h new file mode 100644 index 0000000..cd25e52 --- /dev/null +++ b/i18n/driver_kernel.glade.h @@ -0,0 +1,11 @@ +char *s = N_("Analogue Joystick"); +char *s = N_("Device:"); +char *s = N_("Digital Joystick"); +char *s = N_("Emit Macro Keys"); +char *s = N_("Joystick mode:"); +char *s = N_("Mouse"); +char *s = N_("auto"); +char *s = N_("g13"); +char *s = N_("g15v1"); +char *s = N_("g15v2"); +char *s = N_("g19"); diff --git a/i18n/driver_kernel.pot b/i18n/driver_kernel.pot new file mode 100644 index 0000000..0f62039 --- /dev/null +++ b/i18n/driver_kernel.pot @@ -0,0 +1,47 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2011-10-09 14:52+0100\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: driver_kernel.glade.h:1 +msgid "Emit Macro Keys" +msgstr "" + + +#: driver_kernel.glade.h:6 +msgid "Mouse" +msgstr "" + +#: driver_kernel.glade.h:7 +msgid "auto" +msgstr "" + +#: driver_kernel.glade.h:8 +msgid "g13" +msgstr "" + +#: driver_kernel.glade.h:9 +msgid "g15v1" +msgstr "" + +#: driver_kernel.glade.h:10 +msgid "g15v2" +msgstr "" + +#: driver_kernel.glade.h:11 +msgid "g19" +msgstr "" diff --git a/i18n/g15-config.en_GB.po b/i18n/g15-config.en_GB.po new file mode 100644 index 0000000..230a409 --- /dev/null +++ b/i18n/g15-config.en_GB.po @@ -0,0 +1,330 @@ +# English translations for gnome15 package. +# Copyright (C) 2011 THE gnome15'S COPYRIGHT HOLDER +# This file is distributed under the same license as the gnome15 package. +# Brett Smith , 2011. +# +msgid "" +msgstr "" +"Project-Id-Version: gnome15 0.8.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2011-10-09 14:52+0100\n" +"PO-Revision-Date: 2011-10-09 14:54+0100\n" +"Last-Translator: Brett Smith \n" +"Language-Team: English (British)\n" +"Language: en_GB\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: g15-config.glade.h:1 +msgid "Controls" +msgstr "Controls" + +#: g15-config.glade.h:2 +msgid "Global Plugins" +msgstr "Global Plugins" + +#: g15-config.glade.h:3 +msgid "Images" +msgstr "Images" + +#: g15-config.glade.h:4 +msgid "Keys" +msgstr "Keys" + +#: g15-config.glade.h:5 +msgid "Memory Bank" +msgstr "Memory Bank" + +#: g15-config.glade.h:6 +msgid "Options" +msgstr "Options" + +#: g15-config.glade.h:7 +msgid "Profiles" +msgstr "Profiles" + +#: g15-config.glade.h:8 +msgid "Switches" +msgstr "Switches" + +#: g15-config.glade.h:9 +msgid "A. Author " +msgstr "A. Author " + +#: g15-config.glade.h:10 +msgid "About Plugin" +msgstr "About Plugin" + +#: g15-config.glade.h:11 +msgid "Activate" +msgstr "Activate" + +#: g15-config.glade.h:12 +msgid "" +"Activate this profile when a window with\n" +"the following title has focus" +msgstr "" +"Activate this profile when a window with\n" +"the following title has focus" + +#: g15-config.glade.h:14 +msgid "" +"Activate this profile when no others are\n" +"active" +msgstr "" +"Activate this profile when no others are\n" +"active" + +#: g15-config.glade.h:16 +msgid "Activation" +msgstr "Activation" + +#: g15-config.glade.h:17 +msgid "Add Profile" +msgstr "Add Profile" + +#: g15-config.glade.h:18 +msgid "Are you sure you wish to remove this macro?" +msgstr "Are you sure you wish to remove this macro?" + +#: g15-config.glade.h:19 +msgid "Are you sure you wish to remove this profile?" +msgstr "Are you sure you wish to remove this profile?" + +#: g15-config.glade.h:20 +msgid "Author" +msgstr "Author" + +#: g15-config.glade.h:21 +msgid "Author:" +msgstr "Author:" + +#: g15-config.glade.h:22 +msgid "Background" +msgstr "Background" + +#: g15-config.glade.h:23 +msgid "Balh" +msgstr "Balh" + +#: g15-config.glade.h:24 +msgid "Clear" +msgstr "Clear" + +#: g15-config.glade.h:25 +msgid "Configure" +msgstr "Configure" + +#: g15-config.glade.h:26 +msgid "Copyright © 2006 A. Author" +msgstr "Copyright © 2006 A. Author" + +#: g15-config.glade.h:27 +msgid "Copyright:" +msgstr "Copyright:" + +#: g15-config.glade.h:28 +msgid "Cycle screens" +msgstr "Cycle screens" + +#: g15-config.glade.h:29 +msgid "Delays" +msgstr "Delays" + +#: g15-config.glade.h:30 +msgid "Description of plugin. bold" +msgstr "Description of plugin. bold" + +#: g15-config.glade.h:31 +msgid "Description:" +msgstr "Description:" + +#: g15-config.glade.h:32 +msgid "Driver" +msgstr "Driver" + +#: g15-config.glade.h:33 +msgid "Enabled" +msgstr "Enabled" + +#: g15-config.glade.h:34 +msgid "Every" +msgstr "Every" + +#: g15-config.glade.h:35 +msgid "Export" +msgstr "Export" + +#: g15-config.glade.h:36 +msgid "Get more profiles or upload yours" +msgstr "Get more profiles or upload yours" + +#: g15-config.glade.h:37 +msgid "Global Options" +msgstr "Global Options" + +#: g15-config.glade.h:38 +msgid "Icon" +msgstr "Icon" + +#: g15-config.glade.h:39 +msgid "Import" +msgstr "Import" + +#: g15-config.glade.h:40 +msgid "Information" +msgstr "Information" + +#: g15-config.glade.h:41 +msgid "Keyboard" +msgstr "Keyboard" + +#: g15-config.glade.h:42 +msgid "Logitech G Keyboard Configuration" +msgstr "Logitech G Keyboard Configuration" + +#: g15-config.glade.h:43 +msgid "Logitech Keyboard Device" +msgstr "Logitech Keyboard Device" + +#: g15-config.glade.h:44 +msgid "M1" +msgstr "M1" + +#: g15-config.glade.h:45 +msgid "M2" +msgstr "M2" + +#: g15-config.glade.h:46 +msgid "M3" +msgstr "M3" + +#: g15-config.glade.h:47 +msgid "Macros" +msgstr "Macros" + +#: g15-config.glade.h:48 +msgid "Models supported." +msgstr "Models supported." + +#: g15-config.glade.h:49 +msgid "Only Show Indicator On Error" +msgstr "Only Show Indicator On Error" + +#: g15-config.glade.h:50 +msgid "Plugin Name" +msgstr "Plugin Name" + +#: g15-config.glade.h:51 +msgid "Plugins" +msgstr "Plugins" + +#: g15-config.glade.h:52 +msgid "Press for" +msgstr "Press for" + +#: g15-config.glade.h:53 +msgid "Profile" +msgstr "Profile" + +#: g15-config.glade.h:54 +msgid "Profile Name:" +msgstr "Profile Name:" + +#: g15-config.glade.h:55 +msgid "Profile already exists" +msgstr "Profile already exists" + +#: g15-config.glade.h:56 +msgid "Release, then wait" +msgstr "Release, then wait" + +#: g15-config.glade.h:57 +msgid "Remove Macro" +msgstr "Remove Macro" + +#: g15-config.glade.h:58 +msgid "Remove Profile" +msgstr "Remove Profile" + +#: g15-config.glade.h:59 +msgid "Select Window" +msgstr "Select Window" + +#: g15-config.glade.h:60 +msgid "Select a window from those active" +msgstr "Select a window from those active" + +#: g15-config.glade.h:61 +msgid "Send delays with keystrokes" +msgstr "Send delays with keystrokes" + +#: g15-config.glade.h:62 +msgid "Site:" +msgstr "Site:" + +#: g15-config.glade.h:63 +msgid "Start Desktop Service On Login" +msgstr "Start Desktop Service On Login" + +#: g15-config.glade.h:64 +msgid "Start Indicator On Login" +msgstr "Start Indicator On Login" + +#: g15-config.glade.h:65 +msgid "Start System Tray Icon On Login" +msgstr "Start System Tray Icon On Login" + +#: g15-config.glade.h:66 +msgid "Stop Service" +msgstr "Stop Service" + +#: g15-config.glade.h:67 +msgid "Supported Models:" +msgstr "Supported Models:" + +#: g15-config.glade.h:68 +msgid "Test" +msgstr "Test" + +#: g15-config.glade.h:69 +msgid "" +"The profile name you have supplied already exists, please\n" +"choose another name." +msgstr "" +"The profile name you have supplied already exists, please\n" +"choose another name." + +#: g15-config.glade.h:71 +msgid "Use" +msgstr "Use" + +#: g15-config.glade.h:72 +msgid "Use fixed delay" +msgstr "Use fixed delay" + +#: g15-config.glade.h:73 +msgid "Use macros from another profile when they are not set in this one" +msgstr "Use macros from another profile when they are not set in this one" + +#: g15-config.glade.h:74 +msgid "Window" +msgstr "Window" + +#: g15-config.glade.h:75 +msgid "label" +msgstr "label" + +#: g15-config.glade.h:76 +msgid "seconds" +msgstr "seconds" + +#: g15-config.glade.h:77 +msgid "toolbutton1" +msgstr "toolbutton1" + +#: g15-config.glade.h:78 +msgid "toolbutton2" +msgstr "toolbutton2" diff --git a/i18n/g15-config.glade.h b/i18n/g15-config.glade.h new file mode 100644 index 0000000..3a0915c --- /dev/null +++ b/i18n/g15-config.glade.h @@ -0,0 +1,78 @@ +char *s = N_("Controls"); +char *s = N_("Global Plugins"); +char *s = N_("Images"); +char *s = N_("Keys"); +char *s = N_("Memory Bank"); +char *s = N_("Options"); +char *s = N_("Profiles"); +char *s = N_("Switches"); +char *s = N_("A. Author "); +char *s = N_("About Plugin"); +char *s = N_("Activate"); +char *s = N_("Activate this profile when a window with\n" + "the following title has focus"); +char *s = N_("Activate this profile when no others are\n" + "active"); +char *s = N_("Activation"); +char *s = N_("Add Profile"); +char *s = N_("Are you sure you wish to remove this macro?"); +char *s = N_("Are you sure you wish to remove this profile?"); +char *s = N_("Author"); +char *s = N_("Author:"); +char *s = N_("Background"); +char *s = N_("Balh"); +char *s = N_("Clear"); +char *s = N_("Configure"); +char *s = N_("Copyright © 2006 A. Author"); +char *s = N_("Copyright:"); +char *s = N_("Cycle screens"); +char *s = N_("Delays"); +char *s = N_("Description of plugin. bold"); +char *s = N_("Description:"); +char *s = N_("Driver"); +char *s = N_("Enabled"); +char *s = N_("Every"); +char *s = N_("Export"); +char *s = N_("Get more profiles or upload yours"); +char *s = N_("Global Options"); +char *s = N_("Icon"); +char *s = N_("Import"); +char *s = N_("Information"); +char *s = N_("Keyboard"); +char *s = N_("Logitech G Keyboard Configuration"); +char *s = N_("Logitech Keyboard Device"); +char *s = N_("M1"); +char *s = N_("M2"); +char *s = N_("M3"); +char *s = N_("Macros"); +char *s = N_("Models supported."); +char *s = N_("Only Show Indicator On Error"); +char *s = N_("Plugin Name"); +char *s = N_("Plugins"); +char *s = N_("Press for"); +char *s = N_("Profile"); +char *s = N_("Profile Name:"); +char *s = N_("Profile already exists"); +char *s = N_("Release, then wait"); +char *s = N_("Remove Macro"); +char *s = N_("Remove Profile"); +char *s = N_("Select Window"); +char *s = N_("Select a window from those active"); +char *s = N_("Send delays with keystrokes"); +char *s = N_("Site:"); +char *s = N_("Start Desktop Service On Login"); +char *s = N_("Start Indicator On Login"); +char *s = N_("Start System Tray Icon On Login"); +char *s = N_("Stop Service"); +char *s = N_("Supported Models:"); +char *s = N_("Test"); +char *s = N_("The profile name you have supplied already exists, please\n" + "choose another name."); +char *s = N_("Use"); +char *s = N_("Use fixed delay"); +char *s = N_("Use macros from another profile when they are not set in this one"); +char *s = N_("Window"); +char *s = N_("label"); +char *s = N_("seconds"); +char *s = N_("toolbutton1"); +char *s = N_("toolbutton2"); diff --git a/i18n/g15-config.pot b/i18n/g15-config.pot new file mode 100644 index 0000000..948914b --- /dev/null +++ b/i18n/g15-config.pot @@ -0,0 +1,324 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2011-10-09 14:52+0100\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: g15-config.glade.h:1 +msgid "Controls" +msgstr "" + +#: g15-config.glade.h:2 +msgid "Global Plugins" +msgstr "" + +#: g15-config.glade.h:3 +msgid "Images" +msgstr "" + +#: g15-config.glade.h:4 +msgid "Keys" +msgstr "" + +#: g15-config.glade.h:5 +msgid "Memory Bank" +msgstr "" + +#: g15-config.glade.h:6 +msgid "Options" +msgstr "" + +#: g15-config.glade.h:7 +msgid "Profiles" +msgstr "" + +#: g15-config.glade.h:8 +msgid "Switches" +msgstr "" + +#: g15-config.glade.h:9 +msgid "A. Author " +msgstr "" + +#: g15-config.glade.h:10 +msgid "About Plugin" +msgstr "" + +#: g15-config.glade.h:11 +msgid "Activate" +msgstr "" + +#: g15-config.glade.h:12 +msgid "" +"Activate this profile when a window with\n" +"the following title has focus" +msgstr "" + +#: g15-config.glade.h:14 +msgid "" +"Activate this profile when no others are\n" +"active" +msgstr "" + +#: g15-config.glade.h:16 +msgid "Activation" +msgstr "" + +#: g15-config.glade.h:17 +msgid "Add Profile" +msgstr "" + +#: g15-config.glade.h:18 +msgid "Are you sure you wish to remove this macro?" +msgstr "" + +#: g15-config.glade.h:19 +msgid "Are you sure you wish to remove this profile?" +msgstr "" + +#: g15-config.glade.h:20 +msgid "Author" +msgstr "" + +#: g15-config.glade.h:21 +msgid "Author:" +msgstr "" + +#: g15-config.glade.h:22 +msgid "Background" +msgstr "" + +#: g15-config.glade.h:23 +msgid "Balh" +msgstr "" + +#: g15-config.glade.h:24 +msgid "Clear" +msgstr "" + +#: g15-config.glade.h:25 +msgid "Configure" +msgstr "" + +#: g15-config.glade.h:26 +msgid "Copyright © 2006 A. Author" +msgstr "" + +#: g15-config.glade.h:27 +msgid "Copyright:" +msgstr "" + +#: g15-config.glade.h:28 +msgid "Cycle screens" +msgstr "" + +#: g15-config.glade.h:29 +msgid "Delays" +msgstr "" + +#: g15-config.glade.h:30 +msgid "Description of plugin. bold" +msgstr "" + +#: g15-config.glade.h:31 +msgid "Description:" +msgstr "" + +#: g15-config.glade.h:32 +msgid "Driver" +msgstr "" + +#: g15-config.glade.h:33 +msgid "Enabled" +msgstr "" + +#: g15-config.glade.h:34 +msgid "Every" +msgstr "" + +#: g15-config.glade.h:35 +msgid "Export" +msgstr "" + +#: g15-config.glade.h:36 +msgid "Get more profiles or upload yours" +msgstr "" + +#: g15-config.glade.h:37 +msgid "Global Options" +msgstr "" + +#: g15-config.glade.h:38 +msgid "Icon" +msgstr "" + +#: g15-config.glade.h:39 +msgid "Import" +msgstr "" + +#: g15-config.glade.h:40 +msgid "Information" +msgstr "" + +#: g15-config.glade.h:41 +msgid "Keyboard" +msgstr "" + +#: g15-config.glade.h:42 +msgid "Logitech G Keyboard Configuration" +msgstr "" + +#: g15-config.glade.h:43 +msgid "Logitech Keyboard Device" +msgstr "" + +#: g15-config.glade.h:44 +msgid "M1" +msgstr "" + +#: g15-config.glade.h:45 +msgid "M2" +msgstr "" + +#: g15-config.glade.h:46 +msgid "M3" +msgstr "" + +#: g15-config.glade.h:47 +msgid "Macros" +msgstr "" + +#: g15-config.glade.h:48 +msgid "Models supported." +msgstr "" + +#: g15-config.glade.h:49 +msgid "Only Show Indicator On Error" +msgstr "" + +#: g15-config.glade.h:50 +msgid "Plugin Name" +msgstr "" + +#: g15-config.glade.h:51 +msgid "Plugins" +msgstr "" + +#: g15-config.glade.h:52 +msgid "Press for" +msgstr "" + +#: g15-config.glade.h:53 +msgid "Profile" +msgstr "" + +#: g15-config.glade.h:54 +msgid "Profile Name:" +msgstr "" + +#: g15-config.glade.h:55 +msgid "Profile already exists" +msgstr "" + +#: g15-config.glade.h:56 +msgid "Release, then wait" +msgstr "" + +#: g15-config.glade.h:57 +msgid "Remove Macro" +msgstr "" + +#: g15-config.glade.h:58 +msgid "Remove Profile" +msgstr "" + +#: g15-config.glade.h:59 +msgid "Select Window" +msgstr "" + +#: g15-config.glade.h:60 +msgid "Select a window from those active" +msgstr "" + +#: g15-config.glade.h:61 +msgid "Send delays with keystrokes" +msgstr "" + +#: g15-config.glade.h:62 +msgid "Site:" +msgstr "" + +#: g15-config.glade.h:63 +msgid "Start Desktop Service On Login" +msgstr "" + +#: g15-config.glade.h:64 +msgid "Start Indicator On Login" +msgstr "" + +#: g15-config.glade.h:65 +msgid "Start System Tray Icon On Login" +msgstr "" + +#: g15-config.glade.h:66 +msgid "Stop Service" +msgstr "" + +#: g15-config.glade.h:67 +msgid "Supported Models:" +msgstr "" + +#: g15-config.glade.h:68 +msgid "Test" +msgstr "" + +#: g15-config.glade.h:69 +msgid "" +"The profile name you have supplied already exists, please\n" +"choose another name." +msgstr "" + +#: g15-config.glade.h:71 +msgid "Use" +msgstr "" + +#: g15-config.glade.h:72 +msgid "Use fixed delay" +msgstr "" + +#: g15-config.glade.h:73 +msgid "Use macros from another profile when they are not set in this one" +msgstr "" + +#: g15-config.glade.h:74 +msgid "Window" +msgstr "" + +#: g15-config.glade.h:75 +msgid "label" +msgstr "" + +#: g15-config.glade.h:76 +msgid "seconds" +msgstr "" + +#: g15-config.glade.h:77 +msgid "toolbutton1" +msgstr "" + +#: g15-config.glade.h:78 +msgid "toolbutton2" +msgstr "" diff --git a/i18n/gnome15-drivers.en_GB.po b/i18n/gnome15-drivers.en_GB.po new file mode 100644 index 0000000..3e75f0d --- /dev/null +++ b/i18n/gnome15-drivers.en_GB.po @@ -0,0 +1,177 @@ +# English translations for gnome15 package. +# Copyright (C) 2011 THE gnome15'S COPYRIGHT HOLDER +# This file is distributed under the same license as the gnome15 package. +# Brett Smith , 2011. +# +msgid "" +msgstr "" +"Project-Id-Version: gnome15 0.8.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2011-10-09 14:52+0100\n" +"PO-Revision-Date: 2011-10-09 14:54+0100\n" +"Last-Translator: Brett Smith \n" +"Language-Team: English (British)\n" +"Language: en_GB\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=ASCII\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: ../main/python/gnome15/drivers/driver_g15direct.py:57 +#: ../main/python/gnome15/drivers/driver_g15direct.py:236 +msgid "G15 Direct" +msgstr "G15 Direct" + +#: ../main/python/gnome15/drivers/driver_g15direct.py:59 +msgid "" +"For use with the G15 based devices only, this driver communicates directly, " +msgstr "" +"For use with the G15 based devices only, this driver communicates directly, " + +#: ../main/python/gnome15/drivers/driver_g15direct.py:120 +#: ../main/python/gnome15/drivers/driver_g15.py:106 +#: ../main/python/gnome15/drivers/driver_g19direct.py:104 +#: ../main/python/gnome15/drivers/driver_g19.py:147 +#: ../main/python/gnome15/drivers/driver_gtk.py:46 +#: ../main/python/gnome15/drivers/driver_gtk.py:53 +#: ../main/python/gnome15/drivers/driver_kernel.py:194 +#: ../main/python/gnome15/drivers/driver_kernel.py:206 +msgid "Memory Bank Keys" +msgstr "Memory Bank Keys" + +#: ../main/python/gnome15/drivers/driver_g15direct.py:121 +#: ../main/python/gnome15/drivers/driver_g15direct.py:122 +#: ../main/python/gnome15/drivers/driver_g15.py:107 +#: ../main/python/gnome15/drivers/driver_g15.py:108 +#: ../main/python/gnome15/drivers/driver_g19direct.py:105 +#: ../main/python/gnome15/drivers/driver_g19.py:148 +#: ../main/python/gnome15/drivers/driver_gtk.py:47 +#: ../main/python/gnome15/drivers/driver_gtk.py:57 +#: ../main/python/gnome15/drivers/driver_kernel.py:195 +#: ../main/python/gnome15/drivers/driver_kernel.py:203 +msgid "Keyboard Backlight Colour" +msgstr "Keyboard Backlight Colour" + +#: ../main/python/gnome15/drivers/driver_g15direct.py:123 +#: ../main/python/gnome15/drivers/driver_g15.py:109 +#: ../main/python/gnome15/drivers/driver_gtk.py:54 +#: ../main/python/gnome15/drivers/driver_kernel.py:207 +msgid "Keyboard Backlight Level" +msgstr "Keyboard Backlight Level" + +#: ../main/python/gnome15/drivers/driver_g15direct.py:124 +#: ../main/python/gnome15/drivers/driver_g15.py:110 +msgid "LCD Backlight Level" +msgstr "LCD Backlight Level" + +#: ../main/python/gnome15/drivers/driver_g15direct.py:125 +#: ../main/python/gnome15/drivers/driver_g15.py:111 +#: ../main/python/gnome15/drivers/driver_kernel.py:209 +msgid "LCD Contrast" +msgstr "LCD Contrast" + +#: ../main/python/gnome15/drivers/driver_g15direct.py:126 +#: ../main/python/gnome15/drivers/driver_g15.py:112 +#: ../main/python/gnome15/drivers/driver_gtk.py:55 +#: ../main/python/gnome15/drivers/driver_kernel.py:210 +msgid "Invert LCD" +msgstr "Invert LCD" + +#: ../main/python/gnome15/drivers/driver_g15.py:49 +msgid "G15Daemon" +msgstr "G15Daemon" + +#: ../main/python/gnome15/drivers/driver_g15.py:51 +msgid "" +"For use with the Logitech G15v1, G15v2, G13, G510 and G110. This driver uses " +"g15daemon, available from " +msgstr "" +"For use with the Logitech G15v1, G15v2, G13, G510 and G110. This driver uses " +"g15daemon, available from " + +#: ../main/python/gnome15/drivers/driver_g15.py:354 +msgid "g15daemon driver" +msgstr "g15daemon driver" + +#: ../main/python/gnome15/drivers/driver_g19direct.py:53 +msgid "G19 Direct" +msgstr "G19 Direct" + +#: ../main/python/gnome15/drivers/driver_g19direct.py:55 +msgid "For use with the Logitech G19 only, this driver communicates directly, " +msgstr "" +"For use with the Logitech G19 only, this driver communicates directly, " + +#: ../main/python/gnome15/drivers/driver_g19direct.py:106 +#: ../main/python/gnome15/drivers/driver_g19.py:149 +msgid "Boot Keyboard Backlight Colour" +msgstr "Boot Keyboard Backlight Colour" + +#: ../main/python/gnome15/drivers/driver_g19direct.py:107 +#: ../main/python/gnome15/drivers/driver_g19.py:150 +#: ../main/python/gnome15/drivers/driver_gtk.py:48 +#: ../main/python/gnome15/drivers/driver_kernel.py:197 +msgid "LCD Brightness" +msgstr "LCD Brightness" + +#: ../main/python/gnome15/drivers/driver_g19direct.py:108 +#: ../main/python/gnome15/drivers/driver_g19.py:151 +#: ../main/python/gnome15/drivers/driver_gtk.py:49 +#: ../main/python/gnome15/drivers/driver_kernel.py:198 +msgid "Default LCD Foreground" +msgstr "Default LCD Foreground" + +#: ../main/python/gnome15/drivers/driver_g19direct.py:109 +#: ../main/python/gnome15/drivers/driver_g19.py:152 +#: ../main/python/gnome15/drivers/driver_gtk.py:50 +#: ../main/python/gnome15/drivers/driver_kernel.py:199 +msgid "Default LCD Background" +msgstr "Default LCD Background" + +#: ../main/python/gnome15/drivers/driver_g19direct.py:110 +#: ../main/python/gnome15/drivers/driver_g19.py:153 +#: ../main/python/gnome15/drivers/driver_gtk.py:51 +#: ../main/python/gnome15/drivers/driver_kernel.py:200 +msgid "Default Highlight Color" +msgstr "Default Highlight Color" + +#: ../main/python/gnome15/drivers/driver_g19direct.py:163 +#: ../main/python/gnome15/drivers/driver_g19.py:199 +msgid "G19D Network Daemon Driver" +msgstr "G19D Network Daemon Driver" + +#: ../main/python/gnome15/drivers/driver_g19.py:47 +msgid "G19D" +msgstr "G19D" + +#: ../main/python/gnome15/drivers/driver_g19.py:49 +msgid "For use with the Logitech G19 only, this driver uses G19D, " +msgstr "For use with the Logitech G19 only, this driver uses G19D, " + +#: ../main/python/gnome15/drivers/driver_gtk.py:38 +msgid "GTK Virtual Keyboard Driver" +msgstr "GTK Virtual Keyboard Driver" + +#: ../main/python/gnome15/drivers/driver_gtk.py:39 +msgid "A special development driver that emulates all supported, " +msgstr "A special development driver that emulates all supported, " + +#: ../main/python/gnome15/drivers/driver_gtk.py:136 +msgid "GTK Keyboard Emulator Driver" +msgstr "GTK Keyboard Emulator Driver" + +#: ../main/python/gnome15/drivers/driver_kernel.py:51 +msgid "Kernel Drivers" +msgstr "Kernel Drivers" + +#: ../main/python/gnome15/drivers/driver_kernel.py:52 +msgid "" +"Requires ali123's Logitech Kernel drivers. This method requires no other " +"daemons to be running, and works with the G13, G15, G19 and G110 keyboards. " +msgstr "" +"Requires ali123's Logitech Kernel drivers. This method requires no other " +"daemons to be running, and works with the G13, G15, G19 and G110 keyboards. " + +#: ../main/python/gnome15/drivers/driver_kernel.py:208 +msgid "LCD Backlight" +msgstr "LCD Backlight" diff --git a/i18n/gnome15-drivers.pot b/i18n/gnome15-drivers.pot new file mode 100644 index 0000000..dd33aa8 --- /dev/null +++ b/i18n/gnome15-drivers.pot @@ -0,0 +1,171 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2011-10-09 14:52+0100\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: ../main/python/gnome15/drivers/driver_g15direct.py:57 +#: ../main/python/gnome15/drivers/driver_g15direct.py:236 +msgid "G15 Direct" +msgstr "" + +#: ../main/python/gnome15/drivers/driver_g15direct.py:59 +msgid "" +"For use with the G15 based devices only, this driver communicates directly, " +msgstr "" + +#: ../main/python/gnome15/drivers/driver_g15direct.py:120 +#: ../main/python/gnome15/drivers/driver_g15.py:106 +#: ../main/python/gnome15/drivers/driver_g19direct.py:104 +#: ../main/python/gnome15/drivers/driver_g19.py:147 +#: ../main/python/gnome15/drivers/driver_gtk.py:46 +#: ../main/python/gnome15/drivers/driver_gtk.py:53 +#: ../main/python/gnome15/drivers/driver_kernel.py:194 +#: ../main/python/gnome15/drivers/driver_kernel.py:206 +msgid "Memory Bank Keys" +msgstr "" + +#: ../main/python/gnome15/drivers/driver_g15direct.py:121 +#: ../main/python/gnome15/drivers/driver_g15direct.py:122 +#: ../main/python/gnome15/drivers/driver_g15.py:107 +#: ../main/python/gnome15/drivers/driver_g15.py:108 +#: ../main/python/gnome15/drivers/driver_g19direct.py:105 +#: ../main/python/gnome15/drivers/driver_g19.py:148 +#: ../main/python/gnome15/drivers/driver_gtk.py:47 +#: ../main/python/gnome15/drivers/driver_gtk.py:57 +#: ../main/python/gnome15/drivers/driver_kernel.py:195 +#: ../main/python/gnome15/drivers/driver_kernel.py:203 +msgid "Keyboard Backlight Colour" +msgstr "" + +#: ../main/python/gnome15/drivers/driver_g15direct.py:123 +#: ../main/python/gnome15/drivers/driver_g15.py:109 +#: ../main/python/gnome15/drivers/driver_gtk.py:54 +#: ../main/python/gnome15/drivers/driver_kernel.py:207 +msgid "Keyboard Backlight Level" +msgstr "" + +#: ../main/python/gnome15/drivers/driver_g15direct.py:124 +#: ../main/python/gnome15/drivers/driver_g15.py:110 +msgid "LCD Backlight Level" +msgstr "" + +#: ../main/python/gnome15/drivers/driver_g15direct.py:125 +#: ../main/python/gnome15/drivers/driver_g15.py:111 +#: ../main/python/gnome15/drivers/driver_kernel.py:209 +msgid "LCD Contrast" +msgstr "" + +#: ../main/python/gnome15/drivers/driver_g15direct.py:126 +#: ../main/python/gnome15/drivers/driver_g15.py:112 +#: ../main/python/gnome15/drivers/driver_gtk.py:55 +#: ../main/python/gnome15/drivers/driver_kernel.py:210 +msgid "Invert LCD" +msgstr "" + +#: ../main/python/gnome15/drivers/driver_g15.py:49 +msgid "G15Daemon" +msgstr "" + +#: ../main/python/gnome15/drivers/driver_g15.py:51 +msgid "" +"For use with the Logitech G15v1, G15v2, G13, G510 and G110. This driver uses " +"g15daemon, available from " +msgstr "" + +#: ../main/python/gnome15/drivers/driver_g15.py:354 +msgid "g15daemon driver" +msgstr "" + +#: ../main/python/gnome15/drivers/driver_g19direct.py:53 +msgid "G19 Direct" +msgstr "" + +#: ../main/python/gnome15/drivers/driver_g19direct.py:55 +msgid "For use with the Logitech G19 only, this driver communicates directly, " +msgstr "" + +#: ../main/python/gnome15/drivers/driver_g19direct.py:106 +#: ../main/python/gnome15/drivers/driver_g19.py:149 +msgid "Boot Keyboard Backlight Colour" +msgstr "" + +#: ../main/python/gnome15/drivers/driver_g19direct.py:107 +#: ../main/python/gnome15/drivers/driver_g19.py:150 +#: ../main/python/gnome15/drivers/driver_gtk.py:48 +#: ../main/python/gnome15/drivers/driver_kernel.py:197 +msgid "LCD Brightness" +msgstr "" + +#: ../main/python/gnome15/drivers/driver_g19direct.py:108 +#: ../main/python/gnome15/drivers/driver_g19.py:151 +#: ../main/python/gnome15/drivers/driver_gtk.py:49 +#: ../main/python/gnome15/drivers/driver_kernel.py:198 +msgid "Default LCD Foreground" +msgstr "" + +#: ../main/python/gnome15/drivers/driver_g19direct.py:109 +#: ../main/python/gnome15/drivers/driver_g19.py:152 +#: ../main/python/gnome15/drivers/driver_gtk.py:50 +#: ../main/python/gnome15/drivers/driver_kernel.py:199 +msgid "Default LCD Background" +msgstr "" + +#: ../main/python/gnome15/drivers/driver_g19direct.py:110 +#: ../main/python/gnome15/drivers/driver_g19.py:153 +#: ../main/python/gnome15/drivers/driver_gtk.py:51 +#: ../main/python/gnome15/drivers/driver_kernel.py:200 +msgid "Default Highlight Color" +msgstr "" + +#: ../main/python/gnome15/drivers/driver_g19direct.py:163 +#: ../main/python/gnome15/drivers/driver_g19.py:199 +msgid "G19D Network Daemon Driver" +msgstr "" + +#: ../main/python/gnome15/drivers/driver_g19.py:47 +msgid "G19D" +msgstr "" + +#: ../main/python/gnome15/drivers/driver_g19.py:49 +msgid "For use with the Logitech G19 only, this driver uses G19D, " +msgstr "" + +#: ../main/python/gnome15/drivers/driver_gtk.py:38 +msgid "GTK Virtual Keyboard Driver" +msgstr "" + +#: ../main/python/gnome15/drivers/driver_gtk.py:39 +msgid "A special development driver that emulates all supported, " +msgstr "" + +#: ../main/python/gnome15/drivers/driver_gtk.py:136 +msgid "GTK Keyboard Emulator Driver" +msgstr "" + +#: ../main/python/gnome15/drivers/driver_kernel.py:51 +msgid "Kernel Drivers" +msgstr "" + +#: ../main/python/gnome15/drivers/driver_kernel.py:52 +msgid "" +"Requires ali123's Logitech Kernel drivers. This method requires no other " +"daemons to be running, and works with the G13, G15, G19 and G110 keyboards. " +msgstr "" + +#: ../main/python/gnome15/drivers/driver_kernel.py:208 +msgid "LCD Backlight" +msgstr "" diff --git a/i18n/gnome15.en_GB.po b/i18n/gnome15.en_GB.po new file mode 100644 index 0000000..e14c53b --- /dev/null +++ b/i18n/gnome15.en_GB.po @@ -0,0 +1,187 @@ +# English translations for gnome15 package. +# Copyright (C) 2011 THE gnome15'S COPYRIGHT HOLDER +# This file is distributed under the same license as the gnome15 package. +# Brett Smith , 2011. +# +msgid "" +msgstr "" +"Project-Id-Version: gnome15 0.8.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2011-10-09 14:52+0100\n" +"PO-Revision-Date: 2011-10-09 14:54+0100\n" +"Last-Translator: Brett Smith \n" +"Language-Team: English (British)\n" +"Language: en_GB\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=ASCII\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: ../main/python/gnome15/g15config.py:409 +msgid "Start Service" +msgstr "Start Service" + +#: ../main/python/gnome15/g15config.py:417 +msgid "No supported devices could be found. Is the " +msgstr "No supported devices could be found. Is the " + +#: ../main/python/gnome15/g15config.py:572 +msgid "The Gnome15 desktop service is not running. It is recommended " +msgstr "The Gnome15 desktop service is not running. It is recommended " + +#: ../main/python/gnome15/g15config.py:577 +msgid "The Gnome15 desktop service is starting up. Please wait" +msgstr "The Gnome15 desktop service is starting up. Please wait" + +#: ../main/python/gnome15/g15config.py:581 +msgid "The Gnome15 desktop service is stopping." +msgstr "The Gnome15 desktop service is stopping." + +#: ../main/python/gnome15/g15config.py:599 +msgid "The Gnome15 desktop service is running, but failed to connect " +msgstr "The Gnome15 desktop service is running, but failed to connect " + +#: ../main/python/gnome15/g15config.py:754 +msgid "Hold" +msgstr "Hold" + +#: ../main/python/gnome15/g15config.py:932 +msgid "There is no appropriate driver for the device " +msgstr "There is no appropriate driver for the device " + +#: ../main/python/gnome15/g15config.py:952 +msgid "This driver has no configuration options" +msgstr "This driver has no configuration options" + +#: ../main/python/gnome15/g15config.py:1243 +#: ../main/python/gnome15/g15macroeditor.py:311 +#, python-format +msgid "Macro %s" +msgstr "Macro %s" + +#: ../main/python/gnome15/g15config.py:1635 +msgid "Set backlight colour" +msgstr "Set backlight colour" + +#: ../main/python/gnome15/g15desktop.py:824 +msgid "Desktop integration for Logitech 'G' keyboards." +msgstr "Desktop integration for Logitech 'G' keyboards." + +#: ../main/python/gnome15/g15desktop.py:878 +msgid "Stop Desktop Service" +msgstr "Stop Desktop Service" + +#: ../main/python/gnome15/g15desktop.py:901 +msgid "Cycle screens automatically" +msgstr "Cycle screens automatically" + +#: ../main/python/gnome15/g15desktop.py:923 +#, python-format +msgid "Enable %s" +msgstr "Enable %s" + +#: ../main/python/gnome15/g15desktop.py:937 +msgid "Start Desktop Service" +msgstr "Start Desktop Service" + +#: ../main/python/gnome15/g15desktop.py:980 +#, python-format +msgid "" +"%s is now the active keyboard. Use mouse wheel up and down to cycle screens " +"on this device" +msgstr "" +"%s is now the active keyboard. Use mouse wheel up and down to cycle screens " +"on this device" + +#: ../main/python/gnome15/g15devices.py:310 +msgid "Virtual LCD Window" +msgstr "Virtual LCD Window" + +#: ../main/python/gnome15/g15devices.py:311 +msgid "Logitech G11 Keyboard" +msgstr "Logitech G11 Keyboard" + +#: ../main/python/gnome15/g15devices.py:312 +msgid "Logitech G19 Gaming Keyboard" +msgstr "Logitech G19 Gaming Keyboard" + +#: ../main/python/gnome15/g15devices.py:313 +msgid "Logitech G15 Gaming Keyboard (version 1)" +msgstr "Logitech G15 Gaming Keyboard (version 1)" + +#: ../main/python/gnome15/g15devices.py:314 +msgid "Logitech G15 Gaming Keyboard (version 2)" +msgstr "Logitech G15 Gaming Keyboard (version 2)" + +#: ../main/python/gnome15/g15devices.py:315 +msgid "Logitech G13 Advanced Gameboard" +msgstr "Logitech G13 Advanced Gameboard" + +#: ../main/python/gnome15/g15devices.py:316 +msgid "Logitech G510 Keyboard" +msgstr "Logitech G510 Keyboard" + +#: ../main/python/gnome15/g15devices.py:317 +msgid "Logitech G510 Keyboard (audio)" +msgstr "Logitech G510 Keyboard (audio)" + +#: ../main/python/gnome15/g15devices.py:318 +msgid "Logitech Z10 Speakers" +msgstr "Logitech Z10 Speakers" + +#: ../main/python/gnome15/g15devices.py:319 +msgid "Logitech G110 Keyboard" +msgstr "Logitech G110 Keyboard" + +#: ../main/python/gnome15/g15devices.py:320 +msgid "Logitech GamePanel" +msgstr "Logitech GamePanel" + +#: ../main/python/gnome15/g15devices.py:323 +msgid "Logitech MX5500" +msgstr "Logitech MX5500" + +#: ../main/python/gnome15/g15drivermanager.py:94 +#: ../main/python/gnome15/g15drivermanager.py:110 +#, python-format +msgid "No drivers support the model %s" +msgstr "No drivers support the model %s" + +#: ../main/python/gnome15/g15drivermanager.py:101 +#, python-format +msgid "" +"Driver %s is not available. Do you have to appropriate package installed?" +msgstr "" +"Driver %s is not available. Do you have to appropriate package installed?" + +#: ../main/python/gnome15/g15exceptions.py:25 +msgid "Failed to connect." +msgstr "Failed to connect." + +#: ../main/python/gnome15/g15macroeditor.py:257 +msgid "Open.." +msgstr "Open.." + +#: ../main/python/gnome15/g15macroeditor.py:264 +msgid "All files" +msgstr "All files" + +#: ../main/python/gnome15/g15macroeditor.py:466 +msgid "This key combination is already in use with " +msgstr "This key combination is already in use with " + +#: ../main/python/gnome15/g15macroeditor.py:476 +msgid "This key combination is reserved for use with an action. You " +msgstr "This key combination is reserved for use with an action. You " + +#: ../main/python/gnome15/g15macroeditor.py:485 +msgid "You have not chosen a macro key to assign the action to." +msgstr "You have not chosen a macro key to assign the action to." + +#: ../main/python/gnome15/g15screen.py:505 +msgid "The current device has no suitable output device" +msgstr "The current device has no suitable output device" + +#: ../main/python/gnome15/g15screen.py:1678 +msgid "Starting up .." +msgstr "Starting up .." diff --git a/i18n/gnome15.pot b/i18n/gnome15.pot new file mode 100644 index 0000000..98c724e --- /dev/null +++ b/i18n/gnome15.pot @@ -0,0 +1,184 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2011-10-09 14:52+0100\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: ../main/python/gnome15/g15config.py:409 +msgid "Start Service" +msgstr "" + +#: ../main/python/gnome15/g15config.py:417 +msgid "No supported devices could be found. Is the " +msgstr "" + +#: ../main/python/gnome15/g15config.py:572 +msgid "The Gnome15 desktop service is not running. It is recommended " +msgstr "" + +#: ../main/python/gnome15/g15config.py:577 +msgid "The Gnome15 desktop service is starting up. Please wait" +msgstr "" + +#: ../main/python/gnome15/g15config.py:581 +msgid "The Gnome15 desktop service is stopping." +msgstr "" + +#: ../main/python/gnome15/g15config.py:599 +msgid "The Gnome15 desktop service is running, but failed to connect " +msgstr "" + +#: ../main/python/gnome15/g15config.py:754 +msgid "Hold" +msgstr "" + +#: ../main/python/gnome15/g15config.py:932 +msgid "There is no appropriate driver for the device " +msgstr "" + +#: ../main/python/gnome15/g15config.py:952 +msgid "This driver has no configuration options" +msgstr "" + +#: ../main/python/gnome15/g15config.py:1243 +#: ../main/python/gnome15/g15macroeditor.py:311 +#, python-format +msgid "Macro %s" +msgstr "" + +#: ../main/python/gnome15/g15config.py:1635 +msgid "Set backlight colour" +msgstr "" + +#: ../main/python/gnome15/g15desktop.py:824 +msgid "Desktop integration for Logitech 'G' keyboards." +msgstr "" + +#: ../main/python/gnome15/g15desktop.py:878 +msgid "Stop Desktop Service" +msgstr "" + +#: ../main/python/gnome15/g15desktop.py:901 +msgid "Cycle screens automatically" +msgstr "" + +#: ../main/python/gnome15/g15desktop.py:923 +#, python-format +msgid "Enable %s" +msgstr "" + +#: ../main/python/gnome15/g15desktop.py:937 +msgid "Start Desktop Service" +msgstr "" + +#: ../main/python/gnome15/g15desktop.py:980 +#, python-format +msgid "" +"%s is now the active keyboard. Use mouse wheel up and down to cycle screens " +"on this device" +msgstr "" + +#: ../main/python/gnome15/g15devices.py:310 +msgid "Virtual LCD Window" +msgstr "" + +#: ../main/python/gnome15/g15devices.py:311 +msgid "Logitech G11 Keyboard" +msgstr "" + +#: ../main/python/gnome15/g15devices.py:312 +msgid "Logitech G19 Gaming Keyboard" +msgstr "" + +#: ../main/python/gnome15/g15devices.py:313 +msgid "Logitech G15 Gaming Keyboard (version 1)" +msgstr "" + +#: ../main/python/gnome15/g15devices.py:314 +msgid "Logitech G15 Gaming Keyboard (version 2)" +msgstr "" + +#: ../main/python/gnome15/g15devices.py:315 +msgid "Logitech G13 Advanced Gameboard" +msgstr "" + +#: ../main/python/gnome15/g15devices.py:316 +msgid "Logitech G510 Keyboard" +msgstr "" + +#: ../main/python/gnome15/g15devices.py:317 +msgid "Logitech G510 Keyboard (audio)" +msgstr "" + +#: ../main/python/gnome15/g15devices.py:318 +msgid "Logitech Z10 Speakers" +msgstr "" + +#: ../main/python/gnome15/g15devices.py:319 +msgid "Logitech G110 Keyboard" +msgstr "" + +#: ../main/python/gnome15/g15devices.py:320 +msgid "Logitech GamePanel" +msgstr "" + +#: ../main/python/gnome15/g15devices.py:323 +msgid "Logitech MX5500" +msgstr "" + +#: ../main/python/gnome15/g15drivermanager.py:94 +#: ../main/python/gnome15/g15drivermanager.py:110 +#, python-format +msgid "No drivers support the model %s" +msgstr "" + +#: ../main/python/gnome15/g15drivermanager.py:101 +#, python-format +msgid "" +"Driver %s is not available. Do you have to appropriate package installed?" +msgstr "" + +#: ../main/python/gnome15/g15exceptions.py:25 +msgid "Failed to connect." +msgstr "" + +#: ../main/python/gnome15/g15macroeditor.py:257 +msgid "Open.." +msgstr "" + +#: ../main/python/gnome15/g15macroeditor.py:264 +msgid "All files" +msgstr "" + +#: ../main/python/gnome15/g15macroeditor.py:466 +msgid "This key combination is already in use with " +msgstr "" + +#: ../main/python/gnome15/g15macroeditor.py:476 +msgid "This key combination is reserved for use with an action. You " +msgstr "" + +#: ../main/python/gnome15/g15macroeditor.py:485 +msgid "You have not chosen a macro key to assign the action to." +msgstr "" + +#: ../main/python/gnome15/g15screen.py:505 +msgid "The current device has no suitable output device" +msgstr "" + +#: ../main/python/gnome15/g15screen.py:1678 +msgid "Starting up .." +msgstr "" diff --git a/i18n/macro-editor.en_GB.po b/i18n/macro-editor.en_GB.po new file mode 100644 index 0000000..738ff64 --- /dev/null +++ b/i18n/macro-editor.en_GB.po @@ -0,0 +1,138 @@ +# English translations for gnome15 package. +# Copyright (C) 2011 THE gnome15'S COPYRIGHT HOLDER +# This file is distributed under the same license as the gnome15 package. +# Brett Smith , 2011. +# +msgid "" +msgstr "" +"Project-Id-Version: gnome15 0.8.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2011-10-09 14:52+0100\n" +"PO-Revision-Date: 2011-10-09 14:54+0100\n" +"Last-Translator: Brett Smith \n" +"Language-Team: English (British)\n" +"Language: en_GB\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=ASCII\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: macro-editor.glade.h:1 +msgid "Keys Pressed" +msgstr "Keys Pressed" + +#: macro-editor.glade.h:2 +msgid "Memory Bank:" +msgstr "Memory Bank:" + +#: macro-editor.glade.h:3 +msgid "Name:" +msgstr "Name:" + +#: macro-editor.glade.h:4 +msgid "Repetition" +msgstr "Repetition" + +#: macro-editor.glade.h:5 +msgid "Target:" +msgstr "Target:" + +#: macro-editor.glade.h:6 +msgid "Action" +msgstr "Action" + +#: macro-editor.glade.h:7 +msgid "Allow combination of keys" +msgstr "Allow combination of keys" + +#: macro-editor.glade.h:8 +msgid "BTN_TEST" +msgstr "BTN_TEST" + +#: macro-editor.glade.h:9 +msgid "Delay between repeats" +msgstr "Delay between repeats" + +#: macro-editor.glade.h:10 +msgid "Digital Joystick" +msgstr "Digital Joystick" + +#: macro-editor.glade.h:11 +msgid "Edit Macro" +msgstr "Edit Macro" + +#: macro-editor.glade.h:12 +msgid "Filter:" +msgstr "Filter:" + +#: macro-editor.glade.h:13 +msgid "Joystick Button" +msgstr "Joystick Button" + +#: macro-editor.glade.h:14 +msgid "KEY_TEST" +msgstr "KEY_TEST" + +#: macro-editor.glade.h:15 +msgid "Keyboard Key" +msgstr "Keyboard Key" + +#: macro-editor.glade.h:16 +msgid "Macro Script" +msgstr "Macro Script" + +#: macro-editor.glade.h:17 +msgid "Mode:" +msgstr "Mode:" + +#: macro-editor.glade.h:18 +msgid "Mouse Button" +msgstr "Mouse Button" + +#: macro-editor.glade.h:19 +msgid "None" +msgstr "None" + +#: macro-editor.glade.h:20 +msgid "Override default repeat delay" +msgstr "Override default repeat delay" + +#: macro-editor.glade.h:21 +msgid "Run Command" +msgstr "Run Command" + +#: macro-editor.glade.h:22 +msgid "Simple Macro" +msgstr "Simple Macro" + +#: macro-editor.glade.h:23 +msgid "Toggle" +msgstr "Toggle" + +#: macro-editor.glade.h:24 +msgid "When held" +msgstr "When held" + +#: macro-editor.glade.h:25 +msgid "You can use & to run the command in the background" +msgstr "You can use & to run the command in the background" + +#: macro-editor.glade.h:26 +msgid "" +"\\r for Return, \\e for escape, \\b for backspace, \\t for tab\n" +"\\p for pause and \\\\ for backslash" +msgstr "" +"\\r for Return, \\e for escape, \\b for backspace, \\t for tab\n" +"\\p for pause and \\\\ for backslash" + +#: macro-editor.glade.h:28 +msgid "_Browse" +msgstr "_Browse" + +#: macro-editor.glade.h:29 +msgid "column" +msgstr "column" + +#: macro-editor.glade.h:30 +msgid "label" +msgstr "label" diff --git a/i18n/macro-editor.glade.h b/i18n/macro-editor.glade.h new file mode 100644 index 0000000..2ffbf24 --- /dev/null +++ b/i18n/macro-editor.glade.h @@ -0,0 +1,30 @@ +char *s = N_("Keys Pressed"); +char *s = N_("Memory Bank:"); +char *s = N_("Name:"); +char *s = N_("Repetition"); +char *s = N_("Target:"); +char *s = N_("Action"); +char *s = N_("Allow combination of keys"); +char *s = N_("BTN_TEST"); +char *s = N_("Delay between repeats"); +char *s = N_("Digital Joystick"); +char *s = N_("Edit Macro"); +char *s = N_("Filter:"); +char *s = N_("Joystick Button"); +char *s = N_("KEY_TEST"); +char *s = N_("Keyboard Key"); +char *s = N_("Macro Script"); +char *s = N_("Mode:"); +char *s = N_("Mouse Button"); +char *s = N_("None"); +char *s = N_("Override default repeat delay"); +char *s = N_("Run Command"); +char *s = N_("Simple Macro"); +char *s = N_("Toggle"); +char *s = N_("When held"); +char *s = N_("You can use & to run the command in the background"); +char *s = N_("\\r for Return, \\e for escape, \\b for backspace, \\t for tab\n" + "\\p for pause and \\\\ for backslash"); +char *s = N_("_Browse"); +char *s = N_("column"); +char *s = N_("label"); diff --git a/i18n/macro-editor.pot b/i18n/macro-editor.pot new file mode 100644 index 0000000..092137c --- /dev/null +++ b/i18n/macro-editor.pot @@ -0,0 +1,136 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2011-10-09 14:52+0100\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: macro-editor.glade.h:1 +msgid "Keys Pressed" +msgstr "" + +#: macro-editor.glade.h:2 +msgid "Memory Bank:" +msgstr "" + +#: macro-editor.glade.h:3 +msgid "Name:" +msgstr "" + +#: macro-editor.glade.h:4 +msgid "Repetition" +msgstr "" + +#: macro-editor.glade.h:5 +msgid "Target:" +msgstr "" + +#: macro-editor.glade.h:6 +msgid "Action" +msgstr "" + +#: macro-editor.glade.h:7 +msgid "Allow combination of keys" +msgstr "" + +#: macro-editor.glade.h:8 +msgid "BTN_TEST" +msgstr "" + +#: macro-editor.glade.h:9 +msgid "Delay between repeats" +msgstr "" + +#: macro-editor.glade.h:10 +msgid "Digital Joystick" +msgstr "" + +#: macro-editor.glade.h:11 +msgid "Edit Macro" +msgstr "" + +#: macro-editor.glade.h:12 +msgid "Filter:" +msgstr "" + +#: macro-editor.glade.h:13 +msgid "Joystick Button" +msgstr "" + +#: macro-editor.glade.h:14 +msgid "KEY_TEST" +msgstr "" + +#: macro-editor.glade.h:15 +msgid "Keyboard Key" +msgstr "" + +#: macro-editor.glade.h:16 +msgid "Macro Script" +msgstr "" + +#: macro-editor.glade.h:17 +msgid "Mode:" +msgstr "" + +#: macro-editor.glade.h:18 +msgid "Mouse Button" +msgstr "" + +#: macro-editor.glade.h:19 +msgid "None" +msgstr "" + +#: macro-editor.glade.h:20 +msgid "Override default repeat delay" +msgstr "" + +#: macro-editor.glade.h:21 +msgid "Run Command" +msgstr "" + +#: macro-editor.glade.h:22 +msgid "Simple Macro" +msgstr "" + +#: macro-editor.glade.h:23 +msgid "Toggle" +msgstr "" + +#: macro-editor.glade.h:24 +msgid "When held" +msgstr "" + +#: macro-editor.glade.h:25 +msgid "You can use & to run the command in the background" +msgstr "" + +#: macro-editor.glade.h:26 +msgid "" +"\\r for Return, \\e for escape, \\b for backspace, \\t for tab\n" +"\\p for pause and \\\\ for backslash" +msgstr "" + +#: macro-editor.glade.h:28 +msgid "_Browse" +msgstr "" + +#: macro-editor.glade.h:29 +msgid "column" +msgstr "" + +#: macro-editor.glade.h:30 +msgid "label" +msgstr "" diff --git a/m4/ax_python_devel.m4 b/m4/ax_python_devel.m4 new file mode 100644 index 0000000..436a0bd --- /dev/null +++ b/m4/ax_python_devel.m4 @@ -0,0 +1,324 @@ +# =========================================================================== +# http://www.gnu.org/software/autoconf-archive/ax_python_devel.html +# =========================================================================== +# +# SYNOPSIS +# +# AX_PYTHON_DEVEL([version]) +# +# DESCRIPTION +# +# Note: Defines as a precious variable "PYTHON_VERSION". Don't override it +# in your configure.ac. +# +# This macro checks for Python and tries to get the include path to +# 'Python.h'. It provides the $(PYTHON_CPPFLAGS) and $(PYTHON_LDFLAGS) +# output variables. It also exports $(PYTHON_EXTRA_LIBS) and +# $(PYTHON_EXTRA_LDFLAGS) for embedding Python in your code. +# +# You can search for some particular version of Python by passing a +# parameter to this macro, for example ">= '2.3.1'", or "== '2.4'". Please +# note that you *have* to pass also an operator along with the version to +# match, and pay special attention to the single quotes surrounding the +# version number. Don't use "PYTHON_VERSION" for this: that environment +# variable is declared as precious and thus reserved for the end-user. +# +# This macro should work for all versions of Python >= 2.1.0. As an end +# user, you can disable the check for the python version by setting the +# PYTHON_NOVERSIONCHECK environment variable to something else than the +# empty string. +# +# If you need to use this macro for an older Python version, please +# contact the authors. We're always open for feedback. +# +# LICENSE +# +# Copyright (c) 2009 Sebastian Huber +# Copyright (c) 2009 Alan W. Irwin +# Copyright (c) 2009 Rafael Laboissiere +# Copyright (c) 2009 Andrew Collier +# Copyright (c) 2009 Matteo Settenvini +# Copyright (c) 2009 Horst Knorr +# +# 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 . +# +# As a special exception, the respective Autoconf Macro's copyright owner +# gives unlimited permission to copy, distribute and modify the configure +# scripts that are the output of Autoconf when processing the Macro. You +# need not follow the terms of the GNU General Public License when using +# or distributing such scripts, even though portions of the text of the +# Macro appear in them. The GNU General Public License (GPL) does govern +# all other use of the material that constitutes the Autoconf Macro. +# +# This special exception to the GPL applies to versions of the Autoconf +# Macro released by the Autoconf Archive. When you make and distribute a +# modified version of the Autoconf Macro, you may extend this special +# exception to the GPL to apply to your modified version as well. + +#serial 8 + +AU_ALIAS([AC_PYTHON_DEVEL], [AX_PYTHON_DEVEL]) +AC_DEFUN([AX_PYTHON_DEVEL],[ + # + # Allow the use of a (user set) custom python version + # + AC_ARG_VAR([PYTHON_VERSION],[The installed Python + version to use, for example '2.3'. This string + will be appended to the Python interpreter + canonical name.]) + + AC_PATH_PROG([PYTHON],[python[$PYTHON_VERSION]]) + if test -z "$PYTHON"; then + AC_MSG_ERROR([Cannot find python$PYTHON_VERSION in your system path]) + PYTHON_VERSION="" + fi + + # + # Check for a version of Python >= 2.1.0 + # + AC_MSG_CHECKING([for a version of Python >= '2.1.0']) + ac_supports_python_ver=`$PYTHON -c "import sys; \ + ver = sys.version.split()[[0]]; \ + print (ver >= '2.1.0')"` + if test "$ac_supports_python_ver" != "True"; then + if test -z "$PYTHON_NOVERSIONCHECK"; then + AC_MSG_RESULT([no]) + AC_MSG_FAILURE([ +This version of the AC@&t@_PYTHON_DEVEL macro +doesn't work properly with versions of Python before +2.1.0. You may need to re-run configure, setting the +variables PYTHON_CPPFLAGS, PYTHON_LDFLAGS, PYTHON_SITE_PKG, +PYTHON_EXTRA_LIBS and PYTHON_EXTRA_LDFLAGS by hand. +Moreover, to disable this check, set PYTHON_NOVERSIONCHECK +to something else than an empty string. +]) + else + AC_MSG_RESULT([skip at user request]) + fi + else + AC_MSG_RESULT([yes]) + fi + + # + # if the macro parameter ``version'' is set, honour it + # + if test -n "$1"; then + AC_MSG_CHECKING([for a version of Python $1]) + ac_supports_python_ver=`$PYTHON -c "import sys; ver = sys.version.split()[[0]]; print (ver >= $1)"` + if test "$ac_supports_python_ver" = "True"; then + AC_MSG_RESULT([yes]) + else + AC_MSG_RESULT([no]) + AC_MSG_ERROR([this package requires Python $1. +If you have it installed, but it isn't the default Python +interpreter in your system path, please pass the PYTHON_VERSION +variable to configure. See ``configure --help'' for reference. +]) + PYTHON_VERSION="" + fi + fi + + # + # Check if you have distutils, else fail + # + AC_MSG_CHECKING([for the distutils Python package]) + ac_distutils_result=`$PYTHON -c "import distutils" 2>&1` + if test -z "$ac_distutils_result"; then + AC_MSG_RESULT([yes]) + else + AC_MSG_RESULT([no]) + AC_MSG_ERROR([cannot import Python module "distutils". +Please check your Python installation. The error was: +$ac_distutils_result]) + PYTHON_VERSION="" + fi + + # + # Check for Python include path + # + AC_MSG_CHECKING([for Python include path]) + if test -z "$PYTHON_CPPFLAGS"; then + python_path=`$PYTHON -c "import distutils.sysconfig; \ + print (distutils.sysconfig.get_python_inc ());"` + if test -n "${python_path}"; then + python_path="-I$python_path" + fi + PYTHON_CPPFLAGS=$python_path + fi + AC_MSG_RESULT([$PYTHON_CPPFLAGS]) + AC_SUBST([PYTHON_CPPFLAGS]) + + # + # Check for Python library path + # + AC_MSG_CHECKING([for Python library path]) + if test -z "$PYTHON_LDFLAGS"; then + # (makes two attempts to ensure we've got a version number + # from the interpreter) + ac_python_version=`cat<]], + [[Py_Initialize();]]) + ],[pythonexists=yes],[pythonexists=no]) + AC_LANG_POP([C]) + # turn back to default flags + CPPFLAGS="$ac_save_CPPFLAGS" + LIBS="$ac_save_LIBS" + + AC_MSG_RESULT([$pythonexists]) + + if test ! "x$pythonexists" = "xyes"; then + AC_MSG_FAILURE([ + Could not link test program to Python. Maybe the main Python library has been + installed in some non-standard library path. If so, pass it to configure, + via the LDFLAGS environment variable. + Example: ./configure LDFLAGS="-L/usr/non-standard-path/python/lib" + ============================================================================ + ERROR! + You probably have to install the development version of the Python package + for your distribution. The exact name of this package varies among them. + ============================================================================ + ]) + PYTHON_VERSION="" + fi + + # + # all done! + # +]) + diff --git a/m4/ax_python_module.m4 b/m4/ax_python_module.m4 new file mode 100644 index 0000000..bd70a06 --- /dev/null +++ b/m4/ax_python_module.m4 @@ -0,0 +1,49 @@ +# =========================================================================== +# http://www.gnu.org/software/autoconf-archive/ax_python_module.html +# =========================================================================== +# +# SYNOPSIS +# +# AX_PYTHON_MODULE(modname[, fatal]) +# +# DESCRIPTION +# +# Checks for Python module. +# +# If fatal is non-empty then absence of a module will trigger an error. +# +# LICENSE +# +# Copyright (c) 2008 Andrew Collier +# +# Copying and distribution of this file, with or without modification, are +# permitted in any medium without royalty provided the copyright notice +# and this notice are preserved. This file is offered as-is, without any +# warranty. + +#serial 5 + +AU_ALIAS([AC_PYTHON_MODULE], [AX_PYTHON_MODULE]) +AC_DEFUN([AX_PYTHON_MODULE],[ + if test -z $PYTHON; + then + PYTHON="python" + fi + PYTHON_NAME=`basename $PYTHON` + AC_MSG_CHECKING($PYTHON_NAME module: $1) + $PYTHON -c "import $1" 2>/dev/null + if test $? -eq 0; + then + AC_MSG_RESULT(yes) + eval AS_TR_CPP(HAVE_PYMOD_$1)=yes + else + AC_MSG_RESULT(no) + eval AS_TR_CPP(HAVE_PYMOD_$1)=no + # + if test -n "$2" + then + AC_MSG_ERROR(failed to find required module $1) + exit 1 + fi + fi +]) diff --git a/man/Makefile.am b/man/Makefile.am new file mode 100644 index 0000000..830583d --- /dev/null +++ b/man/Makefile.am @@ -0,0 +1,14 @@ +if ENABLE_SYSTEMTRAY + MAYBE_SYSTEMTRAY = g15-systemtray.1 +endif + +if ENABLE_INDICATOR + MAYBE_INDICATOR = g15-indicator.1 +endif + +if ENABLE_DRIVER_KERNEL + MAYBE_SYSTEM_SERVICE = g15-system-service.1 +endif + +man1_MANS = g15-desktop-service.1 g15-config.1 $(MAYBE_SYSTEMTRAY) $(MAYBE_INDICATOR) $(MAYBE_SYSTEM_SERVICE) +EXTRA_DIST = g15-desktop-service.1 g15-config.1 g15-systemtray.1 g15-indicator.1 g15-system-service.1 \ No newline at end of file diff --git a/man/g15-config.1 b/man/g15-config.1 new file mode 100644 index 0000000..f26d4dd --- /dev/null +++ b/man/g15-config.1 @@ -0,0 +1,33 @@ +.\" Process this file with +.\" groff -man -Tascii g15-config.1 +.\" +.TH g15-config 1 +.SH NAME +g15-config \- Configuration tool for Logitech G keyboards +.SH SYNOPSIS +.B g15-config [-c] +.SH DESCRIPTION +.B g15-config +Starts the +.B Gnome15 +Logitech G keyboard configuration tool. +It allows you to configure things such as the keyboard +backlight colour or level, screen cycling options and which +plugins are enabled. + +If the desktop service is not running, this tool will detect +that and offer advice as to what to do. + +The first time +.B g15-config +is used, you will be prompted for which driver to use. +.SH OPTIONS +.IP -c +Force the first-run configuration dialog to appear, allowing +choice of which driver to use (additional packages may be required +for your hardware). +.SH AUTHOR +Brett Smith +.SH "SEE ALSO" +.BR g15-indicator (1), +.BR g15-macros (1) \ No newline at end of file diff --git a/man/g15-desktop-service.1 b/man/g15-desktop-service.1 new file mode 100644 index 0000000..c173b61 --- /dev/null +++ b/man/g15-desktop-service.1 @@ -0,0 +1,44 @@ +.\" Process this file with +.\" groff -man -Tascii g15-desktop-service.1 +.\" +.TH g15-desktop-service 1 +.SH NAME +g15-desktop-service \- Starts Gnome15 Desktop service +.SH SYNOPSIS +.B g15-desktop-service [-c] +.SH DESCRIPTION +.B g15-desktop-service +Gnome15 is a suite of applications and plugins that provide +integration of the Logitech G series keyboards into the +GNOME desktop environment. + +This service is responsible for connecting to the underlying +service appropriate for the hardware (i.e. g15daemon or g19d), +managing and running the plugins, displaying the plugin's +output on the LCD, delivering key events to X when macro +keys are activated. + +It also provides the DBUS service used by various clients +such as the Panel Applet, Indicator or System Tray Icon, +or other external examples that want to draw on the +LCD. + +The first time Gnome15 is used, you will be prompted for +which driver to use (additional packages may be required +for your hardware). +.SH OPTIONS +.IP "-l DEBUG|INFO|WARNING|ERROR" +Set the log level. +.IP -f +Force the service to run in the foreground. +.IP start +Starts the service. +.IP stop +Stops the service. +.SH AUTHOR +Brett Smith +.SH "SEE ALSO" +.BR g15-config (1), +.BR g15-indicator (1) +.BR g15-systemtray (1) +.BR g15-system-service (1) \ No newline at end of file diff --git a/man/g15-indicator.1 b/man/g15-indicator.1 new file mode 100644 index 0000000..176894f --- /dev/null +++ b/man/g15-indicator.1 @@ -0,0 +1,27 @@ +.\" Process this file with +.\" groff -man -Tascii g15-indicator.1 +.\" +.TH g15-indicator 1 +.SH NAME +g15-indicator \- Starts an indicator for Gnome15 desktop service +.SH SYNOPSIS +.B g15-indicator +.SH DESCRIPTION +.B g15-indicator +Starts the Gnome15 +.I Indicator Applet +that may be used to quickly access configuration, select +the currently active screen, and start the +.B g15-desktop-service +when required. + +Gnome15 is a suite of applications and plugins that provide +integration of the Logitech G series keyboards into the +GNOME desktop environment. +.SH AUTHOR +Brett Smith +.SH "SEE ALSO" +.BR g15-config (1), +.BR g15-desktop-service (1), +.BR g15-systemtray (1) +.BR g15-system-service (1) \ No newline at end of file diff --git a/man/g15-system-service.1 b/man/g15-system-service.1 new file mode 100644 index 0000000..c1efb4a --- /dev/null +++ b/man/g15-system-service.1 @@ -0,0 +1,35 @@ +.\" Process this file with +.\" groff -man -Tascii g15-desktop-service.1 +.\" +.TH g15-system-service 1 +.SH NAME +g15-system-service \- Starts Gnome15 System service +.SH SYNOPSIS +.B g15-system-service +.SH DESCRIPTION +.B g15-system-service +Gnome15 is a suite of applications and plugins that provide +integration of the Logitech G series keyboards into the +GNOME desktop environment. + +This service is responsible for writing values to the +various LED device files exposed by the LG4L kernel drivers. + +Ordinarily, you should never need to start this service yourself, as +it will be started the first time it is needed. +.SH OPTIONS +.IP "-l DEBUG|INFO|WARNING|ERROR" +Set the log level. +.IP -f +Force the service to run in the foreground. +.IP start +Starts the service. +.IP stop +Stops the service. +.SH AUTHOR +Brett Smith +.SH "SEE ALSO" +.BR g15-config (1), +.BR g15-indicator (1) +.BR g15-systemtray (1) +.BR g15-desktop-service (1) \ No newline at end of file diff --git a/man/g15-systemtray.1 b/man/g15-systemtray.1 new file mode 100644 index 0000000..b9feaa9 --- /dev/null +++ b/man/g15-systemtray.1 @@ -0,0 +1,28 @@ +.\" Process this file with +.\" groff -man -Tascii g15-indicator.1 +.\" +.TH g15-systemtray 1 +.SH NAME +g15-systemtray \- Adds an icon in the notification area allowing control of Gnome15 +.SH SYNOPSIS +.B g15-systemtray +.SH DESCRIPTION +.B g15-systemtray +Starts the Gnome15 +.I System Tray +that may be used to quickly access configuration, select +the currently active screen, and start the +.B g15-desktop-service +when required. + +Gnome15 is a suite of applications and plugins that provide +integration of the Logitech G series keyboards into the +GNOME desktop environment. + +.SH AUTHOR +Brett Smith +.SH "SEE ALSO" +.BR g15-config (1), +.BR g15-desktop-service (1), +.BR g15-system-service (1) +.BR g15-indicator (1) \ No newline at end of file diff --git a/mksvgheaders.py b/mksvgheaders.py new file mode 100755 index 0000000..bc5ef8f --- /dev/null +++ b/mksvgheaders.py @@ -0,0 +1,64 @@ +#!/usr/bin/env python2 + +# +-----------------------------------------------------------------------------+ +# | GPL | +# +-----------------------------------------------------------------------------+ +# | Copyright (c) Brett Smith | +# | | +# | This program is free software; you can redistribute it and/or | +# | modify it under the terms of the GNU General Public License | +# | as published by the Free Software Foundation; either version 2 | +# | of the License, or (at your option) any later version. | +# | | +# | This program is distributed in the hope that it will be useful, | +# | but WITHOUT ANY WARRANTY; without even the implied warranty of | +# | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | +# | GNU General Public License for more details. | +# | | +# | You should have received a copy of the GNU General Public License | +# | along with this program; if not, write to the Free Software | +# | Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. | +# +-----------------------------------------------------------------------------+ + +from lxml import etree + +# Logging +import logging +logging.basicConfig(format='%(levelname)s:%(asctime)s:%(threadName)s:%(name)s:%(message)s', datefmt='%H:%M:%S') +logger = logging.getLogger() + +LEVELS = {'debug': logging.DEBUG, + 'info': logging.INFO, + 'warning': logging.WARNING, + 'error': logging.ERROR, + 'critical': logging.CRITICAL} + +nsmap = { + 'sodipodi': 'http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd', + 'cc': 'http://web.resource.org/cc/', + 'svg': 'http://www.w3.org/2000/svg', + 'dc': 'http://purl.org/dc/elements/1.1/', + 'xlink': 'http://www.w3.org/1999/xlink', + 'rdf': 'http://www.w3.org/1999/02/22-rdf-syntax-ns#', + 'inkscape': 'http://www.inkscape.org/namespaces/inkscape', + } + +if __name__ == "__main__": + import optparse + parser = optparse.OptionParser() + parser.add_option("-l", "--log", dest="log_level", metavar="INFO,DEBUG,WARNING,ERROR,CRITICAL", + default="warning" , help="Log level") + (options, args) = parser.parse_args() + + level = logging.NOTSET + if options.log_level != None: + level = LEVELS.get(options.log_level.lower(), logging.NOTSET) + logger.setLevel(level = level) + + for f in args: + document = etree.parse(f) + root = document.getroot() + for text in root.xpath('//text()',namespaces=nsmap): + text = str(text).strip() + if len(text) > 0 and text.startswith("_("): + print "char *s = N_(\"%s\");" % text[2:-1] diff --git a/src/Makefile.am b/src/Makefile.am new file mode 100644 index 0000000..1f595a8 --- /dev/null +++ b/src/Makefile.am @@ -0,0 +1,94 @@ +SUBDIRS = scripts plugins gnome15 + +if ENABLE_DRIVER_G19DIRECT +SUBDIRS += pylibg19 +endif + +if ENABLE_PLUGIN_IMPULSE15 +SUBDIRS += libimpulse +endif + +if ENABLE_GNOME_SHELL_EXTENSION +SUBDIRS += gnome-shell-extension +endif + +all-local: + for PLUGIN in `ls plugins`; do \ + PLUGIN_DIR=plugins/$$PLUGIN; \ + if [ -d $$PLUGIN_DIR ]; then \ + pushd $$PLUGIN_DIR; \ + for M_LOCALE in @ENABLED_LOCALES@; do \ + mkdir -p i18n/$$M_LOCALE/LC_MESSAGES ; \ + if [ `ls i18n/*.po 2>/dev/null|wc -l` -gt 0 ]; then \ + for M_PO in i18n/*.po; do \ + BN=`basename $$M_PO .po`; \ + LL=`basename $$BN .$$M_LOCALE`.mo; \ + echo "$$M_PO -> $$LL"; \ + msgfmt $$M_PO --output-file i18n/$$M_LOCALE/LC_MESSAGES/$$LL; \ + done; \ + fi; \ + done; \ + for THEME_DIR in *; do \ + if [ -d $$THEME_DIR -a -d $$THEME_DIR/i18n ]; then \ + pushd $$THEME_DIR; \ + mkdir -p i18n/$$M_LOCALE/LC_MESSAGES ; \ + if [ `ls i18n/*.po 2>/dev/null|wc -l` -gt 0 ]; then \ + for M_PO in i18n/*.po; do \ + BN=`basename $$M_PO .po`; \ + LL=`basename $$BN .$$M_LOCALE`.mo; \ + echo "$$M_PO -> $$LL"; \ + msgfmt $$M_PO --output-file i18n/$$M_LOCALE/LC_MESSAGES/$$LL; \ + done; \ + fi; \ + popd; \ + fi; \ + done; \ + popd; \ + fi; \ + done; + +clean-local: + find . -name '*.pyc' -exec rm {} \; ; \ + find . -name '*.pyo' -exec rm {} \; ; \ + for PLUGIN in `ls plugins`; do \ + PLUGIN_DIR=plugins/$$PLUGIN; \ + if [ -d $$PLUGIN_DIR ]; then \ + pushd $$PLUGIN_DIR; \ + for M_LOCALE in @ENABLED_LOCALES@; do \ + if [ -d i18n/$$M_LOCALE ]; then \ + rm -fr i18n/$$M_LOCALE; \ + fi; \ + for THEME_DIR in *; do \ + if [ -d $$THEME_DIR/i18n/$$M_LOCALE ]; then \ + pushd $$THEME_DIR; \ + rm -fr i18n/$$M_LOCALE; \ + popd; \ + fi; \ + done; \ + done; \ + popd; \ + fi; \ + done; + +install-exec-hook: + for PLUGIN in `ls plugins`; do \ + PLUGIN_DIR=plugins/$$PLUGIN; \ + if [ -d $(DESTDIR)$(datadir)/gnome15/plugins/$$PLUGIN ]; then \ + pushd $$PLUGIN_DIR; \ + for M_LOCALE in @ENABLED_LOCALES@; do \ + mkdir -p $(DESTDIR)$(datadir)/gnome15/plugins/$$PLUGIN/i18n; \ + cp -pR i18n/$$M_LOCALE $(DESTDIR)$(datadir)/gnome15/plugins/$$PLUGIN/i18n; \ + done; \ + for THEME_DIR in *; do \ + if [ -d $$THEME_DIR -a -d $$THEME_DIR/i18n ]; then \ + pushd $$THEME_DIR; \ + for M_LOCALE in @ENABLED_LOCALES@; do \ + mkdir -p $(DESTDIR)$(datadir)/gnome15/plugins/$$PLUGIN/$$THEME_DIR/i18n; \ + cp -pR i18n/$$M_LOCALE $(DESTDIR)$(datadir)/gnome15/plugins/$$PLUGIN/$$THEME_DIR/i18n; \ + done; \ + popd; \ + fi; \ + done; \ + popd; \ + fi; \ + done; diff --git a/src/gamewrap/gamewrap b/src/gamewrap/gamewrap new file mode 100755 index 0000000..d719b5b --- /dev/null +++ b/src/gamewrap/gamewrap @@ -0,0 +1,101 @@ +#!/usr/bin/env python2 + +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2011 Brett Smith +# +# 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 . + +""" +Wrapper to launch games (and other applications), monitor their output for +patterns and send events to interested DBUS clients +""" + + +import sys +import os +import glib + +# Logging +import logging +logging.basicConfig(format='%(threadName)s:%(name)s:%(message)s') +logger = logging.getLogger() + +LEVELS = {'debug': logging.DEBUG, + 'info': logging.INFO, + 'warning': logging.WARNING, + 'error': logging.ERROR, + 'critical': logging.CRITICAL} + +import gobject +gobject.threads_init() + +# DBUS - Use to check current desktop service status or stop it +import dbus +from dbus.mainloop.glib import DBusGMainLoop +from dbus.mainloop.glib import threads_init +threads_init() +DBusGMainLoop(set_as_default=True) + +# Server host class + +def check_service_status(system_dbus): + try : + system_bus.get_object('org.gnome15.GameWrap', '/org/gnome15/GameWrap').GetServerInformation() + return True + except Exception as e: + logger.debug("D-Bus service not available.", exc_info = e) + return False + +def start_service(args, bus, no_trap=False,): + try : + import setproctitle + setproctitle.setproctitle(os.path.basename(os.path.abspath(sys.argv[0]))) + except ImportError as ie: + # Not a big issue + logger.debug("No setproctitle, process will be named 'python'", exc_info = ie) + + # Start the loop + try : + import gw + service = gw.G15GameWrapperServiceController(args, bus, no_trap=no_trap) + service.start_loop() + except dbus.exceptions.NameExistsException as nee: + logger.debug("D-Bus service already running", exc_info = nee) + print "GameWrap service is already running" + sys.exit(1) + +if __name__ == "__main__": + """ + Allow arguments to be passed to gamewrap itself. If the first command line + argument begins with - or --, then gamewrap options follow until the first + argument that doesn't start with - or -- + """ + import optparse + parser = optparse.OptionParser() + parser.add_option("-l", "--log", dest="log_level", metavar="INFO,DEBUG,WARNING,ERROR,CRITICAL", + default="warning" , help="Log level") + parser.add_option("-n", "--notrap", action="store_true", dest="no_trap", + default=False, help="Do not try to trap signals.") + (options, args) = parser.parse_args() + + level = logging.NOTSET + if options.log_level != None: + level = LEVELS.get(options.log_level.lower(), logging.NOTSET) + logger.setLevel(level = level) + + the_bus = dbus.SessionBus() + if check_service_status(the_bus): + print "GameWrap service already running" + else: + start_service(args, the_bus, options.no_trap) diff --git a/src/gamewrap/gw/__init__.py b/src/gamewrap/gw/__init__.py new file mode 100644 index 0000000..9e3d2fa --- /dev/null +++ b/src/gamewrap/gw/__init__.py @@ -0,0 +1,143 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2011 Brett Smith +# +# 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 . + +import gobject +import subprocess +import signal +import sys +import dbus.service +import threading +import re + +# Logging +import logging +logger = logging.getLogger(__name__) + +NAME = "GameWrap" +VERSION = "0.1" +BUS_NAME = "org.gnome15.GameWrap" +OBJECT_PATH = "/org/gnome15/GameWrap" +IF_NAME = "org.gnome15.GameWrap" + +class RunThread(threading.Thread): + + def __init__(self, controller): + threading.Thread.__init__(self, name = "ExecCommand") + self.controller = controller + + def run(self): + logger.info("Running '%s'", str(self.controller.args)) + self.process = subprocess.Popen(self.controller.args, stdout = subprocess.PIPE, stderr = subprocess.STDOUT) + logger.info("Process started OK") + while True: + line = self.process.stdout.readline(1024) + if line: + logger.info(">%s<", line) + for pattern_id in self.controller.patterns: + pattern = self.controller.patterns[pattern_id] + match = re.search(pattern, line) + if match: + logger.info("Match! %s", str(match)) + gobject.idle_add(self.controller.PatternMatched(patter_id, line)) + else: + break + logger.info("Waiting for process to complete") + self.controller.status = self.process.wait() + logger.info("Process complete with %s", self.controller.status) + self.controller.Stop() + +class G15GameWrapperServiceController(dbus.service.Object): + + def __init__(self, args, bus, no_trap=False): + bus_name = dbus.service.BusName(BUS_NAME, bus=bus, replace_existing=False, allow_replacement=False, do_not_queue=True) + dbus.service.Object.__init__(self, None, OBJECT_PATH, bus_name) + + self._page_sequence_number = 1 + self._bus = bus + self.args = args + self.status = 0 + self.patterns = {} + + logger.info("Exposing service for '%s'. Wait for signal to wait", str(args)) + + if not no_trap: + signal.signal(signal.SIGINT, self.sigint_handler) + signal.signal(signal.SIGTERM, self.sigterm_handler) + + self._loop = gobject.MainLoop() + + def start_loop(self): + logger.info("Starting GLib loop") + self._loop.run() + logger.debug("Exited GLib loop") + + def sigint_handler(self, signum, frame): + logger.info("Got SIGINT signal, shutting down") + self.shutdown() + + def sigterm_handler(self, signum, frame): + logger.info("Got SIGTERM signal, shutting down") + self.shutdown() + + """ + DBUS API + """ + @dbus.service.method(IF_NAME) + def Start(self): + RunThread(self).start() + + @dbus.service.method(IF_NAME) + def Stop(self): + gobject.idle_add(self._shutdown()) + + @dbus.service.method(IF_NAME, in_signature='ss') + def AddPattern(self, pattern_id, pattern): + logger.info("Adding pattern '%s' with id '%s'", pattern, pattern_id) + if pattern_id in self.patterns: + raise Exception("Pattern with ID %s already registered." % pattern_id) + self.patterns[pattern_id] = pattern + + @dbus.service.method(IF_NAME, in_signature='s') + def RemovePattern(self, pattern_id): + logger.info("Removing pattern with id '%s'", pattern_id) + if not pattern_id in self.patterns: + raise Exception("Pattern with ID %s not registered." % pattern_id) + del self.patterns[id] + + @dbus.service.method(IF_NAME, in_signature='', out_signature='ssssas') + def GetInformation(self): + return ("GameWrapper Service", "Gnome15 Project", VERSION, "1.0", self.args) + + + """ + Signals + """ + + """ + DBUS Signals + """ + @dbus.service.signal(SCREEN_IF_NAME, signature='ss') + def PatternMatch(self, pattern_id, line): + pass + + """ + Private + """ + + def _shutdown(self): + logger.info("Shutting down") + self._loop.quit() + sys.exit(self.status) \ No newline at end of file diff --git a/src/gamewrap/gw/wraplet.py b/src/gamewrap/gw/wraplet.py new file mode 100644 index 0000000..f219abf --- /dev/null +++ b/src/gamewrap/gw/wraplet.py @@ -0,0 +1,31 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2011 Brett Smith +# +# 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 . + +import shlex + +class Wraplet(): + + def __init__(self, filename): + self.filename = filename + + fd = open(filename, "r") + try: + lexr = shlex.shlex() + lexr.wordchars = "._" + finally: + fd.close() + + \ No newline at end of file diff --git a/src/gamewrap/ut2004.wlet b/src/gamewrap/ut2004.wlet new file mode 100644 index 0000000..8b0b9a8 --- /dev/null +++ b/src/gamewrap/ut2004.wlet @@ -0,0 +1,58 @@ +# GameWrap config for Unreal Tourname 2004 + +# activate defines a list of patterns to match against the command line +# arguments. When a game is launched with parameters that match these, +# this config file will be activated. + +activate "ut2004", ".*/ut2004" + +# +# Map the internal terminology to the game terminology +# + +terminology { + energy: "Health", + game: "Round", + set: "Game", + match: "Tournament", + shield: "Shield", + boost: "Adrenalin" +} + +# +# Defines which patterns will activate which events +# + +events { + # Game + new-game: "NewGame", + lost-game: "ASGameInfo::EndRound", + won-game: "ASGameInfo::EndRound", + game-over: "ASGameInfo::EndRound", + + # Round + new-round: "NewRound", + lost-round: "LostRound", + won-round: "LostRound", + round-over: "LostRound", + + # Lives + life-lost: "Lifelost", + life-gained: "newlist", + + # Energy + energy-low: "Lifelost", + energy-boost: "newlist", + energy-max: "max-energy", + energy-level: "energy-level", + + # Shield + shield-full: "", + shield-empty: "", + shield-level: "", + + # Boost + boost-available: "", + boost-empty: "", + +} \ No newline at end of file diff --git a/src/gnome-shell-extension/Makefile.am b/src/gnome-shell-extension/Makefile.am new file mode 100644 index 0000000..0d627fe --- /dev/null +++ b/src/gnome-shell-extension/Makefile.am @@ -0,0 +1,9 @@ +SUBDIRS = icons + +extensiondir = $(datadir)/gnome-shell/extensions/gnome15-shell-extension@gnome15.org +extension_DATA = extension.js \ + metadata.json \ + stylesheet.css + +EXTRA_DIST = \ + $(extension_DATA) diff --git a/src/gnome-shell-extension/extension.js b/src/gnome-shell-extension/extension.js new file mode 100644 index 0000000..fa8dd8d --- /dev/null +++ b/src/gnome-shell-extension/extension.js @@ -0,0 +1,750 @@ +// Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +// Copyright (C) 2012 Brett Smith +// Copyright (C) 2013 Brett Smith +// Nuno Araujo +// +// 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 . + +/* + * Gnome15 - The Logitech Keyboard Manager for Linux + * + * This GNOME Shell extension allows control of all supported and connected + * Logitech devices from the shell's top panel. A menu button is added for + * each device, providing options to enable/disable the device, enable/disable + * screen cycling, and make a particular page the currently visible one. + */ + +const St = imports.gi.St; +const Main = imports.ui.main; +const Tweener = imports.ui.tweener; +const Lang = imports.lang; +const PanelMenu = imports.ui.panelMenu; +const PopupMenu = imports.ui.popupMenu; +const Gio = imports.gi.Gio; +const GLib = imports.gi.GLib; +const Clutter = imports.gi.Clutter; +const Config = imports.misc.config; + +let currNotification, gnome15Service, devices, dbus_watch_id; + +/* + * Remote object definitions. This is just a sub-set of the full API + * available, just enough to do the job + */ + +const Gnome15ServiceInterface = + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +const Gnome15ScreenInterface = + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +/* No idea why, but Cycle and CycleKeyboard signatures argument type are actually 'n', + * but this causes an exception when calling with JavaScript integer. + */ + +const Gnome15DeviceInterface = + + + + + + + + + + + + + + + + + + + + + + +const Gnome15PageInterface = + + + + + + + + + +/** + * Instances of this class are responsible for managing a single device. + * A single button is created and added to the top panel, various attributes + * about the device attached are read, and if the device currently has + * a screen (i.e. is enabled), the initial page list is loaded. + * + * Signals are also setup to watch for the screen being enable / disabled + * externally. + */ +const DeviceItem = new Lang.Class({ + Name: 'DeviceItem', + _init : function(key) { + this.parent(); + this._buttonSignals = new Array(); + let gnome15Device = _createDevice(key); + gnome15Device.GetModelFullNameRemote(Lang.bind(this, function(result) { + let [modelFullName] = result; + gnome15Device.GetModelIdRemote(Lang.bind(this, function(result) { + let [uid] = result; + gnome15Device.GetScreenRemote(Lang.bind(this, function(result) { + let [screen] = result; + gnome15Device.connectSignal("ScreenAdded", Lang.bind(this, function(src, senderName, args) { + let [screenPath] = args; + _log("Screen added " + screenPath); + this._getPages(screenPath); + })); + gnome15Device.connectSignal("ScreenRemoved", Lang.bind(this, function(src, senderName, args) { + let [screenPath] = args; + _log("Screen removed " + screenPath); + this._cleanUp(); + this._gnome15Button.clearPages(); + })); + this._addButton(key, modelFullName, uid, screen); + })); + })); + })); + }, + + _addButton: function(key, modelFullName, modelId, screen) { + let hasScreen = screen != null && screen.length > 0; + this._gnome15Button = new DeviceButton(key, modelId, modelFullName, hasScreen); + + if(Config.PACKAGE_VERSION.indexOf("3.4") == 0) { + Main.panel._rightBox.insert_child_at_index(this._gnome15Button.actor, 1); + Main.panel._rightBox.child_set(this._gnome15Button.actor, { + y_fill : true + }); + Main.panel._menus.addMenu(this._gnome15Button.menu); + } + else if(Config.PACKAGE_VERSION.indexOf("3.10") == 0) { + Main.panel.addToStatusArea('gnome15-' + modelId, this._gnome15Button); + } + else { + Main.panel.addToStatusArea('gnome15-' + modelId, this._gnome15Button); + Main.panel.menuManager.addMenu(this._gnome15Button.menu); + } + + if(hasScreen) { + /* If this device already has a screen (i.e. is enabled, load the + * pages now). Otherwise, we wait for ScreenAdded to come in + * in extension itself + */ + this._getPages(screen); + } + else { + this._gnome15Button.reset(); + } + }, + + /** + * Removes the signals that are being watched for this device and + * mark the button so that the enabled switch is turned off when + * the menu is reset + */ + _cleanUp: function() { + if(this._gnome15Button._screen != null) { + for(let key in this._buttonSignals) { + this._gnome15Button._screen.disconnectSignal(this._buttonSignals[key]); + } + this._buttonSignals.splice(0, this._buttonSignals.length); + this._gnome15Button._screen = null; + } + }, + + /** + * Callback that receives the full list of pages currently showing on + * this device and adds them to the button. It then starts watching for + * new pages appearing, or pages being deleted and acts accordingly. + */ + _getPages: function(screen) { + this._cleanUp(); + this._gnome15Button._screen = _createScreen(screen); + this._gnome15Button._screen.GetPagesRemote(Lang.bind(this, function(result) { + let [pages] = result; + this._gnome15Button.clearPages(); + for(let key in pages) { + this._gnome15Button.addPage(pages[key]); + } + this._gnome15Button._screen.IsCyclingEnabledRemote(Lang.bind(this, function(result) { + let [cyclingEnabled] = result; + this._gnome15Button.setCyclingEnabled(cyclingEnabled); + this._buttonSignals.push(this._gnome15Button._screen.connectSignal("PageCreated", Lang.bind(this, function(src, senderName, args) { + let pagePath = args[0]; + this._gnome15Button.addPage(pagePath); + }))); + this._buttonSignals.push(this._gnome15Button._screen.connectSignal("PageDeleting", Lang.bind(this, function(src, senderName, args) { + let pagePath = args[0]; + this._gnome15Button.deletePage(pagePath); + }))); + this._buttonSignals.push(this._gnome15Button._screen.connectSignal("CyclingChanged", Lang.bind(this, function(src, senderName, args) { + let [cycle] = args; + this._gnome15Button.setCyclingEnabled(cycle); + }))); + })); + })); + + + }, + + /** + * Called as a result of the service disappearing or the extension being + * disabled. The button is removed from the top panel. + */ + close : function(pages) { + this._gnome15Button.destroy(); + } +}); + +/** + * A switch menu item that allows a single device to be enabled or disabled. + */ +const EnableDisableMenuItem = new Lang.Class({ + Name: 'EnableDisableMenuItem', + Extends: PopupMenu.PopupSwitchMenuItem, + + _init : function(devicePath, modelName, screen) { + this.parent(modelName); + this.setToggleState(screen != null); + this.connect('toggled', Lang.bind(this, function() { + if(this.state) { + _createDevice(devicePath).EnableRemote(); + } + else { + _createDevice(devicePath).DisableRemote(); + } + })); + }, + + activate : function(event) { + this.parent(event); + }, +}); + +/** + * A switch menu item that allows automatic page cycling to be enabled or + * disabled. + */ +const CyclePagesMenuItem = new Lang.Class({ + Name: 'CyclePagesMenuItem', + Extends: PopupMenu.PopupSwitchMenuItem, + + _init : function(selected, screen) { + this.parent("Cycle pages automatically"); + this.setToggleState(selected); + this._screen = screen; + }, + + activate : function(event) { + this._screen.SetCyclingEnabledRemote(!this.state); + this.parent(event); + }, +}); + +/** + * A menu item that represents a single page on a single device. Activating + * this item causes the page to be displayed. + */ +const PageMenuItem = new Lang.Class({ + Name: 'PageMenuItem', + Extends: PopupMenu.PopupBaseMenuItem, + + _init : function(lblText, lblId, page_proxy) { + this.parent(); + this.label = new St.Label({ + text : lblText + }); + if(Config.PACKAGE_VERSION.indexOf("3.10") == 0) { + this.actor.add_child(this.label); + } + else { + this.addActor(this.label); + } + this._pageProxy = page_proxy; + this._text = lblText; + this._idTxt = lblId; + }, + + activate : function(event) { + this._pageProxy.CycleToRemote(); + this.parent(event); + }, +}); + +/** + * A menu item that that activates g15-config. It will open with provided + * device UID open (via the -d option of g15-config). + */ +const PreferencesMenuItem = new Lang.Class({ + Name: 'PreferencesMenuItem', + Extends: PopupMenu.PopupMenuItem, + + _init : function(deviceUid) { + this.parent("Configuration"); + this._deviceUid = deviceUid + }, + + activate : function(event) { + GLib.spawn_command_line_async('g15-config -d ' + this._deviceUid); + this.parent(event); + }, +}); + +/** + * Shell top panel button that represents a single Gnome15 device. + */ +const DeviceButton = new Lang.Class({ + Name: 'DeviceButton', + Extends: Config.PACKAGE_VERSION.indexOf("3.10") == 0 ? PanelMenu.Button : PanelMenu.SystemStatusButton, + NUMBER_OF_FIXED_MENU_ITEMS: 4, + + _init : function(devicePath, modelId, modelName) { + this._deviceUid = devicePath.substring(devicePath.lastIndexOf('/') + 1); + this._itemMap = {}; + + if(Config.PACKAGE_VERSION.indexOf("3.4") == 0) { + this.parent('logitech-' + modelId); + } + else if(Config.PACKAGE_VERSION.indexOf("3.10") == 0) { + this.parent(0.0, 'logitech-' + modelId + '-symbolic'); + } + else { + this.parent('logitech-' + modelId + '-symbolic'); + } + + this._cyclingEnabled = false; + this._devicePath = devicePath; + this._itemList = new Array(); + this._modelId = modelId; + this._modelName = modelName; + this._screen = null; + if(Config.PACKAGE_VERSION.indexOf("3.4") == 0) { + this._iconActor.add_style_class_name('device-icon'); + this._iconActor.set_icon_size(20); + this._iconActor.add_style_class_name('device-button'); + } + else if (Config.PACKAGE_VERSION.indexOf("3.10") == 0) { + this._icon = new St.Icon({ + icon_name: 'logitech-' + modelId + '-symbolic', + style_class: 'device-icon', + reactive: true, + track_hover: true + }); + this._icon.set_icon_size(20); + this._icon.add_style_class_name('device-button'); + this.actor.add_actor(this._icon); + } + else { + this.mainIcon.add_style_class_name('device-icon'); + this.mainIcon.set_icon_size(20); + this.mainIcon.add_style_class_name('device-button'); + } + + // Mouse whell events + this.actor.connect('scroll-event', Lang.bind(this, this._onScrollEvent)); + }, + + /** + * Set whether cycling is enabled for this device + * + * @param cycle enable cycling + */ + setCyclingEnabled: function(cycle) { + this._cyclingEnabled = cycle; + this.reset(); + }, + + /** + * Remove the menu item for a page given it's path. + * + * @param pagePath path of page + */ + deletePage: function(pagePath) { + let idx = this._itemList.indexOf(pagePath); + if(idx != -1) { + this._itemList.splice(idx, 1); + this._itemMap[pagePath].destroy(); + delete this._itemMap[pagePath]; + } + }, + + /** + * Clear all pages from this menu. + */ + clearPages : function() { + this._itemList = new Array(); + this.reset(); + }, + + /** + * Add a new page to the menu given it's path. + * + * @param pagePath page of page to add + */ + addPage : function(pagePath) { + this._addPage(pagePath, true); + }, + + /** + * Rebuild the entire menu. + */ + reset : function() { + this.menu.removeAll(); + this.menu.addMenuItem(new EnableDisableMenuItem(this._devicePath, this._modelName, this._screen)); + this.menu.addMenuItem(new CyclePagesMenuItem(this._cyclingEnabled, this._screen)); + this.menu.addMenuItem(new PreferencesMenuItem(this._deviceUid)); + this.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem()); + for (let key in this._itemList) { + this._addPage(this._itemList[key]); + } + }, + + /** + * Add the menu items for a single page given it's page. Various attributes + * about the page are read via dbus and the menu item constructed and + * added to the menu component. + * + * @param pagePath page of page. + * @param insertPagePathInItemList flag that specifies if the pagePath should be inserted in + * the _itemList. Should be set to true when adding a page + * for the first time. + */ + _addPage : function(pagePath, insertPagePathInItemList) { + let Gnome15PageProxy = Gio.DBusProxy.makeProxyWrapper(Gnome15PageInterface); + let pageProxy = new Gnome15PageProxy(Gio.DBus.session, 'org.gnome15.Gnome15', pagePath); + pageProxy.GetTitleRemote(Lang.bind(this, function(result) { + let [title] = result; + let item = new PageMenuItem(title, title, pageProxy); + let position = this._findMenuPositionFor(item); + if(insertPagePathInItemList == true) + this._itemList.splice(position, 0, pagePath); + this._itemMap[pagePath] = item; + this.menu.addMenuItem(item, position + this.NUMBER_OF_FIXED_MENU_ITEMS); + })); + }, + + /** + * Find the position where a given menu item must be inserted in the menu so that + * all the items are alphabetically ordered. + * + * @param item item that will be inserted in the menu + */ + _findMenuPositionFor : function(item) { + let i = 0; + for(let key in this._itemList) { + let pagePath = this._itemList[key]; + if(this._itemMap[pagePath]._text > item._text) + return i; + i++; + } + return this._itemList.length; + }, + + /** + * Handle mouse wheel events by cycling pages. + * + * @param actor source of event + * @param event event + */ + _onScrollEvent: function(actor, event) { + let direction = event.get_scroll_direction(); + if(this._screen != null) { + if (direction == Clutter.ScrollDirection.DOWN) { + this._screen.CycleRemote(-1); + } + else if (direction == Clutter.ScrollDirection.UP) { + this._screen.CycleRemote(1); + } + if (direction == Clutter.ScrollDirection.LEFT) { + this._screen.CycleKeyboardRemote(-1); + } + else if (direction == Clutter.ScrollDirection.RIGHT) { + this._screen.CycleKeyboardRemote(1); + } + } + }, +}); + +/* + * GNOME Shell Extension API functions + */ + +function init() { + _log('Loading Gnome15 Gnome Shell Extension') + devices = {} + let Gnome15ServiceProxy = Gio.DBusProxy.makeProxyWrapper(Gnome15ServiceInterface); + + /* The "Service" is the core of Gnome, so connect to it and watch for some + * signals + */ + gnome15Service = new Gnome15ServiceProxy(Gio.DBus.session, + 'org.gnome15.Gnome15', + '/org/gnome15/Service'); + + gnome15Service.connectSignal("Started", _onDesktopServiceStarted); + gnome15Service.connectSignal("Stopping", _onDesktopServiceStopping); + gnome15Service.connectSignal("DeviceAdded", _deviceAdded); + gnome15Service.connectSignal("DeviceRemoved", _deviceRemoved); +} + +function enable() { + _log('Enabling Gnome15 Gnome Shell Extension') + dbus_watch_id = Gio.bus_watch_name(Gio.BusType.SESSION, + 'org.gnome15.Gnome15', + Gio.BusNameWatcherFlags.NONE, + _onDesktopServiceAppeared, + _onDesktopServiceVanished); + + gnome15Service.IsStartedRemote(_onStarted); +} + +function disable() { + _log('Disabling Gnome15 Gnome Shell Extension') + for(let key in devices) { + _removeDevice(key); + } + Gio.bus_unwatch_name(dbus_watch_id); +} + +/* + * Private functions + */ + +/** + * Callback invoked when the DBus name owner changes (added). We don't actually care + * about this one as we load pages on other signals + */ +function _onDesktopServiceAppeared() { +} + +/** + * Callback invoked when the DBus name owner changes (removed). This occurs + * when the service disappears, even when it dies unexpectedly. + */ +function _onDesktopServiceVanished() { + _log('Desktop service vanished'); + _onDesktopServiceStopping(); +} + +/** + * Callback invoked when the Gnome15 service starts. We get the initial device + * list at this point. + */ +function _onDesktopServiceStarted() { + _log('Desktop service started'); + gnome15Service.GetDevicesRemote(_refreshDeviceList); +} + +/** + * Invoked when the Gnome15 desktop service starts shutting down (as a result + * of user selecting "Stop Service" most probably). + */ +function _onDesktopServiceStopping() { + _log('Desktop service stopping'); + for(let key in devices) { + _removeDevice(key); + } +} + +/** + * Callback from IsStarted called during initialisation. + */ +function _onStarted(result, excp) { + /* If there was an exception (e.g. g15-desktop-service isn't running) we + return. started value is null in this case */ + if(excp) { + return; + } + + let [started] = result; + if(started) { + gnome15Service.GetDevicesRemote(_refreshDeviceList); + } +} + +/** + * Callback from GetDevicesRemote that reads the returned device list and + * creates a button for each one. + */ +function _refreshDeviceList(result) { + let [devices] = result; + for (let key in devices) { + _addDevice(devices[key]); + } +} + +/** + * Gnome15 doesn't yet send DBus events when devices are hot-plugged, but it + * soon will and this function will add new device when they appear. + * + * @param source device source (may be null) + * @param key device DBUS object path + */ +function _deviceAdded(source, senderName, args) { + let [key] = args; + _addDevice(key); +} + + +function _addDevice(key) { + _log('Added device ' + key); + devices[key] = new DeviceItem(key); +} + +/** + * Gnome15 doesn't yet send DBus events when devices are hot-plugged, but it + * soon will and this function will add new device when they are removed. + * + * @param source device source (may be null) + * @param key device DBUS object path + */ +function _deviceRemoved(source, senderName, args) { + let [key] = args; + _removeDevice(key); +} + +function _removeDevice(key) { + _log('Removed device ' + key); + devices[key].close(); + delete devices[key]; +} + +/** + * Utility for creating a org.gnome15.Screen instance given it's path. + * + * @param path + * @returns {Gnome15ScreenProxy} + */ +function _createScreen(path) { + let Gnome15ScreenProxy = Gio.DBusProxy.makeProxyWrapper(Gnome15ScreenInterface); + return new Gnome15ScreenProxy(Gio.DBus.session, + 'org.gnome15.Gnome15', path); +} + +/** + * Utility for creating an org.gnome15.Device instance given it's path. + * + * @param path + * @returns {Gnome15DeviceProxy} + */ +function _createDevice(path) { + let Gnome15DeviceProxy = Gio.DBusProxy.makeProxyWrapper(Gnome15DeviceInterface); + return new Gnome15DeviceProxy(Gio.DBus.session, + 'org.gnome15.Gnome15', path); +} + +/** + * Utility for logging messages + * + * @param message + */ +function _log(message) { + global.log('gnome15-gnome-shell: ' + message) +} diff --git a/src/gnome-shell-extension/icons/Makefile.am b/src/gnome-shell-extension/icons/Makefile.am new file mode 100644 index 0000000..2de0d3c --- /dev/null +++ b/src/gnome-shell-extension/icons/Makefile.am @@ -0,0 +1,17 @@ +imagesdir = $(datadir)/icons/gnome/scalable/status +images_DATA = logitech-g110-symbolic.svg \ + logitech-g11-symbolic.svg \ + logitech-g13-symbolic.svg \ + logitech-g15v2-symbolic.svg \ + logitech-g15v1-symbolic.svg \ + logitech-g19-symbolic.svg \ + logitech-g35-symbolic.svg \ + logitech-g510-symbolic.svg \ + logitech-g930-symbolic.svg \ + logitech-gamepanel-symbolic.svg \ + logitech-mx5500-symbolic.svg \ + logitech-virtual-symbolic.svg \ + logitech-z10-symbolic.svg + +EXTRA_DIST = \ + $(images_DATA) diff --git a/src/gnome-shell-extension/icons/logitech-g11-symbolic.svg b/src/gnome-shell-extension/icons/logitech-g11-symbolic.svg new file mode 100644 index 0000000..f45f71d --- /dev/null +++ b/src/gnome-shell-extension/icons/logitech-g11-symbolic.svg @@ -0,0 +1,58 @@ + + + + + + + + + + image/svg+xml + + Gnome Symbolic Icon Theme + + + + Gnome Symbolic Icon Theme + + diff --git a/src/gnome-shell-extension/icons/logitech-g110-symbolic.svg b/src/gnome-shell-extension/icons/logitech-g110-symbolic.svg new file mode 100644 index 0000000..aefc9de --- /dev/null +++ b/src/gnome-shell-extension/icons/logitech-g110-symbolic.svg @@ -0,0 +1,58 @@ + + + + + + + + + + image/svg+xml + + Gnome Symbolic Icon Theme + + + + Gnome Symbolic Icon Theme + + diff --git a/src/gnome-shell-extension/icons/logitech-g13-symbolic.svg b/src/gnome-shell-extension/icons/logitech-g13-symbolic.svg new file mode 100644 index 0000000..7a6b9a6 --- /dev/null +++ b/src/gnome-shell-extension/icons/logitech-g13-symbolic.svg @@ -0,0 +1,58 @@ + + + + + + + + + + image/svg+xml + + Gnome Symbolic Icon Theme + + + + Gnome Symbolic Icon Theme + + diff --git a/src/gnome-shell-extension/icons/logitech-g15v1-symbolic.svg b/src/gnome-shell-extension/icons/logitech-g15v1-symbolic.svg new file mode 100644 index 0000000..abe6601 --- /dev/null +++ b/src/gnome-shell-extension/icons/logitech-g15v1-symbolic.svg @@ -0,0 +1,58 @@ + + + + + + + + + + image/svg+xml + + Gnome Symbolic Icon Theme + + + + Gnome Symbolic Icon Theme + + diff --git a/src/gnome-shell-extension/icons/logitech-g15v2-symbolic.svg b/src/gnome-shell-extension/icons/logitech-g15v2-symbolic.svg new file mode 100644 index 0000000..5229499 --- /dev/null +++ b/src/gnome-shell-extension/icons/logitech-g15v2-symbolic.svg @@ -0,0 +1,58 @@ + + + + + + + + + + image/svg+xml + + Gnome Symbolic Icon Theme + + + + Gnome Symbolic Icon Theme + + diff --git a/src/gnome-shell-extension/icons/logitech-g19-symbolic.svg b/src/gnome-shell-extension/icons/logitech-g19-symbolic.svg new file mode 100644 index 0000000..acd5c75 --- /dev/null +++ b/src/gnome-shell-extension/icons/logitech-g19-symbolic.svg @@ -0,0 +1,58 @@ + + + + + + + + + + image/svg+xml + + Gnome Symbolic Icon Theme + + + + Gnome Symbolic Icon Theme + + diff --git a/src/gnome-shell-extension/icons/logitech-g35-symbolic.svg b/src/gnome-shell-extension/icons/logitech-g35-symbolic.svg new file mode 100644 index 0000000..f209ddf --- /dev/null +++ b/src/gnome-shell-extension/icons/logitech-g35-symbolic.svg @@ -0,0 +1,63 @@ + + + + + + + + + + image/svg+xml + + Gnome Symbolic Icon Theme + + + + Gnome Symbolic Icon Theme + + + diff --git a/src/gnome-shell-extension/icons/logitech-g510-symbolic.svg b/src/gnome-shell-extension/icons/logitech-g510-symbolic.svg new file mode 100644 index 0000000..4841d1f --- /dev/null +++ b/src/gnome-shell-extension/icons/logitech-g510-symbolic.svg @@ -0,0 +1,63 @@ + + + + + + + + + + image/svg+xml + + Gnome Symbolic Icon Theme + + + + Gnome Symbolic Icon Theme + + + diff --git a/src/gnome-shell-extension/icons/logitech-g930-symbolic.svg b/src/gnome-shell-extension/icons/logitech-g930-symbolic.svg new file mode 100644 index 0000000..89c1c4b --- /dev/null +++ b/src/gnome-shell-extension/icons/logitech-g930-symbolic.svg @@ -0,0 +1,63 @@ + + + + + + + + + + image/svg+xml + + Gnome Symbolic Icon Theme + + + + Gnome Symbolic Icon Theme + + + diff --git a/src/gnome-shell-extension/icons/logitech-gamepanel-symbolic.svg b/src/gnome-shell-extension/icons/logitech-gamepanel-symbolic.svg new file mode 100644 index 0000000..5cf9edc --- /dev/null +++ b/src/gnome-shell-extension/icons/logitech-gamepanel-symbolic.svg @@ -0,0 +1,63 @@ + + + + + + + + + + image/svg+xml + + Gnome Symbolic Icon Theme + + + + Gnome Symbolic Icon Theme + + + diff --git a/src/gnome-shell-extension/icons/logitech-mx5500-symbolic.svg b/src/gnome-shell-extension/icons/logitech-mx5500-symbolic.svg new file mode 100644 index 0000000..382b603 --- /dev/null +++ b/src/gnome-shell-extension/icons/logitech-mx5500-symbolic.svg @@ -0,0 +1,63 @@ + + + + + + + + + + image/svg+xml + + Gnome Symbolic Icon Theme + + + + Gnome Symbolic Icon Theme + + + diff --git a/src/gnome-shell-extension/icons/logitech-source.svg b/src/gnome-shell-extension/icons/logitech-source.svg new file mode 100644 index 0000000..617b0d4 --- /dev/null +++ b/src/gnome-shell-extension/icons/logitech-source.svg @@ -0,0 +1,74 @@ + + + + + + + + + + image/svg+xml + + Gnome Symbolic Icon Theme + + + + Gnome Symbolic Icon Theme + + GTK + diff --git a/src/gnome-shell-extension/icons/logitech-virtual-symbolic.svg b/src/gnome-shell-extension/icons/logitech-virtual-symbolic.svg new file mode 100644 index 0000000..91599df --- /dev/null +++ b/src/gnome-shell-extension/icons/logitech-virtual-symbolic.svg @@ -0,0 +1,63 @@ + + + + + + + + + + image/svg+xml + + Gnome Symbolic Icon Theme + + + + Gnome Symbolic Icon Theme + + + diff --git a/src/gnome-shell-extension/icons/logitech-z10-symbolic.svg b/src/gnome-shell-extension/icons/logitech-z10-symbolic.svg new file mode 100644 index 0000000..75dff68 --- /dev/null +++ b/src/gnome-shell-extension/icons/logitech-z10-symbolic.svg @@ -0,0 +1,63 @@ + + + + + + + + + + image/svg+xml + + Gnome Symbolic Icon Theme + + + + Gnome Symbolic Icon Theme + + + diff --git a/src/gnome-shell-extension/metadata.json b/src/gnome-shell-extension/metadata.json new file mode 100644 index 0000000..9adead5 --- /dev/null +++ b/src/gnome-shell-extension/metadata.json @@ -0,0 +1,13 @@ +{ + "shell-version": [ + "3.4", + "3.6", + "3.8", + "3.10" + ], + "uuid": "gnome15-shell-extension@gnome15.org", + "name": "Logitech Keyboard", + "description": "Configure and control Logitech Keyboards such as G15, G19, G13 as well as other devices such as G35 headphones, Z10 speakers and moref", + "url": "http://www.russo79.com/gnome15", + "extension-id": "gnome15-shell-extension" +} diff --git a/src/gnome-shell-extension/stylesheet.css b/src/gnome-shell-extension/stylesheet.css new file mode 100644 index 0000000..f117709 --- /dev/null +++ b/src/gnome-shell-extension/stylesheet.css @@ -0,0 +1,14 @@ +.device-button { +} + +.device-icon { +} + +.helloworld-label { + font-size: 36px; + font-weight: bold; + color: #ffffff; + background-color: rgba(10,10,10,0.7); + border-radius: 5px; + padding: .5em; +} diff --git a/src/gnome15/Makefile.am b/src/gnome15/Makefile.am new file mode 100644 index 0000000..e034f80 --- /dev/null +++ b/src/gnome15/Makefile.am @@ -0,0 +1,43 @@ +SUBDIRS = drivers util +gnome15dir = $(pkgpythondir) +gnome15_PYTHON = \ + __init__.py \ + g15accounts.py \ + g15service.py \ + g15config.py \ + g15macroeditor.py \ + g15actions.py \ + g15devices.py \ + g15desktop.py \ + g15dbus.py \ + g15debug.py \ + g15dconf.py \ + g15gtk.py \ + g15driver.py \ + g15notify.py \ + g15network.py \ + g15drivermanager.py \ + g15globals.py \ + g15plugin.py \ + g15top.py \ + g15locale.py \ + g15keyboard.py \ + g15keyio.py \ + g15pluginmanager.py \ + g15profile.py \ + g15exceptions.py \ + g15screen.py \ + g15system.py \ + g15theme.py \ + g15text.py \ + g15util.py \ + g15upgrade.py \ + g15uinput.py \ + g15logging.py \ + objgraph.py \ + dbusmenu.py \ + colorpicker.py \ + lcdsink.py + +EXTRA_DIST = \ + $(gnome15_PYTHON) \ No newline at end of file diff --git a/src/gnome15/__init__.py b/src/gnome15/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/gnome15/colorpicker.py b/src/gnome15/colorpicker.py new file mode 100644 index 0000000..8d7f5c3 --- /dev/null +++ b/src/gnome15/colorpicker.py @@ -0,0 +1,253 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2011 Brett Smith +# +# 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 . + +import gtk +import cairo +from gtk import gdk +import gobject +import g15globals +import util.g15convert as g15convert +import os + +COLORS_REDBLUE = [(0, 0, 0, 1), (255, 0, 0, 1), (255, 0, 255, 1), (0, 0, 255, 1) ] +COLORS_FULL = [(0, 0, 0, 1), (255, 0, 0, 1), (0, 255, 0, 1), (0, 0, 255, 1), (255, 255, 0, 1), (0, 255, 255, 1), (255, 0, 255, 1), (255, 255, 255, 1) ] +COLORS_NAMES = ["Black", "Red", "Green", "Blue", "Yellow", "Cyan", "Indigo", "White" ] + +CELL_HEIGHT = 12 +CELL_WIDTH = 24 + +def _get_color_at( buffer, x, y): + x = int(x) + y = int(y) + data = buffer.get_data() + w = buffer.get_width() + s = ( buffer.get_stride() / w ) * ( w * y + x ) + s = max(0, min(s, len(data) - 3)) + b = ord(data[s]) + g = ord(data[s + 1]) + r = ord(data[s + 2]) + return (r, g, b) + +def _rounded_rectangle(cr, x, y, w, h, r=20): + cr.move_to(x+r,y) # Move to A + cr.line_to(x+w-r,y) # Straight line to B + cr.curve_to(x+w,y,x+w,y,x+w,y+r) # Curve to C, Control points are both at Q + cr.line_to(x+w,y+h-r) # Move to D + cr.curve_to(x+w,y+h,x+w,y+h,x+w-r,y+h) # Curve to E + cr.line_to(x+r,y+h) # Line to F + cr.curve_to(x,y+h,x,y+h,x,y+h-r) # Curve to G + cr.line_to(x,y+r) # Line to H + cr.curve_to(x,y,x,y,x+r,y) # Curve to A + +class ColorPreview(gtk.DrawingArea): + + def __init__(self, picker): + self.__gobject_init__() + self.picker = picker + super(ColorPreview, self).__init__() + self.set_size_request(CELL_WIDTH, CELL_HEIGHT) + self.connect("expose-event", self._expose) + self.connect("button-press-event", self._button_press) + self.down = False + self.add_events(gdk.BUTTON1_MOTION_MASK | gdk.BUTTON_PRESS_MASK) + + def _show_redblue_picker(self, widget_tree): + main_window = widget_tree.get_object("RBPicker") + c_widget = widget_tree.get_object("RBImageEvents") + img_surface = cairo.ImageSurface.create_from_png(os.path.join(g15globals.ui_dir, 'redblue.png')) + + r_adjustment = widget_tree.get_object("RAdjustment") + r_adjustment.set_value(self.picker.color[0]) + b_adjustment = widget_tree.get_object("BAdjustment") + b_adjustment.set_value(self.picker.color[2]) + self.adjusting_rb = False + self.picker_down = False + + def _update_adj(c): + self.picker._select_color((int(r_adjustment.get_value()), \ + 0, int(b_adjustment.get_value()))) + + def _set_color(c): + r_adjustment.set_value(c[0]) + b_adjustment.set_value(c[2]) + + def _button_release(widget, event): + self.picker_down = False + + def _button_press( widget, event): + _set_color(_get_color_at(img_surface, event.x, event.y)) + self.picker_down = True + + def _mouse_motion(widget, event): + if self.picker_down: + _set_color(_get_color_at(img_surface, event.x, event.y)) + + r_adjustment.connect("value-changed", _update_adj) + b_adjustment.connect("value-changed", _update_adj) + c_widget.connect("button-press-event", _button_press) + c_widget.connect("button-release-event", _button_release) + c_widget.connect("motion-notify-event", _mouse_motion) + c_widget.add_events(gdk.BUTTON1_MOTION_MASK | gdk.BUTTON_PRESS_MASK | gdk.BUTTON_RELEASE_MASK | gtk.gdk.POINTER_MOTION_MASK | gtk.gdk.POINTER_MOTION_HINT_MASK) + + main_window.set_transient_for(self.get_toplevel()) + main_window.run() + main_window.hide() + + def _show_picker(self, widget_tree): + main_window = widget_tree.get_object("RGBPicker") + c_widget = widget_tree.get_object("RGBColour") + c_widget.set_current_color(g15convert.to_color(self.picker.color)) + def colour_picked(arg): + self.picker._select_color(g15convert.color_to_rgb(c_widget.get_current_color())) + c_widget.connect("color-changed", colour_picked) + main_window.set_transient_for(self.get_toplevel()) + main_window.run() + main_window.hide() + + def _button_press(self, widget, event): + widget_tree = gtk.Builder() + widget_tree.set_translation_domain("colorpicker") + widget_tree.add_from_file(os.path.join(g15globals.ui_dir, 'colorpicker.ui')) + if self.picker.redblue: + self._show_redblue_picker(widget_tree) + else: + self._show_picker(widget_tree) + + def _expose(self, widget, event): + size = self.size_request() + cell_height = self.allocation[3] + cell_width = self.allocation[2] + + ctx = widget.window.cairo_create() + ctx.set_line_width(1.0) + + # Draw to a back buffer so we can get the color at the point + ctx.set_source_rgb(float(self.picker.color[0]) / 255.0, float(self.picker.color[1]) / 255.0, float(self.picker.color[2]) / 255.0) + _rounded_rectangle(ctx, 0, 0, cell_width, cell_height, 16) + ctx.fill() + ctx.set_operator(cairo.OPERATOR_OVER) + ctx.set_source_rgb(0.5, 0.5, 0.5) + _rounded_rectangle(ctx, 0, 0, cell_width, cell_height, 16) + ctx.stroke() + +class ColorBar(gtk.DrawingArea): + + def __init__(self, picker): + self.__gobject_init__() + super(ColorBar, self).__init__() + self.picker = picker + self.set_size_request(len(self.picker.colors) * CELL_WIDTH, CELL_HEIGHT) + self.connect("expose-event", self._expose) + self.connect("button-press-event", self._button_press) + self.connect("button-release-event", self._button_release) + self.connect("motion-notify-event", self._mouse_motion) + self.down = False + self.add_events(gdk.BUTTON1_MOTION_MASK | gdk.BUTTON_PRESS_MASK | gdk.BUTTON_RELEASE_MASK | gtk.gdk.POINTER_MOTION_MASK | gtk.gdk.POINTER_MOTION_HINT_MASK) + self.picker_image_surface = None + + def _mouse_motion(self, widget, event): + if self.picker_image_surface is not None and self.down: + self.picker._select_color(_get_color_at(self.picker_image_surface, event.x, event.y)) + + def _button_press(self, widget, event): + if self.picker_image_surface is not None: + self.picker._select_color(_get_color_at(self.picker_image_surface, event.x, event.y)) + self.down = True + + def _button_release(self, widget, event): + self.down = False + + def _do_small_bar(self, ctx, cell_height, cell_width, c): + ctx.set_source_rgb(float(c[0]) / 255.0, float(c[1]) / 255.0, float(c[2]) / 255.0) + ctx.rectangle(0, 0, cell_width, cell_height) + ctx.fill() + ctx.translate(cell_width, 0) + + def _do_bar(self, ctx, cell_height, cell_width, p, c): + ctx.set_source_rgb(float(c[0]) / 255.0, float(c[1]) / 255.0, float(c[2]) / 255.0) + lg1 = cairo.LinearGradient(0.0, 0.0, cell_width, 0) + lg1.add_color_stop_rgba(0.0, float(p[0]) / 255.0, float(p[1]) / 255.0, float(p[2]) / 255.0, float(p[3])) + lg1.add_color_stop_rgba(0.5, float(c[0]) / 255.0, float(c[1]) / 255.0, float(c[2]) / 255.0, float(c[3])) + ctx.rectangle(0, 0, cell_width, cell_height) + ctx.set_source(lg1) + ctx.fill() + ctx.translate(cell_width, 0) + + def _expose(self, widget, event): + cr = widget.window.cairo_create() + cr.set_line_width(1.0) + size = (self.allocation[2],self.allocation[3]) + cell_height = size[1] + tc = len(self.picker.colors) + cell_width = size[0] / tc + main_width = cell_width * tc + + # Draw to a back buffer so we can get the color at the point + picker_image_surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, size[0], size[1]) + ctx = cairo.Context(picker_image_surface) + ctx.save() + ctx.translate(1, 1) + colors = self.picker.colors + lc = colors[0] + self._do_small_bar(ctx, cell_height, cell_width, lc) + for i in range(0, len(colors) - 1): + c = colors[i] + self._do_bar(ctx, cell_height, cell_width, c, colors[i + 1]) + self._do_small_bar(ctx, cell_height, cell_width / 2, lc) + ctx.restore() + _rounded_rectangle(ctx, 0, 0, main_width, cell_height, 16) + ctx.set_operator(cairo.OPERATOR_DEST_IN) + ctx.fill() + ctx.set_operator(cairo.OPERATOR_OVER) + ctx.set_source_rgb(0.5, 0.5, 0.5) + _rounded_rectangle(ctx, 0, 0, main_width, cell_height, 16) + ctx.stroke() + + # Paint + cr.set_source_surface(picker_image_surface) + cr.paint() + self.picker_image_surface = picker_image_surface + +class ColorPicker(gtk.HBox): + + def __init__(self, colors = None, redblue = False): + self.__gobject_init__() + gtk.HBox.__init__(self, spacing = 8) + self.colors = colors if colors is not None else ( COLORS_REDBLUE if redblue else COLORS_FULL ) + self.redblue = redblue + self.color = (0,0,0) + super(ColorPicker, self).__init__() + + bar = ColorBar(self) + preview = ColorPreview(self) + + self.pack_start(bar, True, True) + self.pack_start(preview, False, True) + + def set_color(self, color): + self.color = color + self.queue_draw() + + def _select_color(self, color): + self.color = color + self.queue_draw() + self.emit("color-chosen") + +gobject.type_register(ColorPicker) +gobject.type_register(ColorBar) +gobject.type_register(ColorPreview) +gobject.signal_new("color-chosen", ColorPicker, gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, ()) \ No newline at end of file diff --git a/src/gnome15/dbusmenu.py b/src/gnome15/dbusmenu.py new file mode 100644 index 0000000..91406d9 --- /dev/null +++ b/src/gnome15/dbusmenu.py @@ -0,0 +1,198 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2010 Brett Smith +# +# 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 . + +import dbus + +from lxml import etree +import time +import logging +logger = logging.getLogger(__name__) + + +''' +DBUSMenu property names +''' +VISIBLE = "visible" +ICON_NAME = "icon-name" +TYPE = "type" +LABEL = "label" +TOGGLE_TYPE = "toggle-type" +TOGGLE_STATE = "toggle-state" +ENABLED = "enabled" + +TYPE_SEPARATOR = "separator" +TYPE_ROOT = "root" + +TOGGLE_TYPE_NONE = "none" +TOGGLE_TYPE_RADIO = "radio" + +class DBUSMenuEntry(): + def __init__(self, id, properties, menu): + self.id = id + self.menu = menu + self.set_properties(properties) + self.children = [] + + def set_properties(self, properties): + self.properties = properties + if not VISIBLE in self.properties: + self.properties[VISIBLE] = True + self.label = self.properties[LABEL] if LABEL in self.properties else None + self.icon = None + self.type = self.properties[TYPE] if TYPE in self.properties else TYPE_ROOT + self.toggle_type = self.properties[TOGGLE_TYPE] if TOGGLE_TYPE in self.properties else TOGGLE_TYPE_NONE + self.toggle_state = self.properties[TOGGLE_STATE] if TOGGLE_STATE in self.properties else 0 + self.enabled = not ENABLED in self.properties or self.properties[ENABLED] + + def flatten(self, include_self = False): + flat_list = [] + if include_self: + self._flatten(self, flat_list) + else: + for c in self.children: + self._flatten(c, flat_list) + return flat_list + + def about_to_show(self): + return self.menu.dbus_menu.AboutToShow(self.id) + + def activate(self, variant = 0): + self.menu.dbus_menu.Event(self.id, "clicked", variant, int(time.time())) + + def hover(self, variant = 0): + self.menu.dbus_menu.Event(self.id, "hovered", variant, int(time.time())) + + def _flatten(self, element, flat_list): + flat_list.append(element) + for c in element.children: + self._flatten(c, flat_list) + + def is_visible(self): + return VISIBLE in self.properties and self.properties[VISIBLE] + + def get_label(self): + return self.label + + def get_icon(self): + return self.icon + + def get_alt_label(self): + return "" + + def get_icon_name(self): + return self.properties[ICON_NAME] if ICON_NAME in self.properties else None + +class DBUSMenu(): + + def __init__(self, session_bus, object_name, path, interface, on_change = None, natty = False): + self.natty = natty + self.session_bus = session_bus + self.on_change = on_change + self.messages_menu = self.session_bus.get_object(object_name, path) + self.dbus_menu = dbus.Interface(self.messages_menu, interface) + + self.dbus_menu.connect_to_signal("ItemUpdated", self._item_updated) + self.dbus_menu.connect_to_signal("ItemPropertyUpdated", self._item_property_updated) + self.dbus_menu.connect_to_signal("LayoutUpdated", self._layout_updated) + self.dbus_menu.connect_to_signal("ItemActivationRequested", self._item_activation_requested) + + # From Natty onwards + if self.natty: + self.dbus_menu.connect_to_signal("ItemsPropertiesUpdated", self._item_properties_updated) + + self._get_layout() + + def create_entry(self, id, properties): + return DBUSMenuEntry(id, properties, self) + + ''' + Private + ''' + + def _item_activation_requested(self, id, timestamp): + logger.warning("TODO - implement item activation request for %s on %d", id, timestamp) + + def _layout_updated(self, revision, parent): + self._get_layout() + if self.on_change != None: + self.on_change() + + def _item_updated(self, id): + if str(id) in self.menu_map: + menu = self.menu_map[str(id)] + menu.set_properties(self.dbus_menu.GetProperties(id, [])) + if self.on_change != None: + self.on_change(menu) + else: + logger.warning("Update request for item not in map") + + def _item_properties_updated(self, updated_properties, removed_properties): + for id, properties in updated_properties: + if str(id) in self.menu_map: + menu = self.menu_map[str(id)] + for prop in properties: + value = properties[prop] + if not prop in menu.properties or value != menu.properties[prop]: + menu.properties[prop] = value + menu.set_properties(menu.properties) + if self.on_change != None: + self.on_change(menu, prop, value) + else: + logger.warning("Update request for item not in map") + +# for id, properties in removed_properties: +# print "Removed: ",str(id),str(properties) + + def _item_property_updated(self, id, prop, value): + if str(id) in self.menu_map: + menu = self.menu_map[str(id)] + if not prop in menu.properties or value != menu.properties[prop]: + menu.properties[prop] = value + menu.set_properties(menu.properties) + if self.on_change != None: + self.on_change(menu, prop, value) + else: + logger.warning("Update request for item not in map") + + def _get_layout(self): + self.menu_map = {} + if self.natty: + revision, layout = self.dbus_menu.GetLayout(0, 3, []) + self.root_item = self._load_menu_struct(layout, self.menu_map) + else: + revision, menu_xml = self.dbus_menu.GetLayout(0) + self.root_item = self._load_xml_menu(etree.fromstring(menu_xml), self.menu_map) + + def _load_menu_struct(self, layout, map): + id = layout[0] + properties = layout[1] + menu = self.create_entry(id, dict(properties)) + map[str(id)] = menu + children = layout[2] + for item in children: + menu.children.append(self._load_menu_struct(item, map)) + return menu + + def _load_xml_menu(self, element, map): + id = int(element.get("id")) + menu = self.create_entry(id, dict(self.dbus_menu.GetProperties(id, []))) + map[str(id)] = menu + for child in element: + try : + menu.children.append(self._load_xml_menu(child, map)) + except dbus.DBUSException as e: + logger.warning("Failed to get child menu.", exc_info = e) + return menu diff --git a/src/gnome15/drivers/Makefile.am b/src/gnome15/drivers/Makefile.am new file mode 100644 index 0000000..cecd0d3 --- /dev/null +++ b/src/gnome15/drivers/Makefile.am @@ -0,0 +1,28 @@ +if ENABLE_DRIVER_KERNEL + MAYBE_KERNEL = driver_kernel.py fb.py +endif +if ENABLE_DRIVER_G19DIRECT + MAYBE_G19DIRECT = driver_g19direct.py +endif +if ENABLE_DRIVER_G15DIRECT + MAYBE_G15DIRECT = driver_g15direct.py pylibg15.py +endif +if ENABLE_DRIVER_G930 + MAYBE_G930 = driver_g930.py +endif + +driversdir = $(pkgpythondir)/drivers + +drivers_PYTHON = __init__.py \ + driver_gtk.py \ + $(MAYBE_KERNEL) $(MAYBE_G19DIRECT) $(MAYBE_G15DIRECT) $(MAYBE_G930) + +EXTRA_DIST = __init__.py \ + driver_g15direct.py \ + driver_g930.py \ + driver_gtk.py \ + driver_kernel.py \ + fb.py \ + pylibg15.py + + \ No newline at end of file diff --git a/src/gnome15/drivers/__init__.py b/src/gnome15/drivers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/gnome15/drivers/driver_g15direct.py b/src/gnome15/drivers/driver_g15direct.py new file mode 100644 index 0000000..f410def --- /dev/null +++ b/src/gnome15/drivers/driver_g15direct.py @@ -0,0 +1,691 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2011 Brett Smith +# +# 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 . + +""" +Alternative implementation of a G19 Driver that uses pylibg19 to communicate directly +with the keyboard +""" +import gnome15.g15locale as g15locale +_ = g15locale.get_translation("gnome15-drivers").ugettext + +from threading import RLock +import cairo +import gnome15.g15driver as g15driver +import gnome15.g15globals as g15globals +import gnome15.util.g15scheduler as g15scheduler +import gnome15.util.g15uigconf as g15uigconf +import gnome15.util.g15gconf as g15gconf +import gnome15.g15uinput as g15uinput +import gnome15.g15exceptions as g15exceptions +import sys +import os +import gconf +import gtk +import logging +from PIL import ImageMath +from PIL import Image +import array +logger = logging.getLogger(__name__) +load_error = None +try : + import pylibg15 +except Exception as a: + logger.debug("Could not import pylibg15 module", exc_info = a) + load_error = a + +# Import from local version of pylibg19 if available +if g15globals.dev: + sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "pylibg19")) + +# Driver information (used by driver selection UI) +name=_("G15 Direct") +id="g15direct" +description=_("For use with the G15 based devices only, this driver communicates directly, " + \ + "with the keyboard and so is more efficient than the g15daemon driver. Note, " + \ + "you will have to ensure the permissions of the USB devices are read/write " + \ + "for your user.") +has_preferences=True + + +DEBUG_LIBG15="DEBUG_LIBG15" in os.environ +KEY_MAP = { + g15driver.G_KEY_G1 : 1<<0, + g15driver.G_KEY_G2 : 1<<1, + g15driver.G_KEY_G3 : 1<<2, + g15driver.G_KEY_G4 : 1<<3, + g15driver.G_KEY_G5 : 1<<4, + g15driver.G_KEY_G6 : 1<<5, + g15driver.G_KEY_G7 : 1<<6, + g15driver.G_KEY_G8 : 1<<7, + g15driver.G_KEY_G9 : 1<<8, + g15driver.G_KEY_G10 : 1<<9, + g15driver.G_KEY_G11 : 1<<10, + g15driver.G_KEY_G12 : 1<<11, + g15driver.G_KEY_G13 : 1<<12, + g15driver.G_KEY_G14 : 1<<13, + g15driver.G_KEY_G15 : 1<<14, + g15driver.G_KEY_G16 : 1<<15, + g15driver.G_KEY_G17 : 1<<16, + g15driver.G_KEY_G18 : 1<<17, + + g15driver.G_KEY_M1 : 1<<18, + g15driver.G_KEY_M2 : 1<<19, + g15driver.G_KEY_M3 : 1<<20, + g15driver.G_KEY_MR : 1<<21, + + g15driver.G_KEY_L1 : 1<<22, + g15driver.G_KEY_L2 : 1<<23, + g15driver.G_KEY_L3 : 1<<24, + g15driver.G_KEY_L4 : 1<<25, + g15driver.G_KEY_L5 : 1<<26, + + g15driver.G_KEY_LIGHT : 1<<27 +} + +EXT_KEY_MAP = { + g15driver.G_KEY_G19 : 1<<0, + g15driver.G_KEY_G20 : 1<<1, + g15driver.G_KEY_G21 : 1<<2, + g15driver.G_KEY_G22 : 1<<3, + + g15driver.G_KEY_JOY_LEFT : 1<<4, + g15driver.G_KEY_JOY_DOWN : 1<<5, + g15driver.G_KEY_JOY_CENTER : 1<<6, + g15driver.G_KEY_JOY : 1<<7 + } + +REVERSE_KEY_MAP = {} +for k in KEY_MAP.keys(): + REVERSE_KEY_MAP[KEY_MAP[k]] = k +EXT_REVERSE_KEY_MAP = {} +for k in EXT_KEY_MAP.keys(): + EXT_REVERSE_KEY_MAP[EXT_KEY_MAP[k]] = k + +mkeys_control = g15driver.Control("mkeys", _("Memory Bank Keys"), 1, 0, 15, hint=g15driver.HINT_MKEYS) +color_backlight_control = g15driver.Control("backlight_colour", _("Keyboard Backlight Colour"), (0, 255, 0), hint = g15driver.HINT_DIMMABLE | g15driver.HINT_SHADEABLE) +red_blue_backlight_control = g15driver.Control("backlight_colour", _("Keyboard Backlight Colour"), (255, 0, 0), hint = g15driver.HINT_DIMMABLE | g15driver.HINT_SHADEABLE | g15driver.HINT_RED_BLUE_LED) +backlight_control = g15driver.Control("keyboard_backlight", _("Keyboard Backlight Level"), 2, 0, 2, hint = g15driver.HINT_DIMMABLE | g15driver.HINT_SHADEABLE) +lcd_backlight_control = g15driver.Control("lcd_backlight", _("LCD Backlight Level"), 2, 0, 2, hint = g15driver.HINT_SHADEABLE) +lcd_contrast_control = g15driver.Control("lcd_contrast", _("LCD Contrast"), 22, 0, 2) +invert_control = g15driver.Control("invert_lcd", _("Invert LCD"), 0, 0, 1, hint = g15driver.HINT_SWITCH ) + +controls = { + g15driver.MODEL_G11 : [ mkeys_control, backlight_control ], + g15driver.MODEL_G15_V1 : [ mkeys_control, backlight_control, lcd_contrast_control, lcd_backlight_control, invert_control ], + g15driver.MODEL_G15_V2 : [ mkeys_control, backlight_control, lcd_backlight_control, invert_control ], + g15driver.MODEL_G13 : [ mkeys_control, color_backlight_control, invert_control ], + g15driver.MODEL_G510 : [ mkeys_control, color_backlight_control, invert_control ], + g15driver.MODEL_Z10 : [ backlight_control, lcd_backlight_control, invert_control ], + g15driver.MODEL_G110 : [ mkeys_control, red_blue_backlight_control ], + } + +# Default offsets +ANALOGUE_OFFSET = 20 +DIGITAL_OFFSET = 64 + +def show_preferences(device, parent, gconf_client): + prefs = G15DirectDriverPreferences(device, parent, gconf_client) + prefs.run() + +class G15DirectDriverPreferences(): + + def __init__(self, device, parent, gconf_client): + self.gconf_client = gconf_client + self.device = device + + g15locale.get_translation("driver_g15direct") + widget_tree = gtk.Builder() + widget_tree.set_translation_domain("driver_g15direct") + widget_tree.add_from_file(os.path.join(g15globals.ui_dir, "driver_g15direct.ui")) + self.window = widget_tree.get_object("G15DirectDriverSettings") + self.window.set_transient_for(parent) + + g15uigconf.configure_spinner_from_gconf(gconf_client, + "/apps/gnome15/%s/timeout" % device.uid, + "Timeout", + 10000, + widget_tree, + False) + if not device.model_id == g15driver.MODEL_G13: + widget_tree.get_object("JoyModeCombo").destroy() + widget_tree.get_object("JoyModeLabel").destroy() + widget_tree.get_object("Offset").destroy() + widget_tree.get_object("OffsetLabel").destroy() + widget_tree.get_object("OffsetDescription").destroy() + else: + g15uigconf.configure_combo_from_gconf(gconf_client, + "/apps/gnome15/%s/joymode" % device.uid, + "JoyModeCombo", + "macro", + widget_tree) + # We have separate offset values for digital / analogue, + # so swap between them based on configuration + self.offset_widget = widget_tree.get_object("Offset") + self._set_offset_depending_on_mode(None) + widget_tree.get_object("JoyModeCombo").connect("changed", + self._set_offset_depending_on_mode) + self.offset_widget.connect("value-changed", self._spinner_changed) + + def run(self): + self.window.run() + self.window.hide() + + def _set_offset_depending_on_mode(self, widget): + mode = self.gconf_client.get_string("/apps/gnome15/%s/joymode" % self.device.uid) + offset_model = self.offset_widget.get_adjustment() + if mode in [ g15uinput.JOYSTICK, g15uinput.MOUSE]: + val = g15gconf.get_int_or_default(self.gconf_client, + "/apps/gnome15/%s/analogue_offset" % self.device.uid, + ANALOGUE_OFFSET) + else: + val = g15gconf.get_int_or_default(self.gconf_client, + "/apps/gnome15/%s/digital_offset" % self.device.uid, + DIGITAL_OFFSET) + offset_model.set_value(val) + + def _spinner_changed(self, widget): + mode = self.gconf_client.get_string("/apps/gnome15/%s/joymode" % self.device.uid) + if mode in [ g15uinput.JOYSTICK, g15uinput.MOUSE]: + self.gconf_client.set_int("/apps/gnome15/%s/analogue_offset" % self.device.uid, + int(widget.get_value())) + else: + self.gconf_client.set_int("/apps/gnome15/%s/digital_offset" % self.device.uid, + int(widget.get_value())) + +def fix_sans_style(root): + for element in root.iter(): + style = element.get("style") + if style != None: + element.set("style", style.replace("font-family:Sans","font-family:%s" % g15globals.fixed_size_font_name)) + + +class Driver(g15driver.AbstractDriver): + + def __init__(self, device, on_close = None): + if load_error is not None: + raise load_error + g15driver.AbstractDriver.__init__(self, "g15direct") + self.on_close = on_close + self.device = device + self.timer = None + self.joy_mode = None + self.lock = RLock() + self.down = [] + self.move_x = 0 + self.move_y = 0 + self.connected = False + self.conf_client = gconf.client_get_default() + self.last_keys = None + self.last_ext_keys = None + + # We can only have one instance of this driver active in a single runtime + self.allow_multiple = False + + def get_antialias(self): + return cairo.ANTIALIAS_NONE + + def get_size(self): + return self.device.lcd_size + + def get_bpp(self): + return self.device.bpp + + def get_controls(self): + return controls[self.device.model_id] + + def get_key_layout(self): + if self.get_model_name() == g15driver.MODEL_G13 and "macro" == self.conf_client.get_string("/apps/gnome15/%s/joymode" % self.device.uid): + """ + This driver with the G13 supports some additional keys + """ + l = list(self.device.key_layout) + l.append([ g15driver.G_KEY_UP ]) + l.append([ g15driver.G_KEY_JOY_LEFT, g15driver.G_KEY_LEFT, g15driver.G_KEY_JOY_CENTER, g15driver.G_KEY_RIGHT ]) + l.append([ g15driver.G_KEY_JOY_DOWN, g15driver.G_KEY_DOWN ]) + return l + else: + return self.device.key_layout + + def get_action_keys(self): + return self.device.action_keys + + def process_svg(self, document): + fix_sans_style(document.getroot()) + + def on_update_control(self, control): + self.lock.acquire() + try : + self._do_update_control(control) + finally: + self.lock.release() + + def get_name(self): + return _("G15 Direct") + + def get_model_names(self): + return [ g15driver.MODEL_G11, g15driver.MODEL_G15_V1, \ + g15driver.MODEL_G15_V2, g15driver.MODEL_G110, \ + g15driver.MODEL_G510, g15driver.MODEL_Z10, \ + g15driver.MODEL_G13 ] + + def get_model_name(self): + return self.device.model_id + + def grab_keyboard(self, callback): + self.callback = callback + self.last_keys = None + self.last_ext_keys = None + self.thread = pylibg15.grab_keyboard(self._handle_key_event, \ + g15gconf.get_int_or_default(self.conf_client, "/apps/gnome15/usb_key_read_timeout", 100), + self._on_error) + self.thread.on_unplug = self._keyboard_unplugged + + def is_connected(self): + return self.connected + + def paint(self, img): + if not self.is_connected(): + return + + # Just return if the device has no LCD + if self.device.bpp == 0: + return None + + self.lock.acquire() + try : + size = self.get_size() + + # Paint to 565 image provided into an ARGB image surface for PIL's benefit. PIL doesn't support 565? + argb_surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, size[0], size[1]) + argb_context = cairo.Context(argb_surface) + argb_context.set_source_surface(img) + argb_context.paint() + + # Now convert the ARGB to a PIL image so it can be converted to a 1 bit monochrome image, with all + # colours dithered. It would be nice if Cairo could do this :( Any suggestions? + pil_img = Image.frombuffer("RGBA", size, argb_surface.get_data(), "raw", "RGBA", 0, 1) + pil_img = ImageMath.eval("convert(pil_img,'1')",pil_img=pil_img) + pil_img = ImageMath.eval("convert(pil_img,'P')",pil_img=pil_img) + pil_img = pil_img.point(lambda i: i >= 250,'1') + + invert_control = self.get_control("invert_lcd") + if invert_control.value == 0: + pil_img = pil_img.point(lambda i: 1^i) + + # Convert image buffer to string + buf = "" + for x in list(pil_img.getdata()): + buf += chr(x) + + if len(buf) != self.device.lcd_size[0] * self.device.lcd_size[1]: + logger.warning("Invalid buffer size") + else: + # TODO Replace with C routine + arrbuf = array.array('B', self.empty_buf) + width, height = self.get_size() + max_byte_offset = 0 + for x in range(0, width): + for y in range(0, height): + pixel_offset = y * width + x; + byte_offset = pixel_offset / 8; + max_byte_offset = max(max_byte_offset, byte_offset) + bit_offset = 7-(pixel_offset % 8); + val = ord(buf[x+(y*160)]) + pv = arrbuf[byte_offset] + if val > 0: + arrbuf[byte_offset] = pv | 1 << bit_offset + else: + arrbuf[byte_offset] = pv & ~(1 << bit_offset) + buf = arrbuf.tostring() + try : + logger.debug("Writing buffer of %d bytes", len(buf)) + pylibg15.write_pixmap(buf) + except IOError as e: + logger.error("Failed to send buffer.", exc_info = e) + self.disconnect() + finally: + self.lock.release() + + """ + Private + """ + def _on_error(self, code): + logger.info("Disconnected due to error %d", code) + self.disconnect() + + def _on_connect(self): + self.thread = None + self.callback = None + self.notify_handles = [] + + # Create an empty string buffer for use with monochrome LCD + self.empty_buf = "" + for _ in range(0, 861): + self.empty_buf += chr(0) + + # TODO Enable UINPUT if multimedia key support is required? + self.timeout = 10000 + e = self.conf_client.get("/apps/gnome15/%s/timeout" % self.device.uid) + if e: + self.timeout = e.get_int() + + logger.info("Initialising pylibg15, looking for %s:%s", + hex(self.device.controls_usb_id[0]), + hex(self.device.controls_usb_id[1])) + if DEBUG_LIBG15 or ( logger.level < logging.WARN and logger.level != logging.NOTSET ): + pylibg15.set_debug(pylibg15.G15_LOG_INFO) + err = pylibg15.init(False, self.device.controls_usb_id[0], self.device.controls_usb_id[1]) + if err != pylibg15.G15_NO_ERROR: + raise g15exceptions.NotConnectedException("libg15 returned error %d " % err) + logger.info("Initialised pylibg15") + self.connected = True + + for control in self.get_controls(): + self._do_update_control(control) + + self.notify_handles.append(self.conf_client.notify_add("/apps/gnome15/%s/joymode" % self.device.uid, self._config_changed, None)) + self.notify_handles.append(self.conf_client.notify_add("/apps/gnome15/%s/timeout" % self.device.uid, self._config_changed, None)) + self.notify_handles.append(self.conf_client.notify_add("/apps/gnome15/%s/digital_offset" % self.device.uid, self._config_changed, None)) + self.notify_handles.append(self.conf_client.notify_add("/apps/gnome15/%s/analogue_offset" % self.device.uid, self._config_changed, None)) + + self._load_configuration() + + def _load_configuration(self): + self.joy_mode = self.conf_client.get_string("/apps/gnome15/%s/joymode" % self.device.uid) + self.digital_calibration = g15gconf.get_int_or_default(self.conf_client, "/apps/gnome15/%s/digital_offset" % self.device.uid, 63) + self.analogue_calibration = g15gconf.get_int_or_default(self.conf_client, "/apps/gnome15/%s/analogue_offset" % self.device.uid, 20) + + def _config_changed(self, client, connection_id, entry, args): + self._load_configuration() + + def _on_disconnect(self): + if self.is_connected(): + for h in self.notify_handles: + self.conf_client.notify_remove(h) + logger.info("Exiting pylibg15") + self.connected = False + if self.thread is not None: + self.thread.on_exit = pylibg15.exit + self.thread.deactivate() + else: + pylibg15.exit() + if self.on_close != None: + self.on_close(self) + else: + raise Exception("Not connected") + + def _keyboard_unplugged(self): + logger.info("Keyboard has been unplugged.") + self.disconnect() + + def _get_g510_multimedia_keys(self, code): + keys = [] + code &= ~(1<<28) + if code & 1 << 0 != 0: + keys.append(g15uinput.KEY_PLAYPAUSE) + elif code & 1 << 1 != 0: + keys.append(g15uinput.KEY_STOPCD) + elif code & 1 << 2 != 0: + keys.append(g15uinput.KEY_PREVIOUSSONG) + elif code & 1 << 3 != 0: + keys.append(g15uinput.KEY_NEXTSONG) + elif code & 1 << 4 != 0: + keys.append(g15uinput.KEY_MUTE) + elif code & 1 << 5 != 0: + keys.append(g15uinput.KEY_VOLUMEUP) + elif code & 1 << 6 != 0: + keys.append(g15uinput.KEY_VOLUMEDOWN) + elif code & 1 << 7 != 0: + # Hmm .. whats the proper mute key? There doesn't appear to be + # a headset mute, perhaps we should expose as a macro key + keys.append(g15uinput.KEY_SOUND) + elif code & 1 << 8 != 0: + keys.append(g15uinput.KEY_MICMUTE) + return keys + + def _convert_ext_g15daemon_code(self, code): + keys = [] + for key in EXT_REVERSE_KEY_MAP: + if code & key != 0: + keys.append(EXT_REVERSE_KEY_MAP[key]) + return keys + + def _convert_from_g15daemon_code(self, code): + keys = [] + for key in REVERSE_KEY_MAP: + if code & key != 0: + keys.append(REVERSE_KEY_MAP[key]) + return keys + + + def _handle_key_event(self, code, ext_code): + if not self.is_connected() or self.disconnecting: + return + logger.debug("Key code %d", code) + + has_js = ext_code & EXT_KEY_MAP[g15driver.G_KEY_JOY] > 0 + if has_js: + ext_code -= EXT_KEY_MAP[g15driver.G_KEY_JOY] + + this_keys = [] if code == 0 else self._convert_from_g15daemon_code(code) + if ext_code > 0 and self.get_model_name() == g15driver.MODEL_G510: + this_keys += self._get_g510_multimedia_keys(ext_code) + elif ext_code > 0: + this_keys += self._convert_ext_g15daemon_code(ext_code) + + if self.get_model_name() == g15driver.MODEL_G13: + c = self.analogue_calibration if self.joy_mode in [ g15uinput.JOYSTICK, g15uinput.MOUSE ] else self.digital_calibration + + low_val = g15uinput.JOYSTICK_CENTER - c + high_val = g15uinput.JOYSTICK_CENTER + c + max_step = 5 + + pos = pylibg15.get_joystick_position() + """ + The device itself gives us joystick position values between 0 and 255. + The center is at 128. + The virtual joysticks are set to give values between -127 and 127. + The center is at 0. + So we adapt the received values. + """ + pos = (pos[0] - g15uinput.DEVICE_JOYSTICK_CENTER, + pos[1] - g15uinput.DEVICE_JOYSTICK_CENTER) + + logger.debug("Joystick at %s", str(pos)) + + if self.joy_mode == g15uinput.JOYSTICK: + if has_js: + self._abs_joystick(this_keys, pos) + elif self.joy_mode == g15uinput.DIGITAL_JOYSTICK: + if has_js: + self._digital_joystick(this_keys, pos, low_val, high_val) + elif self.joy_mode == g15uinput.MOUSE: + if has_js: + self._rel_mouse(this_keys, pos, low_val, high_val, max_step) + else: + self._emit_macro_keys(this_keys, pos, low_val, high_val) + + up = [] + down = [] + + last_keys = self.last_keys + + for k in this_keys: + if last_keys is None or not k in last_keys: + down.append(k) + + """ + This is a work around for the G510. Sometimes the key up + events are lost, leaving keys stuck down in Gnome15. Instead, + we just react on key down and ignore the key up if one + actually occurs. + """ + # Work around for the G510 and the volume wheel missing key up events and audio input events + if isinstance(k, tuple) and self.get_model_name() == g15driver.MODEL_G510: + up.append(k) + this_keys.remove(k) + + if last_keys is not None: + for k in last_keys: + if not k in this_keys and not k in down and not k in up: + up.append(k) + + if ( ext_code > 0 ) and self.get_model_name() == g15driver.MODEL_G510: + self._do_macro_keys(self._filter_macro_keys(down), self._filter_macro_keys(up)) + self._do_uinput_keys(self._filter_uinput_keys(down), self._filter_uinput_keys(up)) + else: + self._do_macro_keys(down, up) + + self.last_keys = this_keys + + def has_joystick_key(self, keys): + for k in keys: + if k in [ g15driver.G_KEY_JOY, g15driver.G_KEY_JOY_CENTER, \ + g15driver.G_KEY_JOY_DOWN, g15driver.G_KEY_JOY_LEFT ]: + return True + + def _do_uinput_keys(self, down, up): + if len(down) > 0: + for uinput_code in down: + g15uinput.emit(g15uinput.KEYBOARD, uinput_code, 1, False) + g15uinput.syn(g15uinput.KEYBOARD) + if len(up) > 0: + for uinput_code in up: + g15uinput.emit(g15uinput.KEYBOARD, uinput_code, 0, False) + g15uinput.syn(g15uinput.KEYBOARD) + + def _do_macro_keys(self, down, up): + if len(down) > 0: + self.callback(down, g15driver.KEY_STATE_DOWN) + if len(up) > 0: + self.callback(up, g15driver.KEY_STATE_UP) + + def _filter_macro_keys(self, keys): + m = [] + for c in keys: + if isinstance(c, str): + m.append(c) + return m + + def _filter_uinput_keys(self, keys): + m = [] + for c in keys: + if isinstance(c, tuple): + m.append(c) + return m + + def _emit_macro_keys(self, this_keys, pos, low_val, high_val): + if pos[0] < low_val: + this_keys.append(g15driver.G_KEY_LEFT) + elif pos[0] > high_val: + this_keys.append(g15driver.G_KEY_RIGHT) + if pos[1] < low_val: + this_keys.append(g15driver.G_KEY_UP) + elif pos[1] > high_val: + this_keys.append(g15driver.G_KEY_DOWN) + + def _check_js_buttons(self, joystick_type, this_keys): + self._check_buttons(joystick_type, this_keys, g15driver.G_KEY_JOY_LEFT, g15uinput.BTN_1) + self._check_buttons(joystick_type, this_keys, g15driver.G_KEY_JOY_DOWN, g15uinput.BTN_2) + self._check_buttons(joystick_type, this_keys, g15driver.G_KEY_JOY_CENTER, g15uinput.BTN_3) + + def _check_mouse_buttons(self, this_keys): + self._check_buttons(g15uinput.MOUSE, this_keys, g15driver.G_KEY_JOY_LEFT, g15uinput.BTN_MOUSE) + self._check_buttons(g15uinput.MOUSE, this_keys, g15driver.G_KEY_JOY_DOWN, g15uinput.BTN_RIGHT) + self._check_buttons(g15uinput.MOUSE, this_keys, g15driver.G_KEY_JOY_CENTER, g15uinput.BTN_MIDDLE) + + def _rel_mouse(self, this_keys, pos, low_val, high_val, max_step): + self._check_mouse_buttons(this_keys) + + relx = 0 + rely = 0 + + if pos[0] < low_val: + relx = ( low_val - pos[0] ) * -1 + elif pos[0] > high_val: + relx = pos[0] - high_val + if pos[1] < low_val: + rely = ( low_val - pos[1] ) * -1 + elif pos[1] > high_val: + rely = pos[1] - high_val + + relx = -max_step if relx < -max_step else ( max_step if relx > max_step else relx) + rely = -max_step if rely < -max_step else ( max_step if rely > max_step else rely) + + self.move_x = relx + self.move_y = rely + if relx != 0 or rely != 0: + self._mouse_move() + else: + if self.timer is not None: + self.timer.cancel() + + def _abs_joystick(self, this_keys, pos): + self._check_js_buttons(g15uinput.JOYSTICK, this_keys) + g15uinput.emit(g15uinput.JOYSTICK, g15uinput.ABS_X, pos[0], syn=False) + g15uinput.emit(g15uinput.JOYSTICK, g15uinput.ABS_Y, pos[1]) + + def _digital_joystick(self, this_keys, pos, low_val, high_val): + self._check_js_buttons(g15uinput.DIGITAL_JOYSTICK, this_keys) + pos_x = g15uinput.JOYSTICK_CENTER + pos_y = g15uinput.JOYSTICK_CENTER + + if pos[0] < low_val: + pos_x = g15uinput.JOYSTICK_MIN + elif pos[0] > high_val: + pos_x = g15uinput.JOYSTICK_MAX + if pos[1] < low_val: + pos_y = g15uinput.JOYSTICK_MIN + elif pos[1] > high_val: + pos_y = g15uinput.JOYSTICK_MAX + + g15uinput.emit(g15uinput.DIGITAL_JOYSTICK, g15uinput.ABS_X, pos_x, syn=False) + g15uinput.emit(g15uinput.DIGITAL_JOYSTICK, g15uinput.ABS_Y, pos_y) + + def _check_buttons(self, target, this_keys, key, button): + if key in this_keys: + this_keys.remove(key) + if not key in self.down: + g15uinput.emit(target, button, 1) + self.down.append(key) + elif key in self.down: + g15uinput.emit(target, button, 0) + self.down.remove(key) + + def _mouse_move(self): + if self.move_x != 0 or self.move_y != 0: + if self.move_x != 0: + g15uinput.emit(g15uinput.MOUSE, g15uinput.REL_X, self.move_x) + if self.move_y != 0: + g15uinput.emit(g15uinput.MOUSE, g15uinput.REL_Y, self.move_y) + self.timer = g15scheduler.schedule("MouseMove", 0.05, self._mouse_move) + + def _do_update_control(self, control): + level = control.value + logger.debug("Updating control %s to %s", str(control.id), str(control.value)) + if control.id == backlight_control.id: + self.check_control(control) + pylibg15.set_keyboard_brightness(level) + elif control.id == lcd_backlight_control.id: + self.check_control(control) + pylibg15.set_lcd_brightness(level) + elif control.id == lcd_contrast_control.id: + self.check_control(control) + pylibg15.set_contrast(level) + elif control.id == color_backlight_control.id or control.id == red_blue_backlight_control.id: + pylibg15.set_keyboard_color(level) + elif control.id == mkeys_control.id: + pylibg15.set_leds(level) \ No newline at end of file diff --git a/src/gnome15/drivers/driver_g19direct.py b/src/gnome15/drivers/driver_g19direct.py new file mode 100644 index 0000000..5da0116 --- /dev/null +++ b/src/gnome15/drivers/driver_g19direct.py @@ -0,0 +1,367 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2011 Brett Smith +# +# 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 . + +""" +Alternative implementation of a G19 Driver that uses pylibg19 to communicate directly +with the keyboard +""" +import gnome15.g15locale as g15locale +_ = g15locale.get_translation("gnome15-drivers").ugettext + +from cStringIO import StringIO +from threading import RLock +import cairo +import gnome15.g15driver as g15driver +import gnome15.g15globals as g15globals +import gnome15.util.g15convert as g15convert +import gnome15.util.g15uigconf as g15uigconf +import gnome15.util.g15cairo as g15cairo +import gnome15.g15exceptions as g15exceptions +import sys +import os +import gconf +import gtk +import usb +import logging +import array +logger = logging.getLogger(__name__) + +# Import from local version of pylibg19 if available +if g15globals.dev: + sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "pylibg19")) + +from g19.g19 import G19 + + + +# Driver information (used by driver selection UI) +name=_("G19 Direct") +id="g19direct" +description=_("For use with the Logitech G19 only, this driver communicates directly, " + \ + "with the keyboard and so is more efficient than the G19D driver. Note, " + \ + "you will have to ensure the permissions of the USB devices are read/write " + \ + "for your user.") +has_preferences=True + +MAX_X=320 +MAX_Y=240 + +CLIENT_CMD_KB_BACKLIGHT = "BL" + +KEY_MAP = { + 0: g15driver.G_KEY_LIGHT, + 1: g15driver.G_KEY_M1, + 2: g15driver.G_KEY_M2, + 3: g15driver.G_KEY_M3, + 4: g15driver.G_KEY_MR, + 5: g15driver.G_KEY_G1, + 6: g15driver.G_KEY_G2, + 7: g15driver.G_KEY_G3, + 8: g15driver.G_KEY_G4, + 9: g15driver.G_KEY_G5, + 10: g15driver.G_KEY_G6, + 11: g15driver.G_KEY_G7, + 12: g15driver.G_KEY_G8, + 13: g15driver.G_KEY_G9, + 14: g15driver.G_KEY_G10, + 15: g15driver.G_KEY_G11, + 16: g15driver.G_KEY_G12, + 17: g15driver.G_KEY_BACK, + 18: g15driver.G_KEY_DOWN, + 19: g15driver.G_KEY_LEFT, + 20: g15driver.G_KEY_MENU, + 21: g15driver.G_KEY_OK, + 22: g15driver.G_KEY_RIGHT, + 23: g15driver.G_KEY_SETTINGS, + 24: g15driver.G_KEY_UP, + 25: g15driver.G_KEY_WINKEY_SWITCH, + 26: g15driver.G_KEY_NEXT, + 27: g15driver.G_KEY_PREV, + 28: g15driver.G_KEY_STOP, + 29: g15driver.G_KEY_PLAY, + 30: g15driver.G_KEY_MUTE, + 31: g15driver.G_KEY_VOL_UP, + 32: g15driver.G_KEY_VOL_DOWN + } + + +# Controls +mkeys_control = g15driver.Control("mkeys", _("Memory Bank Keys"), 0, 0, 15, hint=g15driver.HINT_MKEYS) +keyboard_backlight_control = g15driver.Control("backlight_colour", _("Keyboard Backlight Colour"), (0, 255, 0), hint = g15driver.HINT_DIMMABLE | g15driver.HINT_SHADEABLE) +default_keyboard_backlight_control = g15driver.Control("default_backlight_colour", _("Boot Keyboard Backlight Colour"), (0, 255, 0)) +lcd_brightness_control = g15driver.Control("lcd_brightness", _("LCD Brightness"), 100, 0, 100, hint = g15driver.HINT_SHADEABLE) +foreground_control = g15driver.Control("foreground", _("Default LCD Foreground"), (255, 255, 255), hint = g15driver.HINT_FOREGROUND | g15driver.HINT_VIRTUAL) +background_control = g15driver.Control("background", _("Default LCD Background"), (0, 0, 0), hint = g15driver.HINT_BACKGROUND | g15driver.HINT_VIRTUAL) +highlight_control = g15driver.Control("highlight", _("Default Highlight Color"), (255, 0, 0), hint=g15driver.HINT_HIGHLIGHT | g15driver.HINT_VIRTUAL) +controls = [ mkeys_control, keyboard_backlight_control, default_keyboard_backlight_control, lcd_brightness_control, foreground_control, background_control, highlight_control ] + +def show_preferences(device, parent, gconf_client): + prefs = G19DriverPreferences(device, parent, gconf_client) + prefs.run() + +class G19DriverPreferences(): + + def __init__(self, device, parent, gconf_client): + g15locale.get_translation("driver_g19direct") + widget_tree = gtk.Builder() + widget_tree.set_translation_domain("driver_g19direct") + widget_tree.add_from_file(os.path.join(g15globals.ui_dir, "driver_g19direct.ui")) + self.window = widget_tree.get_object("G19DirectDriverSettings") + self.window.set_transient_for(parent) + + g15uigconf.configure_checkbox_from_gconf(gconf_client, + "/apps/gnome15/%s/reset_usb" % device.uid, + "Reset", + False, + widget_tree, + True) + g15uigconf.configure_spinner_from_gconf(gconf_client, + "/apps/gnome15/%s/timeout" % device.uid, + "Timeout", + 10000, + widget_tree, + False) + g15uigconf.configure_spinner_from_gconf(gconf_client, + "/apps/gnome15/%s/reset_wait" % device.uid, + "ResetWait", + 0, + widget_tree, + False) + + def run(self): + self.window.run() + self.window.hide() + +class Driver(g15driver.AbstractDriver): + + def __init__(self, device, on_close = None): + g15driver.AbstractDriver.__init__(self, "g19direct") + self.on_close = on_close + self.device = device + self.lock = RLock() + self.connected = False + self.conf_client = gconf.client_get_default() + + def get_antialias(self): + return cairo.ANTIALIAS_SUBPIXEL + + def get_size(self): + return (MAX_X, MAX_Y) + + def get_bpp(self): + return self.device.bpp + + def get_controls(self): + return controls + + def get_key_layout(self): + return self.device.key_layout + + def get_action_keys(self): + return self.device.action_keys + + def process_svg(self, document): + pass + + def on_update_control(self, control): + self.lock.acquire() + try : + self._do_update_control(control) + finally: + self.lock.release() + + def get_name(self): + return name + + def get_model_names(self): + return [ g15driver.MODEL_G19 ] + + def get_model_name(self): + return self.device.model_id + + def grab_keyboard(self, callback): + self.callback = callback + self.lg19.start_event_handling() + + def is_connected(self): + return self.connected + + def paint(self, img): + if not self.is_connected(): + return + + width = img.get_width() + height = img.get_height() + + # Create a new flipped, rotated image. The G19 expects the image to scan vertically, but + # the cairo image surface will be horizontal. Rotating then flipping the image is the + # quickest way to convert this. 16 bit color (5-6-5) is also required. Unfortunately this format + # was disabled for a long time, as was only re-enabled in version 1.8.6. + try: + back_surface = cairo.ImageSurface (4, height, width) + except Exception as e: + logger.debug('Could not create ImageSurface. Trying earlier API.', exc_info = e) + # Earlier version of Cairo + back_surface = cairo.ImageSurface (cairo.FORMAT_ARGB32, height, width) + + back_context = cairo.Context (back_surface) + g15cairo.rotate_around_center(back_context, width, height, 270) + g15cairo.flip_horizontal(back_context, width, height) + back_context.set_source_surface(img, 0, 0) + back_context.set_operator (cairo.OPERATOR_SOURCE); + back_context.paint() + + if back_surface.get_format() == cairo.FORMAT_ARGB32: + file_str = StringIO() + data = back_surface.get_data() + for i in range(0, len(data), 4): + r = ord(data[i + 2]) + g = ord(data[i + 1]) + b = ord(data[i + 0]) + file_str.write(self._rgb_to_uint16(r, g, b)) + buf = array.array('B', file_str.getvalue()) + else: + buf = array.array('B', str(back_surface.get_data())) + + expected_size = MAX_X * MAX_Y * ( self.get_bpp() / 8 ) + if len(buf) != expected_size: + logger.warning("Invalid buffer size, expected %d, got %d", expected_size, len(buf)) + else: + try: + self.lg19.send_frame(buf) + except usb.USBError as e: + logger.debug("Failed to send buffer.", exc_info = e) + self._on_receive_error(e) + + def process_input(self, event): + if self.callback == None: + logger.debug("Ignoring key input, keyboard not grabbed") + return + + keys_down = event.keysDown + keys_up = event.keysUp + + logger.debug("Processing input, keys_down = %d, keys_up = %d", + len(keys_down), + len(keys_up)) + + if len(keys_up) > 0: + c = [] + for key in keys_up: + c.append(KEY_MAP[key]) + self.callback(c, g15driver.KEY_STATE_UP) + + if len(keys_down) > 0: + c = [] + for key in keys_down: + c.append(KEY_MAP[key]) + self.callback(c, g15driver.KEY_STATE_DOWN) + + """ + Private + """ + def _on_connect(self): + # Detect what version of pyusb we are using + pyusb = self._get_usb_lib_version() + + self.callback = None + + reset = self.conf_client.get_bool("/apps/gnome15/%s/reset_usb" % self.device.uid) + timeout = 10000 + reset_wait = 0 + e = self.conf_client.get("/apps/gnome15/%s/timeout" % self.device.uid) + if e: + timeout = e.get_int() + e = self.conf_client.get("/apps/gnome15/%s/reset_wait" % self.device.uid) + if e: + reset_wait = e.get_int() + if reset and pyusb == 1: + logger.warning("Using pyusb 1.0. Resetting device causes crash in this version, no reset will be done") + reset = False + + try: + self.lg19 = G19(reset, False, timeout, reset_wait) + self.connected = True + except usb.USBError as e: + logger.error("Failed to connect.", exc_info = e) + raise g15exceptions.NotConnectedException() + + # Start listening for keys + self.lg19.add_input_processor(self) + + for control in self.get_controls(): + self._do_update_control(control) + + def _on_disconnect(self): + if self.is_connected(): + self.lg19.close() + self.connected = False + if self.on_close != None: + self.on_close(self) + else: + raise Exception("Not connected") + + def _on_receive_error(self, exception): + if self.is_connected(): + self.disconnect() + + def _get_usb_lib_version(self): + try: + import usb.core + return 1 + except Exception as e: + logger.debug('pyusb version 1 not available.', exc_info = e) + return 0 + + def _set_mkey_lights(self, lights): + val = 0 + if lights & g15driver.MKEY_LIGHT_1 != 0: + val += 0x80 + if lights & g15driver.MKEY_LIGHT_2 != 0: + val += 0x40 + if lights & g15driver.MKEY_LIGHT_3 != 0: + val += 0x20 + if lights & g15driver.MKEY_LIGHT_MR != 0: + val += 0x10 + self.lg19.set_enabled_m_keys(val) + + def _do_update_control(self, control): + try: + if control == keyboard_backlight_control: + self.lg19.set_bg_color(control.value[0], control.value[1], control.value[2]) + elif control == default_keyboard_backlight_control: + self.lg19.save_default_bg_color(control.value[0], control.value[1], control.value[2]) + elif control == lcd_brightness_control: + self.lg19.set_display_brightness(control.value) + elif control == mkeys_control: + self._set_mkey_lights(control.value) + except usb.USBError as e: + logger.debug('Error updating control.', exc_info = e) + self._on_receive_error(e) + + def _rgb_to_uint16(self, r, g, b): + rBits = r * 32 / 255 + gBits = g * 64 / 255 + bBits = b * 32 / 255 + + rBits = rBits if rBits <= 31 else 31 + gBits = gBits if gBits <= 63 else 63 + bBits = bBits if bBits <= 31 else 31 + + valueH = (rBits << 3) | (gBits >> 3) + valueL = (gBits << 5) | bBits + + return chr(valueL & 0xff) + chr(valueH & 0xff) diff --git a/src/gnome15/drivers/driver_g930.py b/src/gnome15/drivers/driver_g930.py new file mode 100644 index 0000000..0ad9fa1 --- /dev/null +++ b/src/gnome15/drivers/driver_g930.py @@ -0,0 +1,291 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2012 Brett Smith +# +# 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 . + +import gnome15.g15locale as g15locale +_ = g15locale.get_translation("gnome15-drivers").ugettext + +from threading import Thread +from pyinputevent.pyinputevent import SimpleDevice + +import select +import pyinputevent.scancodes as S +import gnome15.g15driver as g15driver +import gnome15.util.g15scheduler as g15scheduler +import gnome15.util.g15uigconf as g15uigconf +import gnome15.g15globals as g15globals +import gnome15.g15uinput as g15uinput +import gconf +import fcntl +import os +import gtk +import cairo +import re +import usb + +# Logging +import logging +logger = logging.getLogger(__name__) + +# Driver information (used by driver selection UI) +id = "g930" +name = _("G930 Driver") +description = _("Simple driver that supports the keys on the G930/G35 headset. ") +has_preferences = True + + +""" +This dictionaries map the default codes emitted by the input system to the +Gnome15 codes. +""" +g930_key_map = { + S.KEY_PREVIOUSSONG : g15driver.G_KEY_G1, + S.KEY_PLAYPAUSE : g15driver.G_KEY_G2, + S.KEY_NEXTSONG : g15driver.G_KEY_G3, + S.KEY_MUTE : g15driver.G_KEY_MUTE, + S.KEY_VOLUMEDOWN : g15driver.G_KEY_VOL_DOWN, + S.KEY_VOLUMEUP : g15driver.G_KEY_VOL_UP + } + +# Other constants +EVIOCGRAB = 0x40044590 + +def show_preferences(device, parent, gconf_client): + prefs = G930DriverPreferences(device, parent, gconf_client) + prefs.run() + +class G930DriverPreferences(): + + def __init__(self, device, parent, gconf_client): + self.device = device + + widget_tree = gtk.Builder() + widget_tree.add_from_file(os.path.join(g15globals.ui_dir, "driver_g930.ui")) + self.window = widget_tree.get_object("G930DriverSettings") + self.window.set_transient_for(parent) + + self.grab_multimedia = widget_tree.get_object("GrabMultimedia") + g15uigconf.configure_checkbox_from_gconf(gconf_client, "/apps/gnome15/%s/grab_multimedia" % device.uid, "GrabMultimedia", False, widget_tree) + + def run(self): + self.window.run() + self.window.hide() + +class KeyboardReceiveThread(Thread): + def __init__(self, device): + Thread.__init__(self) + self._run = True + self.name = "KeyboardReceiveThread-%s" % device.uid + self.setDaemon(True) + self.devices = [] + + def deactivate(self): + self._run = False + for dev in self.devices: + logger.info("Ungrabbing %d", dev.fileno()) + try : + fcntl.ioctl(dev.fileno(), EVIOCGRAB, 0) + except Exception as e: + logger.info("Failed ungrab.", exc_info = e) + logger.info("Closing %d", dev.fileno()) + try : + self.fds[dev.fileno()].close() + except Exception as e: + logger.info("Failed close.", exc_info = e) + logger.info("Stopped %d", dev.fileno()) + logger.info("Stopped all input devices") + + def run(self): + self.poll = select.poll() + self.fds = {} + for dev in self.devices: + self.poll.register(dev, select.POLLIN | select.POLLPRI | select.POLLHUP | select.POLLNVAL | select.POLLERR) + fcntl.ioctl(dev.fileno(), EVIOCGRAB, 1) + self.fds[dev.fileno()] = dev + while self._run: + for x, e in self.poll.poll(1000): + dev = self.fds[x] + try : + if dev: + dev.read() + except OSError as e: + logger.debug('Could not read device file.', exc_info = e) + # Ignore this error if deactivated + if self._run: + raise e + logger.info("Thread left") + + +''' +Abstract input device +''' +class AbstractInputDevice(SimpleDevice): + def __init__(self, callback, key_map, *args, **kwargs): + SimpleDevice.__init__(self, *args, **kwargs) + self.callback = callback + self.key_map = key_map + + def _event(self, event_code, state): + if event_code in self.key_map: + key = self.key_map[event_code] + self.callback([key], state) + else: + logger.warning("Unmapped key for event: %s", event_code) + +''' +SimpleDevice implementation for handling multi-media keys. +''' +class MultiMediaDevice(AbstractInputDevice): + def __init__(self, grab_multimedia, callback, *args, **kwargs): + AbstractInputDevice.__init__(self, callback, g930_key_map, *args, **kwargs) + self._grab_multimedia = grab_multimedia + + def receive(self, event): + if event.etype == S.EV_KEY: + state = g15driver.KEY_STATE_DOWN if event.evalue == 1 else g15driver.KEY_STATE_UP + if event.evalue != 2: + self._event(event.ecode, state) + elif event.etype == 0: + return + elif event.etype == 4 and event.evalue == 786666: + # Hack for Volume down on G930 + if not self._grab_multimedia: + g15uinput.emit(g15uinput.KEYBOARD, g15uinput.KEY_VOLUMEDOWN, 1, True) + g15uinput.emit(g15uinput.KEYBOARD, g15uinput.KEY_VOLUMEDOWN, 0, True) + elif event.etype == 4 and event.evalue == 786665: + # Hack for Volume down on G930 + if not self._grab_multimedia: + g15uinput.emit(g15uinput.KEYBOARD, g15uinput.KEY_VOLUMEUP, 1, True) + g15uinput.emit(g15uinput.KEYBOARD, g15uinput.KEY_VOLUMEUP, 0, True) + else: + logger.warning("Unhandled event: %s", str(event)) + +class Driver(g15driver.AbstractDriver): + + def __init__(self, device, on_close=None): + g15driver.AbstractDriver.__init__(self, "g510") + self.notify_handles = [] + self.on_close = on_close + self.key_thread = None + self.device = device + self.connected = False + self.conf_client = gconf.client_get_default() + self._init_device() + self.notify_handles.append(self.conf_client.notify_add("/apps/gnome15/%s/grab_multimedia" % self.device.uid, self._config_changed, None)) + + def get_antialias(self): + return cairo.ANTIALIAS_NONE + + def is_connected(self): + return self.connected + + def get_model_names(self): + return [ g15driver.MODEL_G930, g15driver.MODEL_G35 ] + + def get_name(self): + return "Gnome15 G930/G35 Driver" + + def get_model_name(self): + return self.device.model_id if self.device != None else None + + def get_action_keys(self): + return self.device.action_keys + + def get_key_layout(self): + if self.grab_multimedia: + l = list(self.device.key_layout) + l.append([]) + l.append([ g15driver.G_KEY_VOL_UP, g15driver.G_KEY_VOL_DOWN, g15driver.G_KEY_MUTE ]) + return l + else: + return self.device.key_layout + + def _load_configuration(self): + self.grab_multimedia = self.conf_client.get_bool("/apps/gnome15/%s/grab_multimedia" % self.device.uid) + + def _config_changed(self, client, connection_id, entry, args): + self._reload_and_reconnect() + + def get_size(self): + return self.device.lcd_size + + def get_bpp(self): + return self.device.bpp + + def get_controls(self): + return [] + + def paint(self, img): + pass + + def on_update_control(self, control): + pass + + def grab_keyboard(self, callback): + if self.key_thread != None: + raise Exception("Keyboard already grabbed") + + self.key_thread = KeyboardReceiveThread(self.device) + for devpath in self.mm_devices: + logger.info("Adding input multi-media device %s", devpath) + self.key_thread.devices.append(MultiMediaDevice(self.grab_multimedia, callback, devpath, devpath)) + + self.key_thread.start() + + ''' + Private + ''' + def _on_disconnect(self): + if not self.is_connected(): + raise Exception("Not connected") + self._stop_receiving_keys() + if self.on_close != None: + g15scheduler.schedule("Close", 0, self.on_close, self) + + def _on_connect(self): + self.notify_handles = [] + self._init_driver() + if not self.device: + raise usb.USBError("No supported logitech headphones found on USB bus") + if self.device == None: + raise usb.USBError("WARNING: Found no " + self.model + " Logitech headphone, Giving up") + + def _reload_and_reconnect(self): + self._load_configuration() + if self.is_connected(): + self.disconnect() + + def _stop_receiving_keys(self): + if self.key_thread != None: + self.key_thread.deactivate() + self.key_thread = None + + def _init_device(self): + self._load_configuration() + self.device_name = None + + def _init_driver(self): + self._init_device() + self.mm_devices = [] + dir_path = "/dev/input/by-id" + for p in os.listdir(dir_path): + # TODO - not sure about the G35 - feedback needed + if re.search(r"usb-Logitech_Logitech_G930_Headset-event-if.*", p) or re.search(r"usb-Logitech_Logitech_G35_Headset-event-if.*", p): + logger.info("Input multi-media device %s matches", p) + self.mm_devices.append(dir_path + "/" + p) + + def __del__(self): + for h in self.notify_handles: + self.conf_client.notify_remove(h) \ No newline at end of file diff --git a/src/gnome15/drivers/driver_gtk.py b/src/gnome15/drivers/driver_gtk.py new file mode 100644 index 0000000..6fcd337 --- /dev/null +++ b/src/gnome15/drivers/driver_gtk.py @@ -0,0 +1,428 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2010 Brett Smith +# +# 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 . + +import gnome15.g15locale as g15locale +_ = g15locale.get_translation("gnome15-drivers").ugettext + +import gnome15.g15driver as g15driver +import gnome15.util.g15uigconf as g15uigconf +import gnome15.util.g15cairo as g15cairo +import gnome15.util.g15icontools as g15icontools +import gnome15.g15globals as g15globals + +import gconf + +import os +import gtk.gdk +import gobject +import cairo + +from PIL import Image +from PIL import ImageMath +import logging +logger = logging.getLogger(__name__) + +# Driver information (used by driver selection UI) +id="gtk" +name=_("GTK Virtual Keyboard Driver") +description=_("A special development driver that emulates all supported, " + \ + "models as a window on your desktop. This allows " + \ + "you to develop plugins without having access to a real Logitech hardward ") +has_preferences=True + +# Controls + +g19_mkeys_control = g15driver.Control("mkeys", _("Memory Bank Keys"), 0, 0, 15, hint=g15driver.HINT_MKEYS) +g19_keyboard_backlight_control = g15driver.Control("backlight_colour", _("Keyboard Backlight Colour"), (0, 255, 0), hint = g15driver.HINT_DIMMABLE | g15driver.HINT_SHADEABLE) +g19_lcd_brightness_control = g15driver.Control("lcd_brightness", _("LCD Brightness"), 100, 0, 100, hint = g15driver.HINT_SHADEABLE) +g19_foreground_control = g15driver.Control("foreground", _("Default LCD Foreground"), (255, 255, 255), hint = g15driver.HINT_FOREGROUND | g15driver.HINT_VIRTUAL) +g19_background_control = g15driver.Control("background", _("Default LCD Background"), (0, 0, 0), hint = g15driver.HINT_BACKGROUND | g15driver.HINT_VIRTUAL) +g19_highlight_control = g15driver.Control("highlight", _("Default Highlight Color"), (255, 0, 0), hint=g15driver.HINT_HIGHLIGHT | g15driver.HINT_VIRTUAL) + +g15_mkeys_control = g15driver.Control("mkeys", _("Memory Bank Keys"), 1, 0, 15, hint=g15driver.HINT_MKEYS) +g15_backlight_control = g15driver.Control("keyboard_backlight", _("Keyboard Backlight Level"), 2, 0, 2, hint = g15driver.HINT_DIMMABLE | g15driver.HINT_SHADEABLE) +g15_invert_control = g15driver.Control("invert_lcd", _("Invert LCD"), 0, 0, 1, hint = g15driver.HINT_SWITCH ) + +g110_keyboard_backlight_control = g15driver.Control("backlight_colour", _("Keyboard Backlight Colour"), (255, 0, 0), hint = g15driver.HINT_DIMMABLE | g15driver.HINT_SHADEABLE | g15driver.HINT_RED_BLUE_LED) + +controls = { + g15driver.MODEL_G11 : [ g15_mkeys_control, g15_backlight_control ], + g15driver.MODEL_G19 : [ g19_mkeys_control, g19_keyboard_backlight_control, g19_lcd_brightness_control, g19_foreground_control, g19_background_control, g19_highlight_control ], + g15driver.MODEL_G15_V1 : [ g15_mkeys_control, g15_backlight_control, g15_invert_control ], + g15driver.MODEL_G15_V2 : [ g15_mkeys_control, g15_backlight_control, g15_invert_control ], + g15driver.MODEL_G13 : [ g15_mkeys_control, g15_backlight_control, g15_invert_control ], + g15driver.MODEL_G510 : [ g15_mkeys_control, g19_keyboard_backlight_control, g15_invert_control ], + g15driver.MODEL_Z10 : [ g15_backlight_control, g15_invert_control ], + g15driver.MODEL_GAMEPANEL : [ g15_backlight_control, g15_invert_control ], + g15driver.MODEL_G110 : [ g19_mkeys_control, g110_keyboard_backlight_control ], + g15driver.MODEL_MX5500 : [ g15_invert_control ], + g15driver.MODEL_G930 : [ ], + g15driver.MODEL_G35 : [ ], + } + +def show_preferences(device, parent, gconf_client): + if device.model_id != 'virtual': + return + + prefs = GtkDriverPreferences(device, parent, gconf_client) + prefs.run() + +class GtkDriverPreferences(): + + def __init__(self, device, parent, gconf_client): + g15locale.get_translation("driver_gtk") + widget_tree = gtk.Builder() + widget_tree.set_translation_domain("driver_gtk") + widget_tree.add_from_file(os.path.join(g15globals.ui_dir, "driver_gtk.ui")) + self.window = widget_tree.get_object("GtkDriverSettings") + self.window.set_transient_for(parent) + + mode_model = widget_tree.get_object("ModeModel") + mode_model.clear() + for mode in g15driver.MODELS: + mode_model.append([mode]) + g15uigconf.configure_combo_from_gconf(gconf_client, + "/apps/gnome15/%s/gtk_mode" % device.uid, + "ModeCombo", + g15driver.MODEL_G19, + widget_tree) + + def run(self): + self.window.run() + self.window.hide() + +class Driver(g15driver.AbstractDriver): + + def __init__(self, device, on_close = None): + g15driver.AbstractDriver.__init__(self, "gtk") + self.lights = 0 + self.main_window = None + self.connected = False + self.bpp = device.bpp + self.lcd_size = device.lcd_size + self.callback = None + self.action_keys = None + self.device = device + self.area = None + self.image = None + self.buttons = {} + self.event_box = None + self.on_close = on_close + self.conf_client = gconf.client_get_default() + self.notify_handle = self.conf_client.notify_add("/apps/gnome15/%s/gtk_mode" % self.device.uid, self.config_changed) + self._init_driver() + + def get_antialias(self): + if self.mode == g15driver.MODEL_G19: + return cairo.ANTIALIAS_DEFAULT + else: + return cairo.ANTIALIAS_NONE + + def config_changed(self, client, connection_id, entry, args): + self._init_driver() + if self.on_driver_options_change: + self.on_driver_options_change() + + def is_connected(self): + return self.connected + + def get_model_names(self): + return [ 'virtual' ] + + def get_name(self): + return _("GTK Keyboard Emulator Driver") + + def get_model_name(self): + return self.mode + + def get_action_keys(self): + return self.action_keys + + def get_key_layout(self): + return self.key_layout + + def get_zoomed_size(self): + zoom = self.get_zoom() + return ( self.lcd_size[0] * zoom, self.lcd_size[1] * zoom ) + + def get_zoom(self): + if self.bpp == 16: + return 1 + else: + return 3 + + def get_size(self): + return self.lcd_size + + def get_bpp(self): + return self.bpp + + def get_controls(self): + return self.controls + + def paint(self, image): + + if self.bpp != 0: + width = self.lcd_size[0] + height = self.lcd_size[1] + + if self.bpp == 1: + # Paint to 565 image provided into an ARGB image surface for PIL's benefit. PIL doesn't support 565? + argb_surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, width, height) + argb_context = cairo.Context(argb_surface) + argb_context.set_source_surface(image) + argb_context.paint() + + # Now convert the ARGB to a PIL image so it can be converted to a 1 bit monochrome image, with all + # colours dithered. It would be nice if Cairo could do this :( Any suggestions? + pil_img = Image.frombuffer("RGBA", self.lcd_size, argb_surface.get_data(), "raw", "RGBA", 0, 1) + pil_img = ImageMath.eval("convert(pil_img,'1')",pil_img=pil_img) + pil_img = ImageMath.eval("convert(pil_img,'P')",pil_img=pil_img) + pil_img = pil_img.point(lambda i: i >= 250,'1') + + invert_control = self.get_control("invert_lcd") + if invert_control and invert_control.value == 1: + pil_img = pil_img.point(lambda i: 1^i) + + # Create drawable message + pil_img = pil_img.convert("RGB") + self.image = pil_img + else: + # Take a copy of the image to prevent flickering + argb_surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, width, height) + argb_context = cairo.Context(argb_surface) + argb_context.set_source_surface(image) + argb_context.paint() + self.image = argb_surface + gobject.timeout_add(0, self.redraw) + + def process_svg(self, document): + if self.bpp == 1: + for element in document.getroot().iter(): + style = element.get("style") + if style != None: + element.set("style", style.replace("font-family:Sans","font-family:%s" % g15globals.fixed_size_font_name)) + + def redraw(self): + if self.image != None and self.main_window is not None: + if isinstance(self.image, cairo.Surface): + self._draw_surface() + else: + self._draw_pixbuf() + self.area.queue_draw() + + def on_update_control(self, control): + gobject.idle_add(self._do_update_control, control) + + def grab_keyboard(self, callback): + self.callback = callback; + + ''' + Private + ''' + def _on_connect(self): + self._init_driver() + logger.info("Starting GTK driver") + gobject.idle_add(self._init_ui) + + def _on_disconnect(self): + logger.info("Disconnecting GTK driver") + if not self.is_connected(): + raise Exception("Not connected") + self.connected = False + if self.on_close != None: + self.on_close(self, retry=False) + gobject.idle_add(self._close_window) + + def _simulate_key(self, widget, key, state): + if self.callback != None: + keys = [] + keys.append(key) + self.callback(keys, state) + + def _do_update_control(self, control): + if self.connected: + if control == self.get_control_for_hint(g15driver.HINT_MKEYS): + self._do_set_mkey_lights() + elif control == self.get_control_for_hint(g15driver.HINT_DIMMABLE): + if isinstance(control.value, int): + v = ( 65535 / control.upper ) * control.value + self.event_box.modify_bg(gtk.STATE_NORMAL, gtk.gdk.Color(v, v, v)) + else: + self.event_box.modify_bg(gtk.STATE_NORMAL, gtk.gdk.Color(control.value[0] << 8, control.value[1] << 8, control.value[2] << 8)) + + def _window_closed(self, window, evt): + if self.main_window != None: + self.conf_client.set_bool("/apps/gnome15/%s/enabled" % self.device.uid, False) + + def _do_set_mkey_lights(self): + c = self.get_control_for_hint(g15driver.HINT_MKEYS) + if c is not None and c.value is not None: + if g15driver.G_KEY_M1 in self.buttons: + self._modify_button(g15driver.G_KEY_M1, c.value, g15driver.MKEY_LIGHT_1) + if g15driver.G_KEY_M2 in self.buttons: + self._modify_button(g15driver.G_KEY_M2, c.value, g15driver.MKEY_LIGHT_2) + if g15driver.G_KEY_M3 in self.buttons: + self._modify_button(g15driver.G_KEY_M3, c.value, g15driver.MKEY_LIGHT_3) + if g15driver.G_KEY_MR in self.buttons: + self._modify_button(g15driver.G_KEY_MR, c.value, g15driver.MKEY_LIGHT_MR) + + def _modify_button(self, id, lights, mask): + on = lights & mask != 0 + c = self.buttons[id] + key_text = " ".join(g15driver.get_key_names(list(id))) + c.set_label("*%s" % key_text if on else "%s" % key_text) + + def _close_window(self): + if self.main_window != None: + w = self.main_window + self.main_window = None + w.hide() + w.destroy() + self.area = None + + def _mode_changed(self, client, connection_id, entry, args): + if self.is_connected(): + gobject.idle_add(self.disconnect) + else: + logger.warning("Mode change would cause disconnect when already connected. %s", + str(entry)) + + def _draw_surface(self): + # Finally paint the Cairo surface on the GTK widget + zoom = self.get_zoom() + width = self.lcd_size[0] + height = self.lcd_size[1] + if self.area != None and self.area.window != None: + surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, zoom * width, zoom * height) + context = cairo.Context(surface) + context.set_antialias(self.get_antialias()) + context.scale(zoom, zoom) + context.set_source_surface(self.image) + context.paint() + self.area.set_surface(surface) + + def _draw_pixbuf(self): + width = self.lcd_size[0] + height = self.lcd_size[1] + zoom = self.get_zoom() + pixbuf = g15cairo.image_to_pixbuf(self.image) + pixbuf = pixbuf.scale_simple(zoom * width, zoom * height, 0) + if self.area != None: + self.area.set_pixbuf(pixbuf) + + def _init_driver(self): + logger.info("Initialising GTK driver") + if self.device.model_id == 'virtual': + self.mode = self.conf_client.get_string("/apps/gnome15/%s/gtk_mode" % self.device.uid) + else: + self.mode = self.device.model_id + if self.mode == None or self.mode == "": + self.mode = g15driver.MODEL_G19 + logger.info("Mode is now %s", self.mode) + self.controls = controls[self.mode] + import gnome15.g15devices as g15devices + device_info = g15devices.get_device_info(self.mode) + self.bpp = device_info.bpp + self.action_keys = device_info.action_keys + self.lcd_size = device_info.lcd_size + self.key_layout = device_info.key_layout + logger.info("Initialised GTK driver") + + def _init_ui(self): + logger.info("Initialising GTK UI") + self.area = VirtualLCD(self) + #self.area.connect("expose_event", self._expose) + self.hboxes = [] + self.buttons = {} + zoomed_size = self.get_zoomed_size() + self.area.set_size_request(zoomed_size[0], zoomed_size[1]) + self.vbox = gtk.VBox () + self.vbox.add(self.area) + rows = gtk.VBox() + for row in self.get_key_layout(): + hbox = gtk.HBox() + for key in row: + key_text = " ".join(g15driver.get_key_names(list(key))) + g_button = gtk.Button(key_text) + g_button.connect("pressed", self._simulate_key, key, g15driver.KEY_STATE_DOWN) + g_button.connect("released", self._simulate_key, key, g15driver.KEY_STATE_UP) + hbox.add(g_button) + self.buttons[key] = g_button + rows.add(hbox) + + self.event_box = gtk.EventBox() + self.event_box.add(rows) + self.vbox.add(self.event_box) + + self.main_window = gtk.Window(gtk.WINDOW_TOPLEVEL) + self.main_window.set_title("Gnome15") + self.main_window.set_icon_from_file(g15icontools.get_app_icon(self.conf_client, "gnome15")) + self.main_window.add(self.vbox) + self.main_window.connect("delete-event", self._window_closed) + + control = self.get_control_for_hint(g15driver.HINT_DIMMABLE) + if control: + if isinstance(control.value, int): + v = ( 65535 / control.upper ) * control.value + self.event_box.modify_bg(gtk.STATE_NORMAL, gtk.gdk.Color(v, v, v)) + else: + self.event_box.modify_bg(gtk.STATE_NORMAL, gtk.gdk.Color(control.value[0] << 8, control.value[1] << 8, control.value[2] << 8)) + + self.main_window.show_all() + logger.info("Initialised GTK UI") + self.connected = True + logger.info("Connected") + + def __del__(self): + self.conf_client.notify_remove(self.notify_handle) + +class VirtualLCD(gtk.DrawingArea): + + def __init__(self, driver): + self.__gobject_init__() + self.driver = driver + self.set_double_buffered(True) + super(VirtualLCD, self).__init__() + self.connect("expose-event", self._expose) + self.buffer = None + + def _expose(self, widget, event): + if not self.driver.is_connected(): + return + cr = widget.window.cairo_create() + cr.rectangle(event.area.x, event.area.y, + event.area.width, event.area.height) + cr.clip() + + # Paint + if self.buffer: + cr.set_source_surface(self.buffer) + cr.paint() + + def set_pixbuf(self, pixbuf): + self.buffer = g15cairo.pixbuf_to_surface(pixbuf) + + def set_surface(self, surface): + self.buffer = surface +# self.window.begin_paint_rect((0, 0, zoom * width, zoom * height)) +# context = self.window.cairo_create() +# context.set_source_surface(surface) +# context.paint() +# self.window.end_paint() + +gobject.type_register(VirtualLCD) + diff --git a/src/gnome15/drivers/driver_kernel.py b/src/gnome15/drivers/driver_kernel.py new file mode 100644 index 0000000..dfd6212 --- /dev/null +++ b/src/gnome15/drivers/driver_kernel.py @@ -0,0 +1,1432 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2010 Brett Smith +# +# 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 . + +import gnome15.g15locale as g15locale +_ = g15locale.get_translation("gnome15-drivers").ugettext + +from cStringIO import StringIO +from pyinputevent.uinput import UInputDevice +from pyinputevent.pyinputevent import InputEvent, SimpleDevice +from pyinputevent.keytrans import * +from threading import Thread + +import select +import pyinputevent.scancodes as S +import gnome15.g15driver as g15driver +import gnome15.util.g15scheduler as g15scheduler +import gnome15.util.g15uigconf as g15uigconf +import gnome15.g15globals as g15globals +import gnome15.g15uinput as g15uinput +import gconf +import fcntl +import os +import gtk +import cairo +import re +import usb +import fb +from PIL import Image +from PIL import ImageMath +import array +import struct +import dbus +import gobject + +# Logging +import logging +logger = logging.getLogger(__name__) + +# Driver information (used by driver selection UI) +id = "kernel" +name = _("Kernel Drivers") +description = _("Requires ali123's Logitech Kernel drivers. This method requires no other \ +daemons to be running, and works with the G13, G15, G19 and G110 keyboards. ") +has_preferences = True + + +""" +This dictionaries map the default codes emitted by the input system to the +Gnome15 codes. +""" +g19_key_map = { + S.KEY_PROG1 : g15driver.G_KEY_M1, + S.KEY_PROG2 : g15driver.G_KEY_M2, + S.KEY_PROG3 : g15driver.G_KEY_M3, + S.KEY_RECORD : g15driver.G_KEY_MR, + S.KEY_MENU : g15driver.G_KEY_MENU, + S.KEY_UP : g15driver.G_KEY_UP, + S.KEY_DOWN : g15driver.G_KEY_DOWN, + S.KEY_LEFT : g15driver.G_KEY_LEFT, + S.KEY_RIGHT : g15driver.G_KEY_RIGHT, + S.KEY_OK : g15driver.G_KEY_OK, + S.KEY_BACK : g15driver.G_KEY_BACK, + S.KEY_FORWARD : g15driver.G_KEY_SETTINGS, + 228 : g15driver.G_KEY_LIGHT, + S.KEY_F1 : g15driver.G_KEY_G1, + S.KEY_F2 : g15driver.G_KEY_G2, + S.KEY_F3 : g15driver.G_KEY_G3, + S.KEY_F4 : g15driver.G_KEY_G4, + S.KEY_F5 : g15driver.G_KEY_G5, + S.KEY_F6 : g15driver.G_KEY_G6, + S.KEY_F7 : g15driver.G_KEY_G7, + S.KEY_F8 : g15driver.G_KEY_G8, + S.KEY_F9 : g15driver.G_KEY_G9, + S.KEY_F10 : g15driver.G_KEY_G10, + S.KEY_F11 : g15driver.G_KEY_G11, + S.KEY_F12 : g15driver.G_KEY_G12, + S.KEY_MUTE : g15driver.G_KEY_MUTE, + S.KEY_VOLUMEDOWN : g15driver.G_KEY_VOL_DOWN, + S.KEY_VOLUMEUP : g15driver.G_KEY_VOL_UP, + S.KEY_NEXTSONG : g15driver.G_KEY_NEXT, + S.KEY_PREVIOUSSONG : g15driver.G_KEY_PREV, + S.KEY_PLAYPAUSE : g15driver.G_KEY_PLAY, + S.KEY_STOPCD : g15driver.G_KEY_STOP, + } +g15_key_map = { + S.KEY_PROG1 : g15driver.G_KEY_M1, + S.KEY_PROG2 : g15driver.G_KEY_M2, + S.KEY_PROG3 : g15driver.G_KEY_M3, + S.KEY_RECORD : g15driver.G_KEY_MR, + S.KEY_OK : g15driver.G_KEY_L1, + S.KEY_LEFT : g15driver.G_KEY_L2, + S.KEY_UP : g15driver.G_KEY_L3, + S.KEY_DOWN : g15driver.G_KEY_L4, + S.KEY_RIGHT : g15driver.G_KEY_L5, + 228 : g15driver.G_KEY_LIGHT, + S.KEY_F1 : g15driver.G_KEY_G1, + S.KEY_F2 : g15driver.G_KEY_G2, + S.KEY_F3 : g15driver.G_KEY_G3, + S.KEY_F4 : g15driver.G_KEY_G4, + S.KEY_F5 : g15driver.G_KEY_G5, + S.KEY_F6 : g15driver.G_KEY_G6, + S.KEY_F7 : g15driver.G_KEY_G7, + S.KEY_F8 : g15driver.G_KEY_G8, + S.KEY_F9 : g15driver.G_KEY_G9, + S.KEY_F10 : g15driver.G_KEY_G10, + S.KEY_F11 : g15driver.G_KEY_G11, + S.KEY_F12 : g15driver.G_KEY_G12, + S.KEY_F13 : g15driver.G_KEY_G13, + S.KEY_F14 : g15driver.G_KEY_G14, + S.KEY_F15 : g15driver.G_KEY_G15, + S.KEY_F16 : g15driver.G_KEY_G16, + S.KEY_F17 : g15driver.G_KEY_G17, + S.KEY_F18 : g15driver.G_KEY_G18 + } + +g15v2_key_map = { + S.KEY_PROG1 : g15driver.G_KEY_M1, + S.KEY_PROG2 : g15driver.G_KEY_M2, + S.KEY_PROG3 : g15driver.G_KEY_M3, + S.KEY_RECORD : g15driver.G_KEY_MR, + S.KEY_OK : g15driver.G_KEY_L1, + S.KEY_LEFT : g15driver.G_KEY_L2, + S.KEY_UP : g15driver.G_KEY_L3, + S.KEY_DOWN : g15driver.G_KEY_L4, + S.KEY_RIGHT : g15driver.G_KEY_L5, + 228 : g15driver.G_KEY_LIGHT, + S.KEY_F1 : g15driver.G_KEY_G1, + S.KEY_F2 : g15driver.G_KEY_G2, + S.KEY_F3 : g15driver.G_KEY_G3, + S.KEY_F4 : g15driver.G_KEY_G4, + S.KEY_F5 : g15driver.G_KEY_G5, + S.KEY_F6 : g15driver.G_KEY_G6 + } +g13_key_map = { + S.KEY_PROG1 : g15driver.G_KEY_M1, + S.KEY_PROG2 : g15driver.G_KEY_M2, + S.KEY_PROG3 : g15driver.G_KEY_M3, + S.KEY_RECORD : g15driver.G_KEY_MR, + S.KEY_OK : g15driver.G_KEY_L1, + S.KEY_LEFT : g15driver.G_KEY_L2, + S.KEY_UP : g15driver.G_KEY_L3, + S.KEY_DOWN : g15driver.G_KEY_L4, + S.KEY_RIGHT : g15driver.G_KEY_L5, + 228 : g15driver.G_KEY_LIGHT, + S.KEY_F1 : g15driver.G_KEY_G1, + S.KEY_F2 : g15driver.G_KEY_G2, + S.KEY_F3 : g15driver.G_KEY_G3, + S.KEY_F4 : g15driver.G_KEY_G4, + S.KEY_F5 : g15driver.G_KEY_G5, + S.KEY_F6 : g15driver.G_KEY_G6, + S.KEY_F7 : g15driver.G_KEY_G7, + S.KEY_F8 : g15driver.G_KEY_G8, + S.KEY_F9 : g15driver.G_KEY_G9, + S.KEY_F10 : g15driver.G_KEY_G10, + S.KEY_F11 : g15driver.G_KEY_G11, + S.KEY_F12 : g15driver.G_KEY_G12, + S.KEY_F13 : g15driver.G_KEY_G13, + S.KEY_F14 : g15driver.G_KEY_G14, + S.KEY_F15 : g15driver.G_KEY_G15, + S.KEY_F16 : g15driver.G_KEY_G16, + S.KEY_F17 : g15driver.G_KEY_G17, + S.KEY_F18 : g15driver.G_KEY_G18, + S.KEY_F19 : g15driver.G_KEY_G19, + S.KEY_F20 : g15driver.G_KEY_G20, + S.KEY_F21 : g15driver.G_KEY_G21, + S.KEY_F22 : g15driver.G_KEY_G22, + S.BTN_X: g15driver.G_KEY_JOY_LEFT, + S.BTN_Y: g15driver.G_KEY_JOY_DOWN, + S.BTN_Z: g15driver.G_KEY_JOY_CENTER, + } +g110_key_map = { + S.KEY_PROG1 : g15driver.G_KEY_M1, + S.KEY_PROG2 : g15driver.G_KEY_M2, + S.KEY_PROG3 : g15driver.G_KEY_M3, + S.KEY_RECORD : g15driver.G_KEY_MR, + 228 : g15driver.G_KEY_LIGHT, + S.KEY_F1 : g15driver.G_KEY_G1, + S.KEY_F2 : g15driver.G_KEY_G2, + S.KEY_F3 : g15driver.G_KEY_G3, + S.KEY_F4 : g15driver.G_KEY_G4, + S.KEY_F5 : g15driver.G_KEY_G5, + S.KEY_F6 : g15driver.G_KEY_G6, + S.KEY_F7 : g15driver.G_KEY_G7, + S.KEY_F8 : g15driver.G_KEY_G8, + S.KEY_F9 : g15driver.G_KEY_G9, + S.KEY_F10 : g15driver.G_KEY_G10, + S.KEY_F11 : g15driver.G_KEY_G11, + S.KEY_F12 : g15driver.G_KEY_G12 + } +g510_key_map = { + S.KEY_PROG1 : g15driver.G_KEY_M1, + S.KEY_PROG2 : g15driver.G_KEY_M2, + S.KEY_PROG3 : g15driver.G_KEY_M3, + S.KEY_RECORD : g15driver.G_KEY_MR, + S.KEY_OK : g15driver.G_KEY_L1, + S.KEY_LEFT : g15driver.G_KEY_L2, + S.KEY_UP : g15driver.G_KEY_L3, + S.KEY_DOWN : g15driver.G_KEY_L4, + S.KEY_RIGHT : g15driver.G_KEY_L5, + 228 : g15driver.G_KEY_LIGHT, + S.KEY_F1 : g15driver.G_KEY_G1, + S.KEY_F2 : g15driver.G_KEY_G2, + S.KEY_F3 : g15driver.G_KEY_G3, + S.KEY_F4 : g15driver.G_KEY_G4, + S.KEY_F5 : g15driver.G_KEY_G5, + S.KEY_F6 : g15driver.G_KEY_G6, + S.KEY_F7 : g15driver.G_KEY_G7, + S.KEY_F8 : g15driver.G_KEY_G8, + S.KEY_F9 : g15driver.G_KEY_G9, + S.KEY_F10 : g15driver.G_KEY_G10, + S.KEY_F11 : g15driver.G_KEY_G11, + S.KEY_F12 : g15driver.G_KEY_G12, + S.KEY_F13 : g15driver.G_KEY_G13, + S.KEY_F14 : g15driver.G_KEY_G14, + S.KEY_F15 : g15driver.G_KEY_G15, + S.KEY_F16 : g15driver.G_KEY_G16, + S.KEY_F17 : g15driver.G_KEY_G17, + S.KEY_F18 : g15driver.G_KEY_G18 + } + +g19_mkeys_control = g15driver.Control("mkeys", _("Memory Bank Keys"), 0, 0, 15, hint=g15driver.HINT_MKEYS) +g19_keyboard_backlight_control = g15driver.Control("backlight_colour", _("Keyboard Backlight Colour"), (0, 255, 0), hint=g15driver.HINT_DIMMABLE | g15driver.HINT_SHADEABLE) + +g19_brightness_control = g15driver.Control("lcd_brightness", _("LCD Brightness"), 100, 0, 100, hint = g15driver.HINT_SHADEABLE) +g19_foreground_control = g15driver.Control("foreground", _("Default LCD Foreground"), (255, 255, 255), hint=g15driver.HINT_FOREGROUND | g15driver.HINT_VIRTUAL) +g19_background_control = g15driver.Control("background", _("Default LCD Background"), (0, 0, 0), hint=g15driver.HINT_BACKGROUND | g15driver.HINT_VIRTUAL) +g19_highlight_control = g15driver.Control("highlight", _("Default Highlight Color"), (255, 0, 0), hint=g15driver.HINT_HIGHLIGHT | g15driver.HINT_VIRTUAL) +g19_controls = [ g19_brightness_control, g19_keyboard_backlight_control, g19_foreground_control, g19_background_control, g19_highlight_control, g19_mkeys_control ] + +g110_keyboard_backlight_control = g15driver.Control("backlight_colour", _("Keyboard Backlight Colour"), (255, 0, 0), hint=g15driver.HINT_DIMMABLE | g15driver.HINT_SHADEABLE | g15driver.HINT_RED_BLUE_LED) +g110_controls = [ g110_keyboard_backlight_control, g19_mkeys_control ] + +g15_mkeys_control = g15driver.Control("mkeys", _("Memory Bank Keys"), 1, 0, 15, hint=g15driver.HINT_MKEYS) +g15_backlight_control = g15driver.Control("keyboard_backlight", _("Keyboard Backlight Level"), 2, 0, 2, hint=g15driver.HINT_DIMMABLE) +g15_lcd_backlight_control = g15driver.Control("lcd_backlight", _("LCD Backlight"), 2, 0, 2, g15driver.HINT_SHADEABLE) +g15_lcd_contrast_control = g15driver.Control("lcd_contrast", _("LCD Contrast"), 22, 0, 48, 0) +g15_invert_control = g15driver.Control("invert_lcd", _("Invert LCD"), 0, 0, 1, hint=g15driver.HINT_SWITCH | g15driver.HINT_VIRTUAL) +g15_controls = [ g15_mkeys_control, g15_backlight_control, g15_invert_control, g15_lcd_backlight_control, g15_lcd_contrast_control ] +g11_controls = [ g15_mkeys_control, g15_backlight_control ] +g13_controls = [ g19_keyboard_backlight_control, g15_mkeys_control, g15_invert_control, g15_mkeys_control ] + +""" +Keymaps that are sent to the kernel driver. These are the codes the driver +will emit. + +""" +K_KEYMAPS = { + g15driver.MODEL_G19: { + 0x0000 : S.KEY_F1, + 0x0001 : S.KEY_F2, + 0x0002 : S.KEY_F3, + 0x0003 : S.KEY_F4, + 0x0004 : S.KEY_F5, + 0x0005 : S.KEY_F6, + 0x0006 : S.KEY_F7, + 0x0007 : S.KEY_F8, + 0x0008 : S.KEY_F9, + 0x0009 : S.KEY_F10, + 0x000A : S.KEY_F11, + 0x000B : S.KEY_F12, + 0x000C : S.KEY_PROG1, + 0x000D : S.KEY_PROG2, + 0x000E : S.KEY_PROG3, + 0x000F : S.KEY_RECORD, + 0x0013 : 228, + 0x0018 : S.KEY_FORWARD, + 0x0019 : S.KEY_BACK, + 0x001A : S.KEY_MENU, + 0x001B : S.KEY_OK, + 0x001C : S.KEY_RIGHT, + 0x001D : S.KEY_LEFT, + 0x001E : S.KEY_DOWN, + 0x001F : S.KEY_UP + }, + g15driver.MODEL_G15_V1: { + 0x00 : S.KEY_F1, + 0x02 : S.KEY_F13, + 0x07 : 228, + 0x08 : S.KEY_F7, + 0x09 : S.KEY_F2, + 0x0b : S.KEY_F14, + 0x0f : S.KEY_LEFT, + 0x11 : S.KEY_F8, + 0x12 : S.KEY_F3, + 0x14 : S.KEY_F15, + 0x17 : S.KEY_UP, + 0x1a : S.KEY_F9, + 0x1b : S.KEY_F4, + 0x1d : S.KEY_F16, + 0x1f : S.KEY_DOWN, + 0x23 : S.KEY_F10, + 0x24 : S.KEY_F5, + 0x26 : S.KEY_F17, + 0x27 : S.KEY_RIGHT, + 0x28 : S.KEY_PROG1, + 0x2c : S.KEY_F11, + 0x2d : S.KEY_F6, + 0x31 : S.KEY_PROG2, + 0x35 : S.KEY_F12, + 0x36 : S.KEY_RECORD, + 0x3a : S.KEY_PROG3, + 0x3e : S.KEY_F18, + 0x3f : S.KEY_OK + }, + g15driver.MODEL_G15_V2: { + 0x0000 : S.KEY_F1, + 0x0001 : S.KEY_F2, + 0x0002 : S.KEY_F3, + 0x0003 : S.KEY_F4, + 0x0004 : S.KEY_F5, + 0x0005 : S.KEY_F6, + 0x0006 : S.KEY_PROG1, + 0x0007 : S.KEY_PROG2, + 0x0008 : 228, + 0x0009 : S.KEY_LEFT, + 0x000a : S.KEY_UP, + 0x000b : S.KEY_DOWN, + 0x000c : S.KEY_RIGHT, + 0x000d : S.KEY_PROG3, + 0x000e : S.KEY_RECORD, + 0x000f : S.KEY_OK, + }, + g15driver.MODEL_G13: { + 0x0000 : S.KEY_F1, + 0x0001 : S.KEY_F2, + 0x0002 : S.KEY_F3, + 0x0003 : S.KEY_F4, + 0x0004 : S.KEY_F5, + 0x0005 : S.KEY_F6, + 0x0006 : S.KEY_F7, + 0x0007 : S.KEY_F8, + 0x0008 : S.KEY_F9, + 0x0009 : S.KEY_F10, + 0x000A : S.KEY_F11, + 0x000B : S.KEY_F12, + 0x000C : S.KEY_F13, + 0x000D : S.KEY_F14, + 0x000E : S.KEY_F15, + 0x000F : S.KEY_F16, + 0x0010 : S.KEY_F17, + 0x0011 : S.KEY_F18, + 0x0012 : S.KEY_F19, + 0x0013 : S.KEY_F20, + 0x0014 : S.KEY_F21, + 0x0015 : S.KEY_F22, + 0x0016 : S.KEY_OK, + 0x0017 : S.KEY_LEFT, + 0x0018 : S.KEY_UP, + 0x0019 : S.KEY_DOWN, + 0x001A : S.KEY_RIGHT, + 0x001B : S.KEY_PROG1, + 0x001C : S.KEY_PROG2, + 0x001D : S.KEY_PROG3, + 0x001E : S.KEY_RECORD, + 0x001F : S.BTN_X, + 0x0020 : S.BTN_Y, + 0x0021 : S.BTN_Z, + 0x0022 : 228, + }, + g15driver.MODEL_G110: { + 0x0000 : S.KEY_F1, + 0x0001 : S.KEY_F2, + 0x0002 : S.KEY_F3, + 0x0003 : S.KEY_F4, + 0x0004 : S.KEY_F5, + 0x0005 : S.KEY_F6, + 0x0006 : S.KEY_F7, + 0x0007 : S.KEY_F8, + 0x0008 : S.KEY_F9, + 0x0009 : S.KEY_F10, + 0x000A : S.KEY_F11, + 0x000B : S.KEY_F12, + 0x000C : S.KEY_PROG1, + 0x000D : S.KEY_PROG2, + 0x000E : S.KEY_PROG3, + 0x000F : S.KEY_RECORD, + 0x0010 : 228, + }, + g15driver.MODEL_G510: { + 0x0000 : S.KEY_F1, + 0x0001 : S.KEY_F2, + 0x0002 : S.KEY_F3, + 0x0003 : S.KEY_F4, + 0x0004 : S.KEY_F5, + 0x0005 : S.KEY_F6, + 0x0006 : S.KEY_F7, + 0x0007 : S.KEY_F8, + + 0x0008 : S.KEY_F9, + 0x0009 : S.KEY_F10, + 0x000A : S.KEY_F11, + 0x000B : S.KEY_F12, + 0x000C : S.KEY_F13, + 0x000D : S.KEY_F14, + 0x000E : S.KEY_F15, + 0x000F : S.KEY_F16, + + 0x0010 : S.KEY_F17, + 0x0011 : S.KEY_F18, + 0x0013 : 228, + 0x0014 : S.KEY_PROG1, + 0x0015 : S.KEY_PROG2, + 0x0016 : S.KEY_PROG3, + 0x0017 : S.KEY_RECORD, + + 0x0018 : S.KEY_OK, + 0x0019 : S.KEY_LEFT, + 0x001A : S.KEY_UP, + 0x001B : S.KEY_DOWN, + 0x001C : S.KEY_RIGHT + + }, + } + +# from https://chromium.googlesource.com/chromiumos/third_party/autotest/+/master/client/bin/input/linux_ioctl.py + +_IOC_NRBITS = 8 +_IOC_TYPEBITS = 8 +_IOC_SIZEBITS = 14 +_IOC_DIRBITS = 2 + +_IOC_NRMASK = ((1 << _IOC_NRBITS) - 1) +_IOC_TYPEMASK = ((1 << _IOC_TYPEBITS) - 1) +_IOC_SIZEMASK = ((1 << _IOC_SIZEBITS) - 1) +_IOC_DIRMASK = ((1 << _IOC_DIRBITS) - 1) + +_IOC_NRSHIFT = 0 +_IOC_TYPESHIFT = (_IOC_NRSHIFT + _IOC_NRBITS) +_IOC_SIZESHIFT = (_IOC_TYPESHIFT + _IOC_TYPEBITS) +_IOC_DIRSHIFT = (_IOC_SIZESHIFT + _IOC_SIZEBITS) + +IOC_NONE = 0 +IOC_WRITE = 1 +IOC_READ = 2 + +# Return the byte size of a python struct format string +def sizeof(t): + return struct.calcsize(t) + +def IOC(d, t, nr, size): + return ((d << _IOC_DIRSHIFT) | (ord(t) << _IOC_TYPESHIFT) | + (nr << _IOC_NRSHIFT) | (size << _IOC_SIZESHIFT)) + +# used to create numbers +def IO(t, nr, t_format): + return IOC(IOC_NONE, t, nr, 0) + +def IOW(t, nr, t_format): + return IOC(IOC_WRITE, t, nr, sizeof(t_format)) + +def IOR(t, nr, t_format): + return IOC(IOC_READ, t, nr, sizeof(t_format)) + +# from https://chromium.googlesource.com/chromiumos/third_party/autotest/+/master/client/bin/input/linux_input.py + +# struct input_keymap_entry { +# __u8 flags; +# __u8 len; +# __u16 index; +# __u32 keycode; +# __u8 scancode[32]; +# }; +input_keymap_entry_t = 'BBHI32B' +input_keymap_entry_scancode_offset = 'BBHI' + +EVIOCGKEYCODE = IOR('E', 0x04, '2I') # get keycode +EVIOCGKEYCODE_V2 = IOR('E', 0x04, input_keymap_entry_t) +EVIOCSKEYCODE = IOW('E', 0x04, '2I') # set keycode +EVIOCSKEYCODE_V2 = IOW('E', 0x04, input_keymap_entry_t) + + +class DeviceInfo: + def __init__(self, leds, controls, key_map, led_prefix, keydev_pattern, sink_pattern = None, mm_pattern = None): + + """ + This object describes the device specific details this driver needs to know. Mainly + the file names that are created by the kernel driver and the maps for converting the + uinput keys codes from the driver into Gnome15 key code. + + Keyword arguments: + leds -- a list containing the files names of the 'Memory' keys (M1, M2, M3 and MR). + These names match files in /sys/class/leds, prefixed with the value of + led_prefix (see below) + controls -- a list g15driver.Control objects supported by this device + key_map -- a dictionary of UINPUT -> Gnome15 key codes + led_prefix -- Each LED file in /sys/class/leds is prefixed by this short model name. + keydev_pattern -- A regular expression that matches the filename of the 'if01' device in + /dev/input/by-id. 'G' Keys, 'M' Keys and 'D' Pad Keys are read from this device. + sink_pattern -- Optional. When specified, is a regular expression that matches the filename + of the 'keyboard' device in /dev/input/by-id. When specified, the driver + will open the device and just ignore any events from it. This fixes the problem + of 'F' keys being emitted when keys are pressed. + mm_pattern -- Optional. When specified, is a regular expression that matches the filename + of the 'multimedia keys' device in /dev/input/by-id. When specified, the driver + can open the device to intercept the multimedia keys and interpret them as + Gnome15 macro keys. + """ + + self.leds = leds + self.controls = controls + self.key_map = key_map + self.led_prefix = led_prefix + self.sink_pattern = sink_pattern + self.keydev_pattern = keydev_pattern + self.mm_pattern = mm_pattern + +""" +This dictionary keeps all the device specific details this +drivers needs to know. The key is the model code, the +value is a DeviceInfo object that contains the actual +details +""" +device_info = { + g15driver.MODEL_G19: DeviceInfo( + ["orange:m1", "orange:m2", "orange:m3", "red:mr" ], + g19_controls, g19_key_map, "g19", + r"usb-Logitech_G19_Gaming_Keyboard-event-if.*", + r"usb-Logitech_G19_Gaming_Keyboard-.*event-kbd.*", + r"usb-046d_G19_Gaming_Keyboard-event-if.*"), + + g15driver.MODEL_G11: DeviceInfo( + ["orange:m1", "orange:m2", "orange:m3", "blue:mr" ], + g11_controls, g15_key_map, "g15", + r"G15_Keyboard_G15.*if"), + + g15driver.MODEL_G15_V1: DeviceInfo( + ["orange:m1", "orange:m2", "orange:m3", "blue:mr" ], + g15_controls, g15_key_map, "g15", + r"G15_Keyboard_G15.*if", + r"G15_Keyboard_G15.*kbd", + r"usb-Logitech_Logitech_Gaming_Keyboard-event-if.*"), + + g15driver.MODEL_G15_V2: DeviceInfo( + ["red:m1", "red:m2", "red:m3", "blue:mr" ], + g15_controls, g15v2_key_map, "g15v2", + r"G15_GamePanel_LCD-event-if.*", + r"G15_GamePanel_LCD-event-kdb.*"), + + g15driver.MODEL_G13: DeviceInfo( + ["red:m1", "red:m2", "red:m3", "red:mr" ], + g13_controls, g13_key_map, "g13", + r"_G13-event-mouse"), + + g15driver.MODEL_G110: DeviceInfo( + ["orange:m1", "orange:m2", "orange:m3", "red:mr" ], + g110_controls, g110_key_map, "g110", + r"usb-LOGITECH_G110_G-keys-event-if.*", + r"usb-LOGITECH_G110_G-keys-event-kbd.*"), + + g15driver.MODEL_G510: DeviceInfo( + ["orange:m1", "orange:m2", "orange:m3", "red:mr" ], + g13_controls, g510_key_map, "g510", + r"G510_Gaming_Keyboard.*event-if.*", + r"G510_Gaming_Keyboard.*event.*kbd.*"), + } + + +# Other constants +EVIOCGRAB = 0x40044590 + +def show_preferences(device, parent, gconf_client): + prefs = KernelDriverPreferences(device, parent, gconf_client) + prefs.run() + +class KernelDriverPreferences(): + + def __init__(self, device, parent, gconf_client): + self.device = device + + widget_tree = gtk.Builder() + widget_tree.add_from_file(os.path.join(g15globals.ui_dir, "driver_kernel.ui")) + self.window = widget_tree.get_object("KernelDriverSettings") + self.window.set_transient_for(parent) + + self.joy_mode_label = widget_tree.get_object("JoyModeLabel") + self.joy_mode_combo = widget_tree.get_object("JoyModeCombo") + self.joy_calibrate = widget_tree.get_object("JoyCalibrate") + self.grab_multimedia = widget_tree.get_object("GrabMultimedia") + + device_model = widget_tree.get_object("DeviceModel") + device_model.clear() + device_model.append(["auto"]) + for dev_file in os.listdir("/dev"): + if dev_file.startswith("fb"): + device_model.append(["/dev/%s" % dev_file]) + + g15uigconf.configure_combo_from_gconf(gconf_client, "/apps/gnome15/%s/fb_device" % device.uid, "DeviceCombo", "auto", widget_tree) + g15uigconf.configure_combo_from_gconf(gconf_client, "/apps/gnome15/%s/joymode" % device.uid, "JoyModeCombo", "macro", widget_tree) + g15uigconf.configure_checkbox_from_gconf(gconf_client, "/apps/gnome15/%s/grab_multimedia" % device.uid, "GrabMultimedia", False, widget_tree) + + self.grab_multimedia.set_sensitive(device_info[device.model_id].mm_pattern is not None) + + # See if jstest-gtk is available to do the calibration + self.calibrate_available = g15uinput.are_calibration_tools_available() + + self.joy_mode_combo.connect("changed", self._set_available_options) + self.joy_calibrate.connect("clicked", self._do_calibrate) + + self._set_available_options() + + def run(self): + self.window.run() + self.window.hide() + + def _set_available_options(self, widget = None): + self.joy_mode_label.set_sensitive(self.device.model_id == g15driver.MODEL_G13) + self.joy_mode_combo.set_sensitive(self.device.model_id == g15driver.MODEL_G13) + self.joy_calibrate.set_sensitive(g15uinput.get_device(self._get_device_type()) is not None and \ + self.device.model_id == g15driver.MODEL_G13 and \ + self.calibrate_available and \ + self.joy_mode_combo.get_active() in [1, 3]) + + def _get_device_type(self): + return g15uinput.JOYSTICK if self.joy_mode_combo.get_active() == 1 \ + else g15uinput.DIGITAL_JOYSTICK + + def _do_calibrate(self, widget): + g15uinput.calibrate(self._get_device_type()) + +class KeyboardReceiveThread(Thread): + def __init__(self, device): + Thread.__init__(self) + self._run = True + self.name = "KeyboardReceiveThread-%s" % device.uid + self.setDaemon(True) + self.devices = [] + + def deactivate(self): + self._run = False + for dev in self.devices: + logger.info("Ungrabbing %d", dev.fileno()) + try : + fcntl.ioctl(dev.fileno(), EVIOCGRAB, 0) + except Exception as e: + logger.info("Failed ungrab.", exc_info = e) + logger.info("Closing %d", dev.fileno()) + try : + self.fds[dev.fileno()].close() + except Exception as e: + logger.info("Failed close.", exc_info = e) + logger.info("Stopped %d", dev.fileno()) + logger.info("Stopped all input devices") + + def run(self): + self.poll = select.poll() + self.fds = {} + for dev in self.devices: + self.poll.register(dev, select.POLLIN | select.POLLPRI | select.POLLHUP | select.POLLNVAL | select.POLLERR) + fcntl.ioctl(dev.fileno(), EVIOCGRAB, 1) + self.fds[dev.fileno()] = dev + while self._run: + for x, e in self.poll.poll(1000): + dev = self.fds[x] + try : + if dev: + dev.read() + except OSError as e: + logger.debug('Could not read device file', exc_info = e) + # Ignore this error if deactivated + if self._run: + raise e + logger.info("Thread left") + +''' +SimpleDevice implementation that does nothing with events. This is used to +work-around a problem where X ends up getting the G19 F-key events +''' +class SinkDevice(SimpleDevice): + def __init__(self, *args, **kwargs): + SimpleDevice.__init__(self, *args, **kwargs) + + def receive(self, event): + logger.debug("Sunk event %s", str(event)) + +''' +Abstract input device +''' +class AbstractInputDevice(SimpleDevice): + def __init__(self, callback, key_map, *args, **kwargs): + SimpleDevice.__init__(self, *args, **kwargs) + self.callback = callback + self.key_map = key_map + + def _event(self, event_code, state): + if event_code in self.key_map: + key = self.key_map[event_code] + self.callback([key], state) + else: + logger.warning("Unmapped key for event: %s", event_code) + +''' +SimpleDevice implementation for handling multi-media keys. +''' +class MultiMediaDevice(AbstractInputDevice): + def __init__(self, callback, key_map, *args, **kwargs): + AbstractInputDevice.__init__(self, callback, key_map, *args, **kwargs) + + def receive(self, event): + if event.etype == S.EV_KEY: + state = g15driver.KEY_STATE_DOWN if event.evalue == 1 else g15driver.KEY_STATE_UP + if event.evalue != 2: + self._event(event.ecode, state) + elif event.etype == 0: + return + else: + logger.warning("Unhandled event: %s", str(event)) + +''' +SimpleDevice implementation that translates kernel input events +into Gnome15 key events and forwards them to the registered +Gnome15 keyboard callback. +''' +class ForwardDevice(AbstractInputDevice): + def __init__(self, driver, callback, key_map, *args, **kwargs): + AbstractInputDevice.__init__(self, callback, key_map, *args, **kwargs) + self.driver = driver + self.ctrl = False + self.held_keys = [] + self.alt = False + self.shift = False + self.current_x = g15uinput.JOYSTICK_CENTER + self.digital_down = [] + self.current_y = g15uinput.JOYSTICK_CENTER + self.last_x = g15uinput.JOYSTICK_CENTER + self.last_y = g15uinput.JOYSTICK_CENTER + self.move_timer = None + + def send_all(self, events): + for event in events: + logger.debug(" --> %r", event) + self.udev.send_event(event) + + @property + def modcode(self): + code = 0 + if self.shift: + code += 1 + if self.ctrl: + code += 2 + if self.alt: + code += 4 + return code + + def receive(self, event): + if event.etype == S.EV_ABS: + if self.driver.joy_mode == g15uinput.JOYSTICK: + """ + The kernel modules give a joystick position values between 0 and 255. + The center is at 128. + The virtual joysticks are set to give values between -127 and 127. + The center is at 0. + So we adapt the received values. + """ + val = event.evalue - g15uinput.DEVICE_JOYSTICK_CENTER + g15uinput.emit(self.driver.joy_mode, (event.etype, event.ecode), val, False) + else: + self._update_joystick(event) + elif event.etype == S.EV_KEY: + state = g15driver.KEY_STATE_DOWN if event.evalue == 1 else g15driver.KEY_STATE_UP + if event.ecode in [ S.BTN_X, S.BTN_Y, S.BTN_Z ]: + if self.driver.joy_mode ==g15uinput.MOUSE: + g15uinput.emit(g15uinput.MOUSE, self._translate_mouse_buttons(event.ecode), event.evalue, syn=True) + elif self.driver.joy_mode == g15uinput.DIGITAL_JOYSTICK: + g15uinput.emit(g15uinput.DIGITAL_JOYSTICK, event.ecode, event.evalue, syn=True) + elif self.driver.joy_mode == g15uinput.JOYSTICK: + g15uinput.emit(g15uinput.JOYSTICK, event.ecode, event.evalue, syn=True) + else: + if event.evalue != 2: + self._event(event.ecode, state) + else: + if event.evalue != 2: + self._event(event.ecode, state) + elif event.etype == 0: + if self.driver.joy_mode == g15uinput.JOYSTICK: + # Just pass-through when in analogue joystick mode + g15uinput.emit(self.driver.joy_mode, ( event.etype, event.ecode ), event.evalue, False) + else: + logger.warning("Unhandled event: %s", str(event)) + + """ + Private + """ + + def _record_current_absolute_position(self, event): + """ + Update the current_x and current_y positions if this is an + absolute movement event + """ + if event.ecode == S.ABS_X: + self.current_x = event.evalue - g15uinput.DEVICE_JOYSTICK_CENTER + if event.ecode == S.ABS_Y: + self.current_y = event.evalue - g15uinput.DEVICE_JOYSTICK_CENTER + + def _update_joystick(self, event): + """ + Handle a position update event from the joystick, either by translating + it to mouse movements, digitising it, or emiting macros + + Keyword arguments: + event -- event + """ + if self.driver.joy_mode == g15uinput.DIGITAL_JOYSTICK: + self._record_current_absolute_position(event) + self._digital_joystick(event) + elif self.driver.joy_mode == g15uinput.MOUSE: + low_val = g15uinput.JOYSTICK_CENTER - self.driver.calibration + high_val = g15uinput.JOYSTICK_CENTER + self.driver.calibration + + if event.ecode == S.REL_X: + self.current_x = event.evalue - g15uinput.DEVICE_JOYSTICK_CENTER + if event.ecode == S.REL_Y: + self.current_y = event.evalue - g15uinput.DEVICE_JOYSTICK_CENTER + + # Get the amount between the current value and the centre to move + move_x = 0 + move_y = 0 + if self.current_x >= high_val: + move_x = self.current_x - high_val + elif self.current_x <= low_val: + move_x = self.current_x - low_val + if self.current_y >= high_val: + move_y = self.current_y - high_val + elif self.current_y <= low_val: + move_y = self.current_y - low_val + + if self.current_x != self.last_x or self.current_y != self.last_y: + self.last_x = self.current_x + self.last_y = self.current_y + self.move_x = self._clamp(-3, move_x / 8, 3) + self.move_y = self._clamp(-3, move_y / 8, 3) + self._mouse_move() + else: + if self.move_timer is not None: + self.move_timer.cancel() + else: + self._emit_macro(event) + + def _translate_mouse_buttons(self, ecode): + """ + Translate the default joystick event codes to default mouse + event codes + + Keyword arguments: + ecode -- event code to translate + """ + if ecode == S.BTN_X: + return g15uinput.BTN_LEFT + elif ecode == S.BTN_Y: + return g15uinput.BTN_RIGHT + elif ecode == S.BTN_Z: + return g15uinput.BTN_MIDDLE + + def _compute_bounds(self): + """ + Calculate the distances from the (rough) centre position to the position + when movement each axis will start emiting events based on the + current calibration value. + + """ + return (g15uinput.JOYSTICK_CENTER - (self.driver.calibration), + g15uinput.JOYSTICK_CENTER + (self.driver.calibration)) + + def _emit_macro(self, event): + """ + Emit macro keys for joystick positions, so they can be processed as all + other macro keys are (i.e. assigned to a macro, script, or a different + uinput key) + + Keyword arguments: + event -- event + """ + low_val, high_val = self._compute_bounds() + val = event.evalue - g15uinput.DEVICE_JOYSTICK_CENTER + if event.ecode == S.ABS_X: + if val < low_val: + self._release_keys([g15driver.G_KEY_RIGHT]) + if not g15driver.G_KEY_LEFT in self.held_keys: + self.callback([g15driver.G_KEY_LEFT], g15driver.KEY_STATE_DOWN) + self.held_keys.append(g15driver.G_KEY_LEFT) + elif val > high_val: + self._release_keys([g15driver.G_KEY_LEFT]) + if not g15driver.G_KEY_RIGHT in self.held_keys: + self.callback([g15driver.G_KEY_RIGHT], g15driver.KEY_STATE_DOWN) + self.held_keys.append(g15driver.G_KEY_RIGHT) + else: + self._release_keys([g15driver.G_KEY_LEFT,g15driver.G_KEY_RIGHT]) + if event.ecode == S.ABS_Y: + if val < low_val: + self._release_keys([g15driver.G_KEY_DOWN]) + if not g15driver.G_KEY_UP in self.held_keys: + self.callback([g15driver.G_KEY_UP], g15driver.KEY_STATE_DOWN) + self.held_keys.append(g15driver.G_KEY_UP) + elif val > high_val: + self._release_keys([g15driver.G_KEY_UP]) + if not g15driver.G_KEY_DOWN in self.held_keys: + self.callback([g15driver.G_KEY_DOWN], g15driver.KEY_STATE_DOWN) + self.held_keys.append(g15driver.G_KEY_DOWN) + else: + self._release_keys([g15driver.G_KEY_UP,g15driver.G_KEY_DOWN]) + + def _release_keys(self, keys): + for k in keys: + if k in self.held_keys: + self.callback([k], g15driver.KEY_STATE_UP) + self.held_keys.remove(k) + + def _clamp(self, minimum, x, maximum): + return max(minimum, min(x, maximum)) + + def _mouse_move(self): + if self.move_x != 0 or self.move_y != 0: + if self.move_x != 0: + g15uinput.emit(g15uinput.MOUSE, g15uinput.REL_X, self.move_x) + if self.move_y != 0: + g15uinput.emit(g15uinput.MOUSE, g15uinput.REL_Y, self.move_y) + self.move_timer = g15scheduler.schedule("MouseMove", 0.1, self._mouse_move) + + def _digital_joystick(self, event): + low_val, high_val = self._compute_bounds() + val = event.evalue - g15uinput.DEVICE_JOYSTICK_CENTER + if event.ecode == S.ABS_X: + if val < low_val and not "l" in self.digital_down: + self.digital_down.append("l") + g15uinput.emit(g15uinput.DIGITAL_JOYSTICK, + g15uinput.ABS_X, + g15uinput.JOYSTICK_MIN) + elif val > high_val and not "r" in self.digital_down: + self.digital_down.append("r") + g15uinput.emit(g15uinput.DIGITAL_JOYSTICK, + g15uinput.ABS_X, + g15uinput.JOYSTICK_MAX) + elif val >= low_val and val <= high_val and "l" in self.digital_down: + self.digital_down.remove("l") + g15uinput.emit(g15uinput.DIGITAL_JOYSTICK, + g15uinput.ABS_X, + g15uinput.JOYSTICK_CENTER) + elif val >= low_val and val <= high_val and "r" in self.digital_down: + self.digital_down.remove("r") + g15uinput.emit(g15uinput.DIGITAL_JOYSTICK, + g15uinput.ABS_X, + g15uinput.JOYSTICK_CENTER) + if event.ecode == S.ABS_Y: + if val < low_val and not "u" in self.digital_down: + self.digital_down.append("u") + g15uinput.emit(g15uinput.DIGITAL_JOYSTICK, + g15uinput.ABS_Y, + g15uinput.JOYSTICK_MIN) + elif val > high_val and not "d" in self.digital_down: + self.digital_down.append("d") + g15uinput.emit(g15uinput.DIGITAL_JOYSTICK, + g15uinput.ABS_Y, + g15uinput.JOYSTICK_MAX) + if val >= low_val and val <= high_val and "u" in self.digital_down: + self.digital_down.remove("u") + g15uinput.emit(g15uinput.DIGITAL_JOYSTICK, + g15uinput.ABS_Y, + g15uinput.JOYSTICK_CENTER) + elif val >= low_val and val <= high_val and "d" in self.digital_down: + self.digital_down.remove("d") + g15uinput.emit(g15uinput.DIGITAL_JOYSTICK, + g15uinput.ABS_Y, + g15uinput.JOYSTICK_CENTER) + +class Driver(g15driver.AbstractDriver): + + def __init__(self, device, on_close=None): + g15driver.AbstractDriver.__init__(self, "kernel") + self.notify_handles = [] + self.fb = None + self.var_info = None + self.on_close = on_close + self.key_thread = None + self.device = device + self.device_info = None + self.system_service = None + self.conf_client = gconf.client_get_default() + + try: + self._init_device() + except Exception as e: + # Reset the framebuffer choice back to auto if the requested device does not exist + if self.device_name != None and self.device_name != "" or self.device_name != "auto": + self.conf_client.set_string("/apps/gnome15/%s/fb_device" % self.device.uid, "auto") + self._init_device() + else: + logger.warning("Could not open %s.", self.device_name, exc_info = e) + + self.notify_handles.append(self.conf_client.notify_add("/apps/gnome15/%s/joymode" % self.device.uid, self._config_changed, None)) + self.notify_handles.append(self.conf_client.notify_add("/apps/gnome15/%s/fb_device" % self.device.uid, self._framebuffer_device_changed, None)) + self.notify_handles.append(self.conf_client.notify_add("/apps/gnome15/%s/grab_multimedia" % self.device.uid, self._config_changed, None)) + + def get_antialias(self): + if self.device.bpp != 1: + return cairo.ANTIALIAS_DEFAULT + else: + return cairo.ANTIALIAS_NONE + + def is_connected(self): + return self.system_service != None + + def get_model_names(self): + return device_info.keys() + + def get_name(self): + return "Linux Logitech Kernel Driver" + + def get_model_name(self): + return self.device.model_id if self.device != None else None + + def get_action_keys(self): + return self.device.action_keys + + def get_key_layout(self): + if self.get_model_name() == g15driver.MODEL_G13 and "macro" == self.conf_client.get_string("/apps/gnome15/%s/joymode" % self.device.uid): + """ + This driver with the G13 supports some additional keys + """ + l = list(self.device.key_layout) + l.append([ g15driver.G_KEY_UP ]) + l.append([ g15driver.G_KEY_JOY_LEFT, g15driver.G_KEY_LEFT, g15driver.G_KEY_JOY_CENTER, g15driver.G_KEY_RIGHT ]) + l.append([ g15driver.G_KEY_JOY_DOWN, g15driver.G_KEY_DOWN ]) + return l + elif self.device_info.mm_pattern is not None and self.grab_multimedia: + l = list(self.device.key_layout) + l.append([]) + l.append([ g15driver.G_KEY_VOL_UP, g15driver.G_KEY_VOL_DOWN, g15driver.G_KEY_MUTE ]) + l.append([ g15driver.G_KEY_PREV, g15driver.G_KEY_PLAY, g15driver.G_KEY_STOP, g15driver.G_KEY_NEXT ]) + return l + else: + return self.device.key_layout + + def _load_configuration(self): + self.joy_mode = self.conf_client.get_string("/apps/gnome15/%s/joymode" % self.device.uid) + self.grab_multimedia = self.conf_client.get_bool("/apps/gnome15/%s/grab_multimedia" % self.device.uid) + if self.joy_mode == g15uinput.MOUSE: + logger.info("Enabling mouse emulation") + self.calibration = 20 + elif self.joy_mode == g15uinput.JOYSTICK: + logger.info("Enabling analogue joystick emulation") + self.calibration = 20 + elif self.joy_mode == g15uinput.DIGITAL_JOYSTICK: + logger.info("Enabling digital joystick emulation") + self.calibration = 64 + else: + logger.info("Enabling macro keys for joystick") + self.calibration = 64 + + def _config_changed(self, client, connection_id, entry, args): + self._reload_and_reconnect() + + def _framebuffer_device_changed(self, client, connection_id, entry, args): + self._reload_and_reconnect() + + def get_size(self): + return self.device.lcd_size + + def get_bpp(self): + return self.device.bpp + + def get_controls(self): + return self.device_info.controls if self.device_info != None else None + + def paint(self, img): + if not self.fb: + return + width = img.get_width() + height = img.get_height() + character_width = width / 8 + fixed = self.fb.get_fixed_info() + padding = fixed.line_length - character_width + file_str = StringIO() + + if self.get_model_name() == g15driver.MODEL_G19: + try: + back_surface = cairo.ImageSurface (4, width, height) + except Exception as e: + logger.debug("Could not create ImageSurface. Trying earlier API.", exc_info = e) + # Earlier version of Cairo + back_surface = cairo.ImageSurface (cairo.FORMAT_ARGB32, width, height) + back_context = cairo.Context (back_surface) + back_context.set_source_surface(img, 0, 0) + back_context.set_operator (cairo.OPERATOR_SOURCE); + back_context.paint() + + if back_surface.get_format() == cairo.FORMAT_ARGB32: + """ + If the creation of the type 4 image failed (i.e. earlier version of Cairo) + then we have to convert it ourselves. This is slow. + + TODO Replace with C routine + """ + file_str = StringIO() + data = back_surface.get_data() + for i in range(0, len(data), 4): + r = ord(data[i + 2]) + g = ord(data[i + 1]) + b = ord(data[i + 0]) + file_str.write(self.rgb_to_uint16(r, g, b)) + buf = file_str.getvalue() + else: + buf = str(back_surface.get_data()) + else: + width, height = self.get_size() + arrbuf = array.array('B', self.empty_buf) + + argb_surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, width, height) + argb_context = cairo.Context(argb_surface) + argb_context.set_source_surface(img) + argb_context.paint() + + ''' + Now convert the ARGB to a PIL image so it can be converted to a 1 bit monochrome image, with all + colours dithered. It would be nice if Cairo could do this :( Any suggestions? + ''' + pil_img = Image.frombuffer("RGBA", (width, height), argb_surface.get_data(), "raw", "RGBA", 0, 1) + pil_img = ImageMath.eval("convert(pil_img,'1')",pil_img=pil_img) + pil_img = ImageMath.eval("convert(pil_img,'P')",pil_img=pil_img) + pil_img = pil_img.point(lambda i: i >= 250,'1') + + # Invert the screen if required + if g15_invert_control.value == 0: + pil_img = pil_img.point(lambda i: 1^i) + + # Data is 160x43, 1 byte per pixel. Will have value of 0 or 1. + width, height = self.get_size() + data = list(pil_img.getdata()) + fixed = self.fb.get_fixed_info() + v = 0 + b = 1 + + # TODO Replace with C routine + for row in range(0, height): + for col in range(0, width): + if data[( row * width ) + col]: + v += b + b = b << 1 + if b == 256: + # Full byte + b = 1 + i = row * fixed.line_length + col / 8 + arrbuf[i] = v + v = 0 + buf = arrbuf.tostring() + + if self.fb and self.fb.buffer: + self.fb.buffer[0:len(buf)] = buf + + def process_svg(self, document): + if self.get_bpp() == 1: + for element in document.getroot().iter(): + style = element.get("style") + if style != None: + element.set("style", style.replace("font-family:Sans", "font-family:%s" % g15globals.fixed_size_font_name)) + + def on_update_control(self, control): + if control == g19_keyboard_backlight_control or control == g110_keyboard_backlight_control: + self._write_to_led("red:bl", control.value[0]) + if control.hint & g15driver.HINT_RED_BLUE_LED == 0: + self._write_to_led("green:bl", control.value[1]) + self._write_to_led("blue:bl", control.value[2]) + else: + # The G110 only has red and blue LEDs + self._write_to_led("blue:bl", control.value[2]) + elif control == g15_backlight_control: + if self.get_model_name() == g15driver.MODEL_G15_V2: + # G15v2 has different coloured backlight + self._write_to_led("orange:keys", control.value) + else: + self._write_to_led("blue:keys", control.value) + elif control == g15_lcd_backlight_control or control == g19_brightness_control: + self._write_to_led("white:screen", control.value) + elif control == g15_lcd_contrast_control: + self._write_to_led("contrast:screen", control.value) + elif control == g15_mkeys_control or control == g19_mkeys_control: + self._set_mkey_lights(control.value) + else: + if control.hint & g15driver.HINT_VIRTUAL == 0: + logger.warning("Setting the control " + control.id + " is not yet supported on this model. " + \ + "Please report this as a bug, providing the contents of your /sys/class/led" + \ + "directory and the keyboard model you use.") + + def grab_keyboard(self, callback): + if self.key_thread != None: + raise Exception("Keyboard already grabbed") + + # Configure the keymap + logger.info("Grabbing current keymap settings") + self.original_keymap = self._get_keymap() + kernel_keymap_replacement = K_KEYMAPS[self.device.model_id] + self._set_keymap(kernel_keymap_replacement) + + self.key_thread = KeyboardReceiveThread(self.device) + for devpath in self.keyboard_devices: + logger.info("Adding input device %s", devpath) + self.key_thread.devices.append(ForwardDevice(self, callback, self.device_info.key_map, devpath, devpath)) + for devpath in self.sink_devices: + logger.info("Adding input sink device %s", devpath) + self.key_thread.devices.append(SinkDevice(devpath, devpath)) + for devpath in self.mm_devices: + logger.info("Adding input multi-media device %s", devpath) + self.key_thread.devices.append(MultiMediaDevice(callback, self.device_info.key_map, devpath, devpath)) + self.key_thread.start() + + ''' + Private + ''' + + def _on_connect(self): + self.notify_handles = [] + # Check hardware again + self._init_driver() + + # Sanity check + if not self.device: + raise usb.USBError("No supported logitech keyboards found on USB bus") + if self.device == None: + raise usb.USBError("WARNING: Found no " + self.model + " Logitech keyboard, Giving up") + + # If there is no LCD for this device, don't open the framebuffer + if self.device.bpp != 0: + if self.fb_mode == None or self.device_name == None: + raise usb.USBError("No matching framebuffer device found") + if self.fb_mode != self.framebuffer_mode: + raise usb.USBError("Unexpected framebuffer mode %s, expected %s for device %s" % (self.fb_mode, self.framebuffer_mode, self.device_name)) + + # Open framebuffer + logger.info("Using framebuffer %s", self.device_name) + self.fb = fb.fb_device(self.device_name) + if logger.isEnabledFor(logging.DEBUG): + self.fb.dump() + self.var_info = self.fb.get_var_info() + + # Create an empty string buffer for use with monochrome LCD + self.empty_buf = "" + for i in range(0, self.fb.get_fixed_info().smem_len): + self.empty_buf += chr(0) + + # Connect to DBUS + system_bus = dbus.SystemBus() + try: + system_service_object = system_bus.get_object('org.gnome15.SystemService', '/org/gnome15/SystemService') + except dbus.DBusException as e: + logger.debug('D-Bus service not available.', exc_info = e) + raise Exception("Failed to connect to Gnome15 system service. Is g15-system-service running (as root). \ +It should be launched automatically if Gnome15 is installed correctly.") + self.system_service = dbus.Interface(system_service_object, 'org.gnome15.SystemService') + + # Centre the joystick by default + if self.joy_mode in [ g15uinput.JOYSTICK, g15uinput.DIGITAL_JOYSTICK ]: + g15uinput.emit(self.joy_mode, g15uinput.ABS_X, g15uinput.JOYSTICK_CENTER, False) + g15uinput.emit(self.joy_mode, g15uinput.ABS_Y, g15uinput.JOYSTICK_CENTER, False) + g15uinput.syn(self.joy_mode) + + def _on_disconnect(self): + if not self.is_connected(): + raise Exception("Not connected") + self._stop_receiving_keys() + if self.fb is not None: + self.fb.__del__() + self.fb = None + if self.on_close != None: + g15scheduler.schedule("Close", 0, self.on_close, self) + self.system_service = None + + def _reload_and_reconnect(self): + self._load_configuration() + if self.is_connected(): + self.disconnect() + + def _set_mkey_lights(self, lights): + if self.device_info.leds: + leds = self.device_info.leds + self._write_to_led(leds[0], lights & g15driver.MKEY_LIGHT_1 != 0) + self._write_to_led(leds[1], lights & g15driver.MKEY_LIGHT_2 != 0) + self._write_to_led(leds[2], lights & g15driver.MKEY_LIGHT_3 != 0) + self._write_to_led(leds[3], lights & g15driver.MKEY_LIGHT_MR != 0) + else: + logger.warning(" Setting MKey lights on " + self.device.model_id + " not yet supported. " + \ + "Please report this as a bug, providing the contents of your /sys/class/led" + \ + "directory and the keyboard model you use.") + + def _stop_receiving_keys(self): + if self.key_thread != None: + # Configure the keymap + logger.info("Resetting keymap settings back to the way they were") + self._set_keymap(self.original_keymap) + + self.key_thread.deactivate() + self.key_thread = None + + def _do_write_to_led(self, name, value): + if not self.system_service: + logger.warning("Attempt to write to LED when not connected") + else: + logger.debug("Writing %s to LED %s", value, name) + self.system_service.SetLight(self.device.uid, name, value) + + def _write_to_led(self, name, value): + gobject.idle_add(self._do_write_to_led, name, value) + + def _set_keymap(self, keymap): + for devpath in self.keyboard_devices: + logger.debug("Setting keymap on device %s", devpath) + fd = open(devpath, "rw") + try: + buf = array.array('B', [0] * sizeof(input_keymap_entry_t)) + for scancode, keycode in keymap.items(): + struct.pack_into(input_keymap_entry_t, buf, 0, + 0, # flags + sizeof('I'), # len + 0, # index + keycode, # keycode + *([0] * 32)) + struct.pack_into('I', buf, sizeof(input_keymap_entry_scancode_offset), scancode) + + fcntl.ioctl(fd, EVIOCSKEYCODE_V2, buf) + logger.debug(" key %d := %d", scancode, keycode) + finally: + fd.close() + + def _get_keymap(self): + for devpath in self.keyboard_devices: + logger.debug("Getting keymap from device %s", devpath) + fd = open(devpath, "rw") + try: + keymap = K_KEYMAPS[self.device.model_id].copy() + buf = array.array('B', [0] * sizeof(input_keymap_entry_t)) + for scancode in keymap: + struct.pack_into(input_keymap_entry_t, buf, 0, + 0, # flags + sizeof('I'), # len + 0, # index + S.KEY_RESERVED, # keycode + *([0] * 32)) + struct.pack_into('I', buf, sizeof(input_keymap_entry_scancode_offset), scancode) + + fcntl.ioctl(fd, EVIOCGKEYCODE_V2, buf) + keymap[scancode] = struct.unpack(input_keymap_entry_t, buf)[3] + logger.debug(" key %d = %d", scancode, keymap[scancode]) + return keymap + finally: + fd.close() + return None + + def _handle_bound_key(self, key): + logger.info("G key - %d", key) + return True + + def _mode_changed(self, client, connection_id, entry, args): + if self.is_connected(): + self.disconnect() + + def _init_device(self): + self._load_configuration() + if not self.device.model_id in device_info: + return + + self.device_info = device_info[self.device.model_id] + self.fb_mode = None + self.device_name = None + self.framebuffer_mode = "NONE" + + if self.device.bpp == 0: + logger.info("Device %s has no framebuffer", self.device.model_id) + else: + if self.device.bpp == 1: + self.framebuffer_mode = "GFB_MONO" + else: + self.framebuffer_mode = "GFB_QVGA" + logger.info("Using %s frame buffer mode", self.framebuffer_mode) + + # Determine the framebuffer device to use + self.device_name = self.conf_client.get_string("/apps/gnome15/%s/fb_device" % self.device.uid) + if self.device_name == None or self.device_name == "" or self.device_name == "auto": + for fb in os.listdir("/sys/class/graphics"): + if fb != "fbcon": + logger.info("Trying %s", fb) + device_file = "/sys/class/graphics/%s/device" % fb + if os.path.exists(device_file): + usb_id = os.path.basename(os.path.realpath(device_file)).split(".")[0].split(":") + if len(usb_id) > 2: + if usb_id[1].lower() == "%04x" % self.device.controls_usb_id[0] and usb_id[2].lower() == "%04x" % self.device.controls_usb_id[1]: + self.device_name = "/dev/%s" % fb + break + + # If still no device name, give up + if self.device_name == None or self.device_name == "" or self.device_name == "auto": + raise Exception("No frame buffer device specified, and none could be found automatically. Are the kernel modules loaded?") + + # Get the mode of the device + f = open("/sys/class/graphics/" + os.path.basename(self.device_name) + "/name", "r") + try : + self.fb_mode = f.readline().replace("\n", "") + finally : + f.close() + + def _init_driver(self): + self._init_device() + + # Try and find the paths for the keyboard devices + self.keyboard_devices = [] + self.sink_devices = [] + self.mm_devices = [] + + dir = "/dev/input/by-id" + for p in os.listdir(dir): + if re.search(self.device_info.keydev_pattern, p): + logger.info("Input device %s matches %s", p, self.device_info.keydev_pattern) + self.keyboard_devices.append(dir + "/" + p) + if self.device_info.sink_pattern is not None and re.search(self.device_info.sink_pattern, p): + logger.info("Input sink device %s matches %s", p, self.device_info.sink_pattern) + self.sink_devices.append(dir + "/" + p) + if self.grab_multimedia and self.device_info.mm_pattern is not None and re.search(self.device_info.mm_pattern, p): + logger.info("Input multi-media device %s matches %s", p, self.device_info.mm_pattern) + self.mm_devices.append(dir + "/" + p) + + def __del__(self): + for h in self.notify_handles: + self.conf_client.notify_remove(h) diff --git a/src/gnome15/drivers/driver_mx5500.py b/src/gnome15/drivers/driver_mx5500.py new file mode 100644 index 0000000..d9e2252 --- /dev/null +++ b/src/gnome15/drivers/driver_mx5500.py @@ -0,0 +1,406 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2010 Brett Smith +# +# 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 . + +""" +Main implementation of a G15Driver that uses g15daemon to control and query the +keyboard +""" + +import gnome15.g15driver as g15driver +import gnome15.g15globals as g15globals +import gnome15.util.g15uigconf as g15uigconf +import gnome15.util.g15scheduler as g15scheduler +import gtk +import os.path +import socket +import cairo +import gconf +from PIL import ImageMath +from PIL import Image +from threading import Thread +from threading import Lock +import struct +import time +import logging +import asyncore +import sys +logger = logging.getLogger(__name__) + +# Driver information (used by driver selection UI) +name="MX5500" +id="mx5500" +description="For use with the Logitech G15v1, G15v2, G13, G510 and G110. This driver uses mx5500tools, available from " + \ + "mx5500tools. The mx5500d service " + \ + "must be installed and running when starting Gnome15." +has_preferences=True + +DEFAULT_PORT=15550 + +CLIENT_CMD_KB_BACKLIGHT = 0x08 +CLIENT_CMD_CONTRAST = 0x40 +CLIENT_CMD_BACKLIGHT = 0x80 +CLIENT_CMD_GET_KEYSTATE = ord('k') +CLIENT_CMD_KEY_HANDLER = 0x10 +CLIENT_CMD_MKEY_LIGHTS = 0x20 +CLIENT_CMD_SWITCH_PRIORITIES = ord('p') +CLIENT_CMD_NEVER_SELECT = ord('n') +CLIENT_CMD_IS_FOREGROUND = ord('v') +CLIENT_CMD_IS_USER_SELECTED = ord('u') +CLIENT_CMD_KB_BACKLIGHT_COLOR = ord('r') + +KEY_MAP = { + g15driver.G_KEY_G1 : 1<<0, + g15driver.G_KEY_G2 : 1<<1, + g15driver.G_KEY_G3 : 1<<2, + g15driver.G_KEY_G4 : 1<<3, + g15driver.G_KEY_G5 : 1<<4, + g15driver.G_KEY_G6 : 1<<5, + g15driver.G_KEY_G7 : 1<<6, + g15driver.G_KEY_G8 : 1<<7, + g15driver.G_KEY_G9 : 1<<8, + g15driver.G_KEY_G10 : 1<<9, + g15driver.G_KEY_G11 : 1<<10, + g15driver.G_KEY_G12 : 1<<11, + g15driver.G_KEY_G13 : 1<<12, + g15driver.G_KEY_G14 : 1<<13, + g15driver.G_KEY_G15 : 1<<14, + g15driver.G_KEY_G16 : 1<<15, + g15driver.G_KEY_G17 : 1<<16, + g15driver.G_KEY_G18 : 1<<17, + + g15driver.G_KEY_M1 : 1<<18, + g15driver.G_KEY_M2 : 1<<19, + g15driver.G_KEY_M3 : 1<<20, + g15driver.G_KEY_MR : 1<<21, + + g15driver.G_KEY_L1 : 1<<22, + g15driver.G_KEY_L2 : 1<<23, + g15driver.G_KEY_L3 : 1<<24, + g15driver.G_KEY_L4 : 1<<25, + g15driver.G_KEY_L5 : 1<<26, + + g15driver.G_KEY_LIGHT : 1<<27 + } + + +invert_control = g15driver.Control("invert_lcd", "Invert LCD", 0, 0, 1, hint = g15driver.HINT_SWITCH ) + +def show_preferences(device, parent, gconf_client): + widget_tree = gtk.Builder() + widget_tree.add_from_file(os.path.join(g15globals.ui_dir, "driver_g15.ui")) + g15uigconf.configure_spinner_from_gconf(gconf_client, "/apps/gnome15/%s/g15daemon_port" % device.uid, "Port", DEFAULT_PORT, widget_tree, False) + return widget_tree.get_object("DriverComponent") + +def fix_sans_style(root): + for element in root.iter(): + style = element.get("style") + if style != None: + element.set("style", style.replace("font-family:Sans","font-family:%s" % g15globals.fixed_size_font_name)) + +class G15Dispatcher(asyncore.dispatcher): + def __init__(self, map, conn, callback = None): + self.key_stage = 0 + self.out_buffer = "" + self.oob_buffer = "" + self.recv_buffer = "" + self.callback = callback; + self.reverse_map = {} + for k in KEY_MAP.keys(): + self.reverse_map[KEY_MAP[k]] = k + self.received_handshake = False + asyncore.dispatcher.__init__(self, sock=conn, map = map) + + def wait_for_handshake(self): + while not self.received_handshake: + time.sleep(0.5) + + def handle_close(self): + self.received_handshake = True + + def handle_expt(self): + data = self.socket.recv(1, socket.MSG_OOB) + if len(data) > 0: + val = ord(data[0]) + if val & CLIENT_CMD_BACKLIGHT: + level = val - CLIENT_CMD_BACKLIGHT + elif val & CLIENT_CMD_KB_BACKLIGHT: + level = val - CLIENT_CMD_KB_BACKLIGHT + elif val & CLIENT_CMD_CONTRAST: + logger.warning("Ignoring contrast command") + else: + logger.warning("Ignoring unknown OOB command") + + def handle_key(self, data): + if self.key_stage == 0: + self.last_key = struct.unpack("0: + self.recv_buffer += received + + # Have we collected enough for a key? + # TODO is this even neccesary, will we always get those 4 bytes when they are available + if self.received_handshake: + while len(self.recv_buffer) > 3: + data = self.get_data(4) + if data: + self.handle_key(data) + else: + data = self.get_data(16) + if data: + if data != "G15 daemon HELLO": + raise Exception("Excepted G15 daemon handshake.") + self.out_buffer = "GBUF" + self.received_handshake = True + except Exception as e: + self.oob_buffer = "" + self.out_buffer = "" + logger.debug("Error reading data from G15 daemon", exc_info = e) + raise e + + def get_data(self, required_length): + if len(self.recv_buffer) >= required_length: + data = self.recv_buffer[0:required_length] + self.recv_buffer = self.recv_buffer[required_length:] + return data + + def writable(self): + return len(self.oob_buffer) > 0 or len(self.out_buffer) > 0 + + def send_with_options(self, buffer, options = 0): + try: + return self.socket.send(buffer, options) + except socket.error, why: + self.oob_buffer = "" + if why.args[0] == EWOULDBLOCK: + return 0 + elif why.args[0] in (ECONNRESET, ENOTCONN, ESHUTDOWN, ECONNABORTED): + self.handle_close() + return 0 + else: + raise + + def handle_write(self): + if len(self.out_buffer) > 0: + sent = self.send(self.out_buffer) + self.out_buffer = self.out_buffer[sent:] + return sent + elif len(self.oob_buffer) > 0: + s = 0 + for c in self.oob_buffer: + s += self.send_with_options(c, socket.MSG_OOB) + self.oob_buffer = self.oob_buffer[s:] + + def convert_from_g15daemon_code(self, code): + keys = [] + for key in self.reverse_map: + if code & key != 0: + keys.append(self.reverse_map[key]) + return keys + +class G15Async(Thread): + def __init__(self, map): + Thread.__init__(self) + self.name = "G15Async" + self.setDaemon(True) + self.map = map + + def run(self): + asyncore.loop(timeout = 0.01, map = self.map) + +class Driver(g15driver.AbstractDriver): + + def __init__(self, device, on_close = None): + g15driver.AbstractDriver.__init__(self, "g15") + self.device = device + self.lock = Lock() + self.dispatcher = None + self.on_close = on_close + self.socket = None + self.connected = False + self.async = None + self.change_timer = None + self.conf_client = gconf.client_get_default() + + def get_size(self): + return self.device.lcd_size + + def get_bpp(self): + return self.device.bpp + + def get_controls(self): + return [ invert_control ] + + def get_antialias(self): + return cairo.ANTIALIAS_NONE + + def get_action_keys(self): + return self.device.action_keys + + def get_key_layout(self): + return self.device.key_layout + + def send(self, data, opt = None): + if opt == socket.MSG_OOB: + self.dispatcher.oob_buffer += data + else: + self.dispatcher.out_buffer += data + + def on_update_control(self, control): + pass + + def get_name(self): + return "mx5500tools driver" + + def get_model_names(self): + return [ g15driver.MODEL_MX5500 ] + + def get_model_name(self): + return self.device.model_id + + + def is_connected(self): + return self.connected + + def config_changed(self, client, connection_id, entry, args): + if self.change_timer != None: + self.change_timer.cancel() + self.change_timer = g15scheduler.schedule("ChangeG15DaemonConfiguration", 3.0, self.update_conf) + + def update_conf(self): + logger.info("Configuration changed") + if self.connected: + logger.info("Reconnecting") + self.disconnect() + self.connect() + + def grab_keyboard(self, callback): + self.dispatcher.callback = callback + self.send(chr(CLIENT_CMD_KEY_HANDLER),socket.MSG_OOB) + + def process_svg(self, document): + fix_sans_style(document.getroot()) + + def paint(self, img): + if not self.is_connected(): + return + + # Just return if the device has no LCD + if self.device.bpp == 0: + return None + + self.lock.acquire() + try : + size = self.get_size() + + # Paint to 565 image provided into an ARGB image surface for PIL's benefit. PIL doesn't support 565? + argb_surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, size[0], size[1]) + argb_context = cairo.Context(argb_surface) + argb_context.set_source_surface(img) + argb_context.paint() + + # Now convert the ARGB to a PIL image so it can be converted to a 1 bit monochrome image, with all + # colours dithered. It would be nice if Cairo could do this :( Any suggestions? + pil_img = Image.frombuffer("RGBA", size, argb_surface.get_data(), "raw", "RGBA", 0, 1) + pil_img = ImageMath.eval("convert(pil_img,'1')",pil_img=pil_img) + pil_img = ImageMath.eval("convert(pil_img,'P')",pil_img=pil_img) + pil_img = pil_img.point(lambda i: i >= 250,'1') + + invert_control = self.get_control("invert_lcd") + if invert_control.value == 0: + pil_img = pil_img.point(lambda i: 1^i) + + # Covert image buffer to string + buf = "" + for x in list(pil_img.getdata()): + buf += chr(x) + + if len(buf) != self.device.lcd_size[0] * self.device.lcd_size[1]: + logger.warning("Invalid buffer size") + else: + try : + self.send(buf) + except IOError as e: + logger.error("Failed to send buffer.", exc_info = e) + self.disconnect() + finally: + self.lock.release() + + def _on_disconnect(self): + if not self.is_connected(): + raise Exception("Already disconnected") + self.conf_client.notify_remove(self.notify_handle) + self.connected = False + if self.dispatcher != None: + self.dispatcher.running = False + self.socket.close() + self.socket = None + self.dispatcher = None + if self.on_close != None: + self.on_close(self) + + def _on_connect(self): + port = 15550 + e = self.conf_client.get("/apps/gnome15/%s/g15daemon_port" % self.device.uid) + if e: + port = e.get_int() + + map = {} + + self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.socket.settimeout(5.0) + self.socket.connect(("127.0.0.1", port)) + + self.dispatcher = G15Dispatcher(map, self.socket) + self.async = G15Async(map).start() + self.dispatcher.wait_for_handshake() + self.connected = True + + self.notify_handle = self.conf_client.notify_add("/apps/gnome15/%s/g15daemon_port" % self.device.uid, self.config_changed, None) \ No newline at end of file diff --git a/src/gnome15/drivers/fb.py b/src/gnome15/drivers/fb.py new file mode 100644 index 0000000..32016cb --- /dev/null +++ b/src/gnome15/drivers/fb.py @@ -0,0 +1,191 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2010 Brett Smith +# +# 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 . + +from ctypes import * +from fcntl import ioctl +import mmap +import os + +FBIOGET_VSCREENINFO=0x4600 +FBIOPUT_VSCREENINFO=0x4601 +FBIOGET_FSCREENINFO=0x4602 + +class fb_fix_screeninfo(Structure): + _fields_ = [ + ("id", c_char * 16), + ("smem_start", c_ulong), + ("smem_len", c_int, 32), + ("type", c_int, 32), + ("type_aux", c_int, 32), + ("visual", c_int, 32), + ("xpanstep", c_int, 16), + ("ypanstep", c_int, 16), + ("ywrapstep", c_int, 16), + ("line_length", c_int, 32), + ("mmio_start", c_ulong), + ("mmio_len", c_int, 32), + ("accel", c_int, 32), + ("reserved", c_ushort * 3), + ] + +class fb_bitfield(Structure): + _fields_ = [ + ("offset", c_int, 32), + ("length", c_int, 32), + ("msb_right", c_int, 32), + ] + + def __repr__(self): + return "bitfield [ offset = %d, length = %d, msb_right = %d ]" % ( self.offset, self.length, self.msb_right ) + +class fb_var_screeninfo(Structure): + _fields_ = [ + ( "xres", c_int, 32), + ( "yres", c_int, 32), + ( "xres_virtual", c_int, 32), + ( "yres_virtual", c_int, 32), + ( "xoffset", c_int, 32), + ( "yoffset", c_int, 32), + ( "bits_per_pixel", c_int, 32), + ( "grayscale", c_int, 32), + ( "red", fb_bitfield), + ( "green", fb_bitfield), + ( "blue", fb_bitfield), + ( "transp", fb_bitfield), + ( "nonstd", c_int, 32), + ( "activate", c_int, 32), + ( "height", c_int, 32), + ( "width", c_int, 32), + ( "accel_flags", c_int, 32), + ( "pixclock", c_int, 32), + ( "left_margin", c_int, 32), + ( "right_margin", c_int, 32), + ( "upper_margin", c_int, 32), + ( "lower_margin", c_int, 32), + ( "hsync_len", c_int, 32), + ( "vsync_len", c_int, 32), + ( "sync", c_int, 32), + ( "vmode", c_int, 32), + ( "rotate", c_int, 32), + ( "reserved", c_ulong * 5), + ] + +class fb_device(): + def __init__(self, device_name, mode = os.O_RDWR): + self.device_file = os.open(device_name, os.O_RDWR) + self.buffer = None + self.invalidate() + + def invalidate(self): + if self.buffer != None: + self.buffer().close() + self.buffer = mmap.mmap(self.device_file, self.get_screen_size(), mmap.MAP_SHARED, mmap.PROT_READ | mmap.PROT_WRITE) + + def get_fixed_info(self): + fixed_info = fb_fix_screeninfo() + if ioctl(self.device_file, FBIOGET_FSCREENINFO, fixed_info): + raise Exception("Error reading fixed information.\n") + return fixed_info + + def get_var_info(self): + variable_info = fb_var_screeninfo() + if ioctl(self.device_file, FBIOGET_VSCREENINFO, variable_info): + raise Exception("Error reading variable information.\n") + return variable_info + + def close(self): + if self.buffer != None: + self.buffer.close() + self.buffer = None + os.close(self.device_file) + + def __del__(self): + try: + self.close() + except Exception as e: + logger.debug('Error destroying fb_device.', exc_info = e) + pass + + def get_screen_size(self): + # fb_sys_write() in linux kernel 2.6.36 relies on this value + return self.get_fixed_info().smem_len + + # variable_info = self.get_var_info() + # return variable_info.xres * variable_info.yres * variable_info.bits_per_pixel / 8 + + + def dump(self): + + fixed_info = self.get_fixed_info() + + print "--------------" + print "Fixed" + print "--------------" + print "id:", fixed_info.id + print "smem_start:", fixed_info.smem_start + print "smem_len:", fixed_info.smem_len + print "type:", fixed_info.type + print "type_aux:", fixed_info.type_aux + print "visual:", fixed_info.visual + print "xpanstep:", fixed_info.xpanstep + print "ypanstep:", fixed_info.ypanstep + print "ywrapstep:", fixed_info.ywrapstep + print "line_length:", fixed_info.line_length + print "mmio_start:", fixed_info.mmio_start + print "mmio_len:", fixed_info.mmio_len + print "accel:", fixed_info.accel + + + variable_info = self.get_var_info() + + print "--------------" + print "Variable" + print "--------------" + print "xres:",variable_info.xres + print "yres:",variable_info.yres + print "xres_virtual:",variable_info.xres_virtual + print "yres_virtual:",variable_info.yres_virtual + print "xoffset:",variable_info.xoffset + print "yoffset:",variable_info.yoffset + print "bits_per_pixel:",variable_info.bits_per_pixel + print "grayscale:",variable_info.grayscale + print "red:",variable_info.red + print "green:",variable_info.green + print "blue:",variable_info.blue + print "transp:",variable_info.transp + print "activate:",variable_info.activate + print "height:",variable_info.height + print "width:",variable_info.width + print "accel_flags:",variable_info.accel_flags + print "pixclock:",variable_info.pixclock + print "left_margin:",variable_info.left_margin + print "right_margin:",variable_info.right_margin + print "update_margin:",variable_info.upper_margin + print "lower_margin:",variable_info.lower_margin + print "hsync_len:",variable_info.hsync_len + print "vsync_len:",variable_info.vsync_len + print "sync:",variable_info.sync + print "vmode:",variable_info.vmode + print "rotate:",variable_info.rotate + + +if __name__ == "__main__": + for d in os.listdir("/dev"): + if d.startswith("fb"): + print "---------",d + device = fb_device("/dev/%s" %d) + print "Screen bytes: " + str(device.get_screen_size()) + device.dump() diff --git a/src/gnome15/drivers/pylibg15.py b/src/gnome15/drivers/pylibg15.py new file mode 100644 index 0000000..bc9f7ff --- /dev/null +++ b/src/gnome15/drivers/pylibg15.py @@ -0,0 +1,187 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2011 Brett Smith +# +# 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 . + +import time +from ctypes import * +from threading import Thread + +libg15 = cdll.LoadLibrary("libg15.so.1") + +# Default key read timeout. Too low and keys will be missed (very obvious in a VM) +KEY_READ_TIMEOUT = 100 + +G15_LCD = 1 +G15_KEYS = 2 +G15_DEVICE_IS_SHARED = 4 +G15_DEVICE_5BYTE_RETURN = 8 +G15_DEVICE_G13 = 16 +G15_DEVICE_G510 = 32 + +G15_KEY_READ_LENGTH = 9 +G510_STANDARD_KEYBOARD_INTERFACE = 0x0 + +# Error codes +G15_NO_ERROR = 0 +G15_ERROR_READING_USB_DEVICE=4 +G15_TRY_AGAIN = 5 +G15_ERROR_NOENT = -2 +G15_ERROR_NODEV = -19 + +# Debug levels +G15_LOG_INFO = 1 +G15_LOG_WARN = 0 + +class KeyboardReceiveThread(Thread): + def __init__(self, callback, key_read_timeout, on_error): + Thread.__init__(self) + self._run = True + self.name = "KeyboardReceiveThread" + self.callback = callback + self.on_exit = None + self.on_unplug = None + self.key_read_timeout = key_read_timeout + self.on_error = on_error + + def deactivate(self): + if self._run: + self._run = False + + def run(self): + try: + pressed_keys = c_int(0) + while self._run: + err = libg15.getPressedKeys(byref(pressed_keys), 10) + code = 0 + ext_code = 0 + if err == G15_NO_ERROR: + if is_ext_key(pressed_keys.value): + ext_code = int(pressed_keys.value) + ext_code &= ~(1<<28) + err = libg15.getPressedKeys(byref(pressed_keys), 10) + if err == G15_NO_ERROR: + code = pressed_keys.value + elif err in [ G15_TRY_AGAIN, G15_ERROR_READING_USB_DEVICE ]: + pass + elif err == G15_ERROR_NODEV: + # Device unplugged + self._run = False + if self.on_unplug is not None: + self.on_unplug() + else: + if self.on_error is not None: + self.on_error(err) + break + else: + code = pressed_keys.value + + self.callback(code, ext_code) + elif err in [ G15_TRY_AGAIN, G15_ERROR_READING_USB_DEVICE ] : + continue + elif err == G15_ERROR_NODEV: + # Device unplugged + self._run = False + if self.on_unplug is not None: + self.on_unplug() + else: + if self.on_error is not None: + self.on_error(err) + break + + finally: + if self.on_exit is not None: + self.on_exit() + self._run = True + +class libg15_devices_t(Structure): + _fields_ = [ ("name", c_char_p), + ("vendorid", c_int), + ("productid", c_int), + ("caps", c_int) ] + +def is_ext_key(code): + """ + Get if the key code provide is an "Extended Key". Extended keys are used + to cope with libg15's restriction on the number of available codes, + which the G13 exceeds. + + Keyword arguments: + code -- code to test if extended + """ + return code & (1<<28) != 0 + +def grab_keyboard(callback, key_read_timeout = KEY_READ_TIMEOUT, on_error = None): + """ + Start polling for keyboard events. Device must be initialised. The thread + returned can be stopped by calling deactivate(). + + The callback is invoked with two arguments. The first being the bit mask + of any pressed non-extended codes. The second is the mask of any extended + key presses. + + Keyword arguments: + callback -- function to call on key event + key_read_timeout -- timeout for reading key presses. too low and keys will be missed + """ + t = KeyboardReceiveThread(callback, key_read_timeout, on_error) + t.start() + return t + +def init(init_usb = True, vendor_id = 0, product_id = 0): + """ + This one return G15_NO_ERROR on success, something + else otherwise (for instance G15_ERROR_OPENING_USB_DEVICE + """ + return libg15.setupLibG15(vendor_id, product_id, 1 if init_usb else 0) + +def reinit(): + """ re-initialise a previously unplugged keyboard ie ENODEV was returned at some point """ + return libg15.re_initLibG15() + + +def exit(): + return libg15.exitLibG15() + +def set_debug(level): + """ + Keyword arguments: + level -- level, one of G15_LOG_INFO or G15_LOG_WARN + """ + libg15.libg15Debug(level) + +def write_pixmap(data): + libg15.writePixmapToLCD(data) + +def set_contrast(level): + return libg15.setLCDContrast(level) + +def set_leds(leds): + return libg15.setLEDs(leds) + +def set_lcd_brightness(level): + return libg15.setLCDBrightness(level) + +def set_keyboard_brightness(level): + return libg15.setKBBrightness(level) + +def set_keyboard_color(color): + val = libg15.setG510LEDColor(color[0], color[1], color[2]) + return val + +def get_joystick_position(): + return ( libg15.getJoystickX(), libg15.getJoystickY() ) + +def __handle_key(code): + print "Got %d" %code \ No newline at end of file diff --git a/src/gnome15/g15accounts.py b/src/gnome15/g15accounts.py new file mode 100644 index 0000000..d0b5805 --- /dev/null +++ b/src/gnome15/g15accounts.py @@ -0,0 +1,538 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2012 Brett Smith +# +# 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 . + +""" +Classes that may be used by plugins that require account management, usually +to access some kind of network server such as Email, Calendar or Feeds. + +A GTK UI is also provided that subclasses may plug-in their own protocol specific +configuration widgets. + +Accounts are stored as simple XML files in .g +""" + +import os +from lxml import etree +import gtk +import g15globals +import util.g15scheduler as g15scheduler +import util.g15gconf as g15gconf +import util.g15os as g15os +import util.g15pythonlang as g15pythonlang +import pyinotify +import pwd +from threading import Lock +import gobject +import keyring + +import logging +logger = logging.getLogger(__name__) + +""" +Functions +""" + +""" +Configure monitoring of account files. This allows plugins to get notified +when accounts they are using change +""" +account_listeners = [] + +watch_manager = pyinotify.WatchManager() +mask = pyinotify.IN_DELETE | pyinotify.IN_MODIFY | pyinotify.IN_CREATE | pyinotify.IN_ATTRIB # watched events + +class EventHandler(pyinotify.ProcessEvent): + + def _notify(self, event): + for a in account_listeners: + a(event) + + def process_IN_MODIFY(self, event): + self._notify(event) + + def process_IN_CREATE(self, event): + self._notify(event) + + def process_IN_ATTRIB(self, event): + self._notify(event) + + def process_IN_DELETE(self, event): + self._notify(event) + +notifier = pyinotify.ThreadedNotifier(watch_manager, EventHandler()) +notifier.name = "AccountsPyInotify" +notifier.setDaemon(True) +notifier.start() + +CURRENT_USERNAME=pwd.getpwuid(os.getuid())[0] + +class Status(): + def __init__(self): + self.stopping = False + +STATUS = Status() + +''' +Helper classes for getting a secret from the keyring +''' +class G15Keyring(): + + def __init__(self): + self.lock = Lock() + self.password = None + + def get_username(self, account): + username = account.get_property("username", "") + return username if username != "" else CURRENT_USERNAME + + def get_uri_and_props(self, account, hostname = None, port = None): + + username = self.get_username(account) + name = account.type + "://" + username + + props = { + 'service':account.type, + 'username':username + } + + if hostname is not None: + name = name + "@" + hostname + props['server'] = hostname + if port is not None: + props['port'] = port + name = name + ":" + str(port) + + return props, name + + def store_password(self, account, password, hostname = None, port = None): + _, name = self.get_uri_and_props(account, hostname, port) + keyring.set_password(name, self.get_username(account), password) + + def find_secret(self, account, name, release_lock = True): + username = self.get_username(account) + try : + if STATUS.stopping: + self.password = None + return + + pw = keyring.get_password(name, username) + if pw is not None: + self.password = pw + return + + # Ask for the password + widget_tree = gtk.Builder() + widget_tree.add_from_file(os.path.join(g15globals.ui_dir, "password.ui")) + dialog = widget_tree.get_object("PasswordDialog") + text_widget = widget_tree.get_object("Text") + text_widget.set_text(_("The account %s for the user %s.\n\ +requires a password, This will be stored in the Gnome Keyring and \n\ +and will not be asked for again unless there is some later problem\n\ +problem authentication (for example as the result of\n\ +a password change).") % (account.name, username)) + text_widget.set_use_markup(True) + password_widget = widget_tree.get_object("Password") + dialog.show_all() + + response = dialog.run() + try : + if response == 1: + self.password = password_widget.get_text() + finally : + dialog.destroy() + + return + finally: + if release_lock: + self.lock.release() + + + def retrieve_password(self, account, hostname = None, port = None, force_dialog = False): + + _, name = self.get_uri_and_props(account, hostname, port) + + ''' + Find the item. It appears gnome keyring access must be run on the gobject loop? I don't + really understand the problem, but doing this seems to fix it + + TODO find out what is actually happening + ''' + if g15pythonlang.is_gobject_thread(): + self.find_secret(account, name, False) + else: + self.lock.acquire() + self.password = None + gobject.idle_add(self.find_secret, account, name) + self.lock.acquire() + self.lock.release() + if self.password != None: + return self.password + + +class G15AccountManager(G15Keyring): + """ + Manages the storage and loading of an account list. This is + stored as an XML file in the Gnome configuration directory + """ + + def __init__(self, file_path, item_name): + """ + Constructor + + Keyword arguments: + file_path -- location accounts are stored. Directory will be created if it does not exist + item_name -- name of item in XML file + """ + G15Keyring.__init__(self) + + self._conf_file = os.path.expanduser(file_path) + self.item_name = item_name + self.load() + self.listeners = {} + self.listener_functions = {} + + def add_change_listener(self, listener): + self.listeners[listener] = watch_manager.add_watch(os.path.dirname(self._conf_file), mask, rec=True) + def a(event): + self.load() + if event.pathname == self._conf_file: + listener(self) + self.listener_functions[listener] = a + account_listeners.append(a) + + def remove_change_listener(self, listener): + wdd = self.listeners[listener] + account_listeners.remove(self.listener_functions[listener]) + del self.listener_functions[listener] + for k in wdd: + try: + watch_manager.rm_watch(wdd[k],quiet = False) + except Exception as e: + logger.debug("Error removing change listener '%s'", str(k), exc_info = e) + pass + + def load(self): + accounts = [] + if not os.path.exists(self._conf_file): + dir_path = os.path.dirname(self._conf_file) + g15os.mkdir_p(dir_path) + else: + document = etree.parse(self._conf_file) + for element in document.getroot().xpath('//%s' % self.item_name): + acc = G15Account(element.get("name"), element.get("type")) + for property_element in element: + acc.properties[property_element.get("name")] = property_element.get("value") + accounts.append(acc) + + self.accounts = accounts + + + def by_name(self, name): + """ + Get an account given its name. + + Keyword arguments: + name -- account name + """ + for acc in self.accounts: + if acc.name == name: + return acc + + + def save(self): + """ + Save all accounts. + """ + root = etree.Element("xml") + document = etree.ElementTree(root) + for acc in self.accounts: + acc_el = etree.SubElement(root, self.item_name, type=acc.type, name=acc.name) + for key in acc.properties: + etree.SubElement(acc_el, "property", name=key, value=acc.properties[key]) + xml = etree.tostring(document) + fh = open(self._conf_file, "w") + try : + fh.write(xml) + finally : + fh.close() + +class G15Account(): + """ + A single account. An account has two main attributes, + a name and a type. All protocol specific details are + stored in the properties map. + """ + + def __init__(self, name, account_type): + """ + Constructor + + Keyword arguments: + name -- account name + account_type -- account type + """ + self.name = name + self.type = account_type + self.properties = {} + + def get_property(self, key, default_value=None): + return self.properties[key] if key in self.properties else default_value + + +class G15AccountOptions(): + """ + Superclass of the UI protocol specific configuration. + """ + + def __init__(self, account, account_ui): + """ + Constructor + + Keyword arguments: + account -- account + account_ui -- instance of G15AccountPreferences that contains the options widget + """ + self.account = account + self.account_ui = account_ui + +class G15AccountPreferences(): + """ + Configuration UI + """ + + + def __init__(self, parent, gconf_client, gconf_key, file_path, item_name, default_refresh = 60): + """ + Constructor + + Keyword arguments: + parent -- parent GTK component (for modality) + gconf_client -- gconf client + gconf_key -- gconf key prefix for this plugin + file_path -- location of accounts file + item_name -- name of item in XML files + """ + + self.gconf_client = gconf_client + self.gconf_key = gconf_key + self.visible_options = None + self._save_timer = None + self._adjusting = False + + self.account_mgr = G15AccountManager(file_path, item_name) + + + self.widget_tree = gtk.Builder() + self.widget_tree.add_from_file(os.path.join(g15globals.ui_dir, "accounts.ui")) + + # Models + self.type_model = self.widget_tree.get_object("TypeModel") + self.account_model = self.widget_tree.get_object("AccountModel") + self.type_model.clear() + for t in self.get_account_types(): + self.type_model.append([ t, self.get_account_type_name(t) ]) + + # Widgets + self.account_type = self.widget_tree.get_object("TypeCombo") + self.account_list = self.widget_tree.get_object("AccountList") + self.url_renderer = self.widget_tree.get_object("URLRenderer") + + # Updates + self.update_adjustment = self.widget_tree.get_object("UpdateAdjustment") + self.update_adjustment.set_value(g15gconf.get_int_or_default(gconf_client, gconf_key + "/update_time", default_refresh)) + + # Connect to events + self.account_list.connect("cursor-changed", self._select_account) + self.account_type.connect("changed", self._type_changed) + self.update_adjustment.connect("value-changed", self._update_time_changed) + self.url_renderer.connect("edited", self._url_edited) + self.widget_tree.get_object("NewAccount").connect("clicked", self._new_url) + self.widget_tree.get_object("RemoveAccount").connect("clicked", self._remove_url) + + # Configure widgets + self._reload_model() + self._select_account() + + # Hide non-relevant stuff + self.widget_tree.get_object("TypeContainer").set_visible(len(self.get_account_types()) > 1) + + # Additional options + place_holder = self.widget_tree.get_object("OptionsContainer") + opts = self.create_general_options() + if opts: + opts.reparent(place_holder) + + # Show dialog + dialog = self.widget_tree.get_object("AccountDialog") + dialog.set_transient_for(parent) + + ah = gconf_client.notify_add(gconf_key + "/urls", self._urls_changed); + dialog.run() + dialog.hide() + gconf_client.notify_remove(ah) + + """ + Implement + """ + def create_general_options(self): + """ + Create general options for the dialog. These are added to the area + beneath the refresh interval spinner + """ + pass + + def get_account_type_name(self, account_type): + """ + Get the localized account type name + + Keyword arguments: + account_type -- account type (always provided, will be same as account.type if exists) + """ + raise Exception("Not implemented") + + def get_account_types(self): + """ + Get the account types that are available + """ + raise Exception("Not implemented") + + def create_options_for_type(self, account, account_type): + """ + Create the concrete G15AccountOptions object given the account type name. + + Keyword arguments: + account -- account object (will be None if this is for a new account) + account_type -- account type (always provided, will be same as account.type if exists) + """ + raise Exception("Not implemented") + + """ + Private + """ + def save_accounts(self): + if not self._adjusting: + if self._save_timer is not None: + self._save_timer.cancel() + self._save_timer = g15scheduler.schedule("SaveAccounts", 2, self._do_save_accounts) + + def _do_save_accounts(self): + self.account_mgr.save() + + def _update_time_changed(self, widget): + self.gconf_client.set_int(self.gconf_key + "/update_time", int(widget.get_value())) + + def _url_edited(self, widget, row_index, value): + row = self.account_model[row_index] + if value != "": + acc = self.account_mgr.by_name(row[0]) + if acc == None: + acc = G15Account(value, self.get_account_types()[0]) + self.account_mgr.accounts.append(acc) + else: + acc.name = value + self.save_accounts() + self._reload_model() + self.account_list.get_selection().select_path(row_index) + self._select_account() + else: + acc = self.account_mgr.by_name(row[0]) + if acc is not None: + self.account_mgr.accounts.remove(acc) + self._reload_model() + + def _urls_changed(self, client, connection_id, entry, args): + self._reload_model() + + def _reload_model(self): + acc = self._get_selected_account() + self.account_model.clear() + for i in range(0, len(self.account_mgr.accounts)): + account = self.account_mgr.accounts[i] + row = [ account.name, True ] + self.account_model.append(row) + if account == acc: + self.account_list.get_selection().select_path(i) + + (model, sel) = self.account_list.get_selection().get_selected() + if sel == None: + self.account_list.get_selection().select_path(0) + + def _new_url(self, widget): + self.account_model.append(["", True]) + self.account_list.set_cursor_on_cell(str(len(self.account_model) - 1), focus_column=self.account_list.get_column(0), focus_cell=self.url_renderer, start_editing=True) +# self.account_list.grab_focus() + + def _remove_url(self, widget): + (model, path) = self.account_list.get_selection().get_selected() + url = model[path][0] + acc = self.account_mgr.by_name(url) + if acc is not None: + self.account_mgr.accounts.remove(acc) + self.save_accounts() + self._reload_model() + self._load_options_for_type() + + def _type_changed(self, widget): + sel = self._get_selected_type() + acc = self._get_selected_account() + if acc.type != sel: + acc.type = sel + self.save_accounts() + self._load_options_for_type() + + def _load_options_for_type(self): + account_type = self._get_selected_type() + acc = self._get_selected_account() + options = self.create_options_for_type(acc, account_type) if acc is not None else None + if self.visible_options != None: + self.visible_options.component.destroy() + self.visible_options = options + place_holder = self.widget_tree.get_object("PlaceHolder") + for c in place_holder.get_children(): + place_holder.remove(c) + if self.visible_options is not None: + self.visible_options.component.reparent(place_holder) + else: + l = gtk.Label("No options found for this account\ntype. Do you have all the required\nplugins installed?") + l.xalign = 0.5 + l.show() + place_holder.add(l) + + def _select_account(self, widget=None): + account = self._get_selected_account() + self._adjusting = True + if account != None: + self.account_type.set_sensitive(True) + self.widget_tree.get_object("PlaceHolder").set_visible(True) + for i in range(0, len(self.type_model)): + if self.type_model[i][0] == account.type: + self.account_type.set_active(i) + if self.account_type.get_active() == -1: + self.account_type.set_active(0) + self._load_options_for_type() + else: + self.account_type.set_sensitive(False) + self.widget_tree.get_object("PlaceHolder").set_visible(False) + self._adjusting = False + + def _get_selected_type(self): + active = self.account_type.get_active() + return None if active == -1 else self.type_model[active][0] + + def _get_selected_account(self): + (model, path) = self.account_list.get_selection().get_selected() + if path != None: + return self.account_mgr.by_name(model[path][0]) \ No newline at end of file diff --git a/src/gnome15/g15actions.py b/src/gnome15/g15actions.py new file mode 100644 index 0000000..979bb2a --- /dev/null +++ b/src/gnome15/g15actions.py @@ -0,0 +1,72 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2011 Brett Smith +# +# 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 . + +""" +Manages registration of 'actions'. Each device will support default bindings +to these actions based on the keys they have available. + +Additionally, plugins may register new actions that may be bound to macro +keys. +""" + +import g15driver + +""" +Some screen related actions that may be mapped to additional keys +""" +NEXT_SCREEN = "next-screen" +PREVIOUS_SCREEN = "previous-screen" +NEXT_BACKLIGHT = "next-backlight" +PREVIOUS_BACKLIGHT = "previous-backlight" +CANCEL_MACRO = "cancel-macro" + +""" +Global the plugins and other subsystems may add new actions too. The list +here is the minimum a device must support to be useful. +""" +actions = [ + g15driver.NEXT_SELECTION, + g15driver.PREVIOUS_SELECTION, + g15driver.NEXT_PAGE, + g15driver.PREVIOUS_PAGE, + g15driver.SELECT, + g15driver.VIEW, + g15driver.CLEAR, + g15driver.MENU, + g15driver.MEMORY_1, + g15driver.MEMORY_2, + g15driver.MEMORY_3, + NEXT_SCREEN, + PREVIOUS_SCREEN, + NEXT_BACKLIGHT, + PREVIOUS_BACKLIGHT, + CANCEL_MACRO + ] + + +class ActionBinding(): + """ + Created when an action is invoked and contains the keys that activated + the action (if any), the state they were in and the action ID + """ + def __init__(self, action, keys, state): + self.action = action + self.state = state + self.keys = keys + + def __cmp__(self, other): + f = cmp(self.keys, other.keys) + return f if f != 0 else cmp(self.state, other.state) \ No newline at end of file diff --git a/src/gnome15/g15config.py b/src/gnome15/g15config.py new file mode 100644 index 0000000..02da5d4 --- /dev/null +++ b/src/gnome15/g15config.py @@ -0,0 +1,1993 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2009-2012 Brett Smith +# Copyright (C) 2013 Gnome15 authors +# +# 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 . + +import gnome15.g15locale as g15locale +_ = g15locale.get_translation("gnome15").ugettext + +import pygtk +pygtk.require('2.0') +import gtk +import gobject +import pango +import dbus.service +import os +import sys +import g15globals +import g15profile +import gconf +import g15pluginmanager +import g15driver +import g15desktop +import g15drivermanager +import g15macroeditor +import g15devices +import util.g15convert as g15convert +import util.g15scheduler as g15scheduler +import util.g15uigconf as g15uigconf +import util.g15gconf as g15gconf +import util.g15os as g15os +import util.g15icontools as g15icontools +import g15theme +import colorpicker +import subprocess +import shutil +import zipfile +import time + +import logging +logger = logging.getLogger(__name__) + +# Upgrade +import g15upgrade +g15upgrade.upgrade() + +# Determine if appindicator is available, this decides the nature +# of the message displayed when the Gnome15 service is not running +HAS_APPINDICATOR=False +try : + import appindicator + appindicator.__path__ + HAS_APPINDICATOR=True +except Exception as e: + logger.debug('Could not load appindicator module', exc_info = e) + pass + +# Store the temporary profile icons here (for when the icon comes from a window, the filename is not known +icons_dir = os.path.join(g15globals.user_cache_dir, "macro_profiles") +g15os.mkdir_p(icons_dir) + +PALE_RED = gtk.gdk.Color(213, 65, 54) + + +BUS_NAME="org.gnome15.Configuration" +NAME="/org/gnome15/Config" +IF_NAME="org.gnome15.Config" + +STOPPED = 0 +STARTING = 1 +STARTED = 2 +STOPPING = 3 + +class G15ConfigService(dbus.service.Object): + """ + DBUS Service used to prevent g15-config from running more than once. Each run will + test if this service is available, if it is, then the Present function will be + called and the runtime exited. + """ + + def __init__(self, config): + self._config = config + bus_name = dbus.service.BusName(BUS_NAME, bus=config.session_bus, replace_existing=False, allow_replacement=False, do_not_queue=True) + dbus.service.Object.__init__(self, bus_name, NAME) + + @dbus.service.method(IF_NAME, in_signature='', out_signature='') + def Present(self): + self._config.main_window.present() + + @dbus.service.method(IF_NAME, in_signature='s', out_signature='') + def PresentWithDeviceUID(self, device_uid): + self._config._default_device_name = device_uid + self._config._load_devices() + self._config.main_window.present() + +class G15GlobalConfig: + + def __init__(self, parent, widget_tree, conf_client): + self.widget_tree = widget_tree + self.conf_client = conf_client + self.selected_id = None + + only_show_indicator_on_error = self.widget_tree.get_object("OnlyShowIndicatorOnError") + start_desktop_service_on_login = self.widget_tree.get_object("StartDesktopServiceOnLogin") + start_indicator_on_login = self.widget_tree.get_object("StartIndicatorOnLogin") + start_system_tray_on_login = self.widget_tree.get_object("StartSystemTrayOnLogin") + global_plugin_enabled_renderer = self.widget_tree.get_object("GlobalPluginEnabledRenderer") + enable_gnome_shell_extension = self.widget_tree.get_object("EnableGnomeShellExtension") + + self.dialog = self.widget_tree.get_object("GlobalOptionsDialog") + self.global_plugin_model = self.widget_tree.get_object("GlobalPluginModel") + self.global_plugin_tree = self.widget_tree.get_object("GlobalPluginTree") + self.global_plugin_tree.connect("cursor-changed", self._select_plugin) + + self.widget_tree.get_object("GlobalPreferencesButton").connect("clicked", self._show_preferences) + self.widget_tree.get_object("GlobalAboutPluginButton").connect("clicked", self._show_about_plugin) + start_desktop_service_on_login.connect("toggled", self._change_desktop_service, "gnome15") + start_indicator_on_login.connect("toggled", self._change_desktop_service, "g15-indicator") + start_system_tray_on_login.connect("toggled", self._change_desktop_service, "g15-systemtray") + enable_gnome_shell_extension.connect("toggled", self._change_gnome_shell_extension) + global_plugin_enabled_renderer.connect("toggled", self._toggle_plugin) + + # Service options + gnome_shell = g15desktop.get_desktop() == "gnome-shell" + shell_extension_installed = g15desktop.is_shell_extension_installed("gnome15-shell-extension@gnome15.org") + only_show_indicator_on_error.set_visible(g15desktop.is_desktop_application_installed("g15-indicator") and not gnome_shell) + start_indicator_on_login.set_visible(g15desktop.is_desktop_application_installed("g15-indicator") and not gnome_shell) + start_system_tray_on_login.set_visible(g15desktop.is_desktop_application_installed("g15-systemtray") and not gnome_shell) + enable_gnome_shell_extension.set_visible(gnome_shell and shell_extension_installed) + start_desktop_service_on_login.set_active(g15desktop.is_autostart_application("gnome15")) + start_indicator_on_login.set_active(g15desktop.is_autostart_application("g15-indicator")) + start_system_tray_on_login.set_active(g15desktop.is_autostart_application("g15-systemtray")) + enable_gnome_shell_extension.set_active(g15desktop.is_gnome_shell_extension_enabled("gnome15-shell-extension@gnome15.org")) + + self.dialog.set_transient_for(parent) + + def run(self): + notify_h = self.conf_client.notify_add("/apps/gnome15/global/plugins", self._plugins_changed) + # Plugins + self._load_plugins() + + if len(self.global_plugin_model) == 0: + self.widget_tree.get_object("GlobalPluginsFrame").set_visible(False) + self.dialog.set_size_request(-1, -1) + elif self._get_selected_plugin() == None: + self.global_plugin_tree.get_selection().select_path(self.global_plugin_model.get_path(self.global_plugin_model.get_iter(0))) + self._select_plugin() + + self.dialog.run() + self.dialog.hide() + self.conf_client.notify_remove(notify_h) + + def _show_about_plugin(self, widget): + plugin = self._get_selected_plugin() + dialog = self.widget_tree.get_object("AboutPluginDialog") + dialog.set_title("About %s" % plugin.name) + dialog.run() + dialog.hide() + + def _show_preferences(self, widget): + plugin = self._get_selected_plugin() + plugin.show_preferences(self.dialog, None, self.conf_client, "/apps/gnome15/global/plugins/%s" % plugin.id) + + def _get_selected_plugin(self): + (model, path) = self.global_plugin_tree.get_selection().get_selected() + if path != None: + return g15pluginmanager.get_module_for_id(model[path][3]) + + def _select_plugin(self, widget = None): + plugin = self._get_selected_plugin() + if plugin != None: + self.selected_id = plugin.id + self.widget_tree.get_object("GlobalPluginNameLabel").set_text(plugin.name) + self.widget_tree.get_object("GlobalDescriptionLabel").set_text(plugin.description) + self.widget_tree.get_object("GlobalDescriptionLabel").set_use_markup(True) + self.widget_tree.get_object("AuthorLabel").set_text(plugin.author) + self.widget_tree.get_object("SupportedLabel").set_text(", ".join(g15pluginmanager.get_supported_models(plugin)).upper()) + self.widget_tree.get_object("CopyrightLabel").set_text(plugin.copyright) + self.widget_tree.get_object("SiteLabel").set_uri(plugin.site) + self.widget_tree.get_object("SiteLabel").set_label(plugin.site) + self.widget_tree.get_object("GlobalPreferencesButton").set_sensitive(plugin.has_preferences) + self.widget_tree.get_object("GlobalPluginDetails").set_visible(True) + else: + self.widget_tree.get_object("GlobalPluginDetails").set_visible(False) + + def _load_plugins(self): + self.global_plugin_model.clear() + for mod in sorted(g15pluginmanager.imported_plugins, key=lambda key: key.name): + if g15pluginmanager.is_global_plugin(mod): + passive = g15pluginmanager.is_passive_plugin(mod) + enabled = passive or self.conf_client.get_bool("/apps/gnome15/global/plugins/%s/enabled" % mod.id ) + self.global_plugin_model.append([enabled, not passive, mod.name, mod.id]) + if mod.id == self.selected_id: + self.global_plugin_tree.get_selection().select_path(self.global_plugin_model.get_path(self.global_plugin_model.get_iter(len(self.global_plugin_model) - 1))) + + def _plugins_changed(self, client, connection_id, entry, args): + self._load_plugins() + + def _change_gnome_shell_extension(self, widget): + g15desktop.set_gnome_shell_extension_enabled("gnome15-shell-extension@gnome15.org", widget.get_active()) + + def _change_desktop_service(self, widget, application_name): + g15desktop.set_autostart_application(application_name, widget.get_active()) + + def _toggle_plugin(self, widget, path): + plugin = g15pluginmanager.get_module_for_id(self.global_plugin_model[path][3]) + if plugin != None: + key = "/apps/gnome15/global/plugins/%s/enabled" % plugin.id + self.conf_client.set_bool(key, not self.conf_client.get_bool(key)) + + +class G15Config: + + """ + Configuration user interface for Gnome15. Allows selection and configuration + of the device, macros and enabled plugins. + """ + + adjusting = False + + def __init__(self, parent_window=None, service=None, options=None): + self.parent_window = parent_window + self._options = options + self._controls_visible = False + self.profile_save_timer = None + self._signal_handles = [] + self.notify_handles = [] + self.control_notify_handles = [] + self.selected_id = None + self.service = service + self.conf_client = gconf.client_get_default() + self.rows = None + self.adjusting = False + self.gnome15_service = None + self.connected = False + self.color_button = None + self.screen_services = {} + self.state = STOPPED + self.driver = None + self.selected_device = None + self._last_no_devices = -1 + + # Load main Glade file + g15locale.get_translation("g15-config") + g15Config = os.path.join(g15globals.ui_dir, 'g15-config.ui') + self.widget_tree = gtk.Builder() + self.widget_tree.set_translation_domain("g15-config") + self.widget_tree.add_from_file(g15Config) + self.main_window = self.widget_tree.get_object("MainWindow") + + # Make sure there is only one g15config running + self.session_bus = dbus.SessionBus() + try : + G15ConfigService(self) + except dbus.exceptions.NameExistsException as e: + logger.debug("D-Bus service already running", exc_info = e) + if self._options is not None and self._options.device_uid != "": + self.session_bus.get_object(BUS_NAME, NAME).PresentWithDeviceUID(self._options.device_uid) + else: + self.session_bus.get_object(BUS_NAME, NAME).Present() + self.session_bus.close() + g15profile.notifier.stop() + sys.exit() + + # Get the initially selected device + self._default_device_name = self.conf_client.get_string("/apps/gnome15/config_device_name") \ + if self._options is None or self._options.device_uid == "" else self._options.device_uid + + # Widgets + self.site_label = self.widget_tree.get_object("SiteLabel") + self.cycle_screens = self.widget_tree.get_object("CycleScreens") + self.cycle_screens_options = self.widget_tree.get_object("CycleScreensOptions") + self.cycle_seconds = self.widget_tree.get_object("CycleAdjustment") + self.cycle_seconds_widget = self.widget_tree.get_object("CycleSeconds") + self.plugin_model = self.widget_tree.get_object("PluginModel") + self.plugin_tree = self.widget_tree.get_object("PluginTree") + self.plugin_enabled_renderer = self.widget_tree.get_object("PluginEnabledRenderer") + self.main_vbox = self.widget_tree.get_object("MainVBox") + self.profiles_tree = self.widget_tree.get_object("ProfilesTree") + self.profileNameColumn = self.widget_tree.get_object("ProfileName") + self.keyNameColumn = self.widget_tree.get_object("KeyName") + self.macroNameColumn = self.widget_tree.get_object("MacroName") + self.macro_list = self.widget_tree.get_object("MacroList") + self.application = self.widget_tree.get_object("ApplicationLocation") + self.m1 = self.widget_tree.get_object("M1") + self.m2 = self.widget_tree.get_object("M2") + self.m3 = self.widget_tree.get_object("M3") + self.window_model = self.widget_tree.get_object("WindowModel") + self.window_combo = self.widget_tree.get_object("WindowCombo") + self.window_entry = self.widget_tree.get_object("WindowEntry") + self.window_name = self.widget_tree.get_object("WindowName") + self.window_select = self.widget_tree.get_object("WindowSelect") + self.context_remove_profile = self.widget_tree.get_object("ContextRemoveProfile") + self.context_activate_profile = self.widget_tree.get_object("ContextActivateProfile") + self.context_lock_profile = self.widget_tree.get_object("LockProfile") + self.context_unlock_profile = self.widget_tree.get_object("UnlockProfile") + self.activate_on_focus = self.widget_tree.get_object("ActivateProfileOnFocusCheckbox") + self.macro_name_renderer = self.widget_tree.get_object("MacroNameRenderer") + self.profile_name_renderer = self.widget_tree.get_object("ProfileNameRenderer") + self.window_label = self.widget_tree.get_object("WindowLabel") + self.activate_by_default = self.widget_tree.get_object("ActivateByDefaultCheckbox") + self.send_delays = self.widget_tree.get_object("SendDelaysCheckbox") + self.fixed_delays = self.widget_tree.get_object("FixedDelaysCheckbox") + self.release_delay = self.widget_tree.get_object("ReleaseDelay") + self.press_delay = self.widget_tree.get_object("PressDelay") + self.press_delay_adjustment = self.widget_tree.get_object("PressDelayAdjustment") + self.release_delay_adjustment = self.widget_tree.get_object("ReleaseDelayAdjustment") + self.profile_icon = self.widget_tree.get_object("ProfileIcon") + self.background = self.widget_tree.get_object("Background") + self.background_label = self.widget_tree.get_object("BackgroundLabel") + self.icon_browse_button = self.widget_tree.get_object("BrowseForIcon") + self.background_browse_button = self.widget_tree.get_object("BrowseForBackground") + self.clear_icon_button = self.widget_tree.get_object("ClearIcon") + self.clear_background_button = self.widget_tree.get_object("ClearBackground") + self.macro_properties_button = self.widget_tree.get_object("MacroPropertiesButton") + self.new_macro_button = self.widget_tree.get_object("NewMacroButton") + self.delete_macro_button = self.widget_tree.get_object("DeleteMacroButton") + self.memory_bank_vbox = self.widget_tree.get_object("MemoryBankVBox") + self.macros_model = self.widget_tree.get_object("MacroModel") + self.mapped_key_model = self.widget_tree.get_object("MappedKeyModel") + self.profiles_model = self.widget_tree.get_object("ProfileModel") + self.profiles_context_menu = self.widget_tree.get_object("ProfileContextMenu") + self.device_model = self.widget_tree.get_object("DeviceModel") + self.device_view = self.widget_tree.get_object("DeviceView") + self.main_pane = self.widget_tree.get_object("MainPane") + self.main_parent = self.widget_tree.get_object("MainParent") + self.device_title = self.widget_tree.get_object("DeviceTitle") + self.device_enabled = self.widget_tree.get_object("DeviceEnabled") + self.tabs = self.widget_tree.get_object("Tabs") + self.stop_service_button = self.widget_tree.get_object("StopServiceButton") + self.driver_model = self.widget_tree.get_object("DriverModel") + self.driver_combo = self.widget_tree.get_object("DriverCombo") + self.global_options_button = self.widget_tree.get_object("GlobalOptionsButton") + self.macro_edit_close_button = self.widget_tree.get_object("MacroEditCloseButton") + self.key_table = self.widget_tree.get_object("KeyTable") + self.key_frame = self.widget_tree.get_object("KeyFrame") + self.memory_bank = self.widget_tree.get_object("MemoryBank") + self.macros_tab = self.widget_tree.get_object("MacrosTab") + self.macros_tab_label = self.widget_tree.get_object("MacrosTabLabel") + self.keyboard_tab = self.widget_tree.get_object("KeyboardTab") + self.plugins_tab = self.widget_tree.get_object("PluginsTab") + self.profile_plugins_tab = self.widget_tree.get_object("ProfilePluginsTab") + self.parent_profile_box = self.widget_tree.get_object("ParentProfileBox") + self.parent_profile_label = self.widget_tree.get_object("ParentProfileLabel") + self.parent_profile_model = self.widget_tree.get_object("ParentProfileModel") + self.parent_profile_combo = self.widget_tree.get_object("ParentProfileCombo") + self.profile_author = self.widget_tree.get_object("ProfileAuthor") + self.export_profile = self.widget_tree.get_object("Export") + self.import_profile = self.widget_tree.get_object("ImportButton") + self.information_content = self.widget_tree.get_object("InformationContent") + self.delays_content = self.widget_tree.get_object("DelaysContent") + self.activation_content = self.widget_tree.get_object("ActivationContent") + self.launch_pattern_box = self.widget_tree.get_object("LaunchPatternBox") + self.activate_on_launch = self.widget_tree.get_object("ActivateOnLaunch") + self.launch_pattern = self.widget_tree.get_object("LaunchPattern") + self.theme_model = self.widget_tree.get_object("ThemeModel") + self.theme_label = self.widget_tree.get_object("ThemeLabel") + self.theme_combo = self.widget_tree.get_object("ThemeCombo") + self.profile_plugins_mode_model = self.widget_tree.get_object("ProfilePluginsModeModel") + self.profile_plugins_mode = self.widget_tree.get_object("ProfilePluginsMode") + self.enabled_profile_plugins_model = self.widget_tree.get_object("EnabledProfilePluginsModel") + self.enabled_profile_plugins = self.widget_tree.get_object("EnabledProfilePlugins") + self.enabled_profile_plugins_renderer = self.widget_tree.get_object("EnabledProfilePluginsRenderer") + self.device_settings = self.widget_tree.get_object("DeviceSettings") + self.no_device_selected = self.widget_tree.get_object("NoDeviceSelected") + self.no_driver_available = self.widget_tree.get_object("NoDriverAvailable") + self.driver_options = self.widget_tree.get_object("DriverOptions") + + # Window + self.main_window.set_transient_for(self.parent_window) + self.main_window.set_icon_from_file(g15icontools.get_app_icon(self.conf_client, "gnome15")) + + # Monitor gconf + self.conf_client.add_dir("/apps/gnome15", gconf.CLIENT_PRELOAD_NONE) + + # Monitor macro profiles changing + g15profile.profile_listeners.append(self._profiles_changed) + + # Configure widgets + self.profiles_tree.get_selection().set_mode(gtk.SELECTION_SINGLE) + self.macro_list.get_selection().set_mode(gtk.SELECTION_SINGLE) + + # Indicator options + # TODO move this out of here + g15uigconf.configure_checkbox_from_gconf(self.conf_client, "/apps/gnome15/indicate_only_on_error", "OnlyShowIndicatorOnError", False, self.widget_tree, True) + + # Bind to events + self.cycle_seconds.connect("value-changed", self._cycle_seconds_changed) + self.cycle_screens.connect("toggled", self._cycle_screens_changed) + self.site_label.connect("activate", self._open_site) + self.plugin_tree.connect("cursor-changed", self._select_plugin) + self.plugin_enabled_renderer.connect("toggled", self._toggle_plugin) + self.enabled_profile_plugins_renderer.connect("toggled", self._toggle_enabled_profile_plugins) + self.widget_tree.get_object("PreferencesButton").connect("clicked", self._show_preferences) + self.widget_tree.get_object("AboutPluginButton").connect("clicked", self._show_about_plugin) + self.widget_tree.get_object("AddButton").connect("clicked", self._add_profile) + self.widget_tree.get_object("ContextDuplicateProfile").connect("activate", self._copy_profile) + self.context_activate_profile.connect("activate", self._activate) + self.widget_tree.get_object("ContextExportProfile").connect("activate", self._export) + self.context_unlock_profile.connect("activate", self._unlock_profile) + self.context_lock_profile.connect("activate", self._lock_profile) + self.activate_on_focus.connect("toggled", self._activate_on_focus_changed) + self.activate_by_default.connect("toggled", self._activate_on_focus_changed) + self.clear_icon_button.connect("clicked", self._clear_icon) + self.clear_background_button.connect("clicked", self._clear_icon) + self.delete_macro_button.connect("clicked", self._remove_macro) + self.icon_browse_button.connect("clicked", self._browse_for_icon) + self.background_browse_button.connect("clicked", self._browse_for_icon) + self.macro_properties_button.connect("clicked", self._macro_properties) + self.new_macro_button.connect("clicked", self._new_macro) + self.macro_list.connect("cursor-changed", self._select_macro) + self.macro_name_renderer.connect("edited", self._macro_name_edited) + self.profile_name_renderer.connect("edited", self._profile_name_edited) + self.m1.connect("toggled", self._memory_changed) + self.m2.connect("toggled", self._memory_changed) + self.m3.connect("toggled", self._memory_changed) + self.profiles_tree.connect("cursor-changed", self._select_profile) + self.profiles_tree.connect("button-press-event", self._show_profile_list_context) + self.context_remove_profile.connect("activate", self._remove_profile) + self.send_delays.connect("toggled", self._send_delays_changed) + self.fixed_delays.connect("toggled", self._send_delays_changed) + self.press_delay_adjustment.connect("value-changed", self._send_delays_changed) + self.release_delay_adjustment.connect("value-changed", self._send_delays_changed) + self.window_select.connect("clicked", self._select_window) + self.window_name.connect("changed", self._window_name_changed) + self.window_combo.connect("changed", self._window_name_changed) + self.parent_profile_combo.connect("changed", self._parent_profile_changed) + self.m1.connect("toggled", self._memory_changed) + self.profile_author.connect("changed", self._profile_author_changed) + self.stop_service_button.connect("clicked", self._stop_service) + self.export_profile.connect("clicked", self._export) + self.device_view.connect("selection-changed", self._device_selection_changed) + self.device_enabled.connect("toggled", self._device_enabled_changed) + self.driver_combo.connect("changed", self._driver_changed) + self.theme_combo.connect("changed", self._theme_changed) + self.profile_plugins_mode.connect("changed", self._profile_plugins_mode_changed) + self.global_options_button.connect("clicked", self._show_global_options) + self.macro_list.add_events(gtk.gdk.BUTTON_PRESS_MASK) + self.macro_list.connect("button_press_event", self._macro_list_clicked) + self.import_profile.connect("clicked", self._import_profile) + self.driver_options.connect('clicked', self._show_driver_options) + + # Enable profiles to be dropped onto the list + self.macro_list.enable_model_drag_dest([('text/plain', 0, 0)], + gtk.gdk.ACTION_DEFAULT | gtk.gdk.ACTION_COPY) + self.macro_list.connect("drag-data-received", self._macro_profile_dropped) + + # Connection to BAMF for running applications list + try : + bamf_object = self.session_bus.get_object('org.ayatana.bamf', '/org/ayatana/bamf/matcher') + self.bamf_matcher = dbus.Interface(bamf_object, 'org.ayatana.bamf.matcher') + except Exception as e: + logger.warning("BAMF not available, falling back to WNCK", exc_info = e) + self.bamf_matcher = None + import wnck + self.screen = wnck.screen_get_default() + + # Show infobar component to start desktop service if it is not running + self.infobar = gtk.InfoBar() + self.infobar.set_size_request(-1, 64) + self.warning_label = gtk.Label() + self.warning_label.set_size_request(400, -1) + self.warning_label.set_line_wrap(True) + self.warning_label.set_alignment(0.0, 0.0) + self.warning_image = gtk.Image() + + # Start button + self.stop_service_button.set_sensitive(False) + button_vbox = gtk.VBox() + self.start_button = None + self.start_button = gtk.Button(_("Start Service")) + self.start_button.connect("clicked", self._start_service) + self.start_button.show() + button_vbox.pack_start(self.start_button, False, False) + + # Populate model and configure other components + self._load_devices() + if len(self.device_model) == 0: + raise Exception(_("No supported devices could be found. Is the " + \ + "device correctly plugged in and powered and " + \ + "do you have all the required drivers installed?")) + else: + if len(self.device_model) == 1 and not g15devices.is_enabled(self.conf_client, self.selected_device): + self.device_enabled.set_active(True) + + # Build the infobar content + content = self.infobar.get_content_area() + content.pack_start(self.warning_image, False, False) + content.pack_start(self.warning_label, True, True) + content.pack_start(button_vbox, False, False) + + # Add the bar to the glade built UI + self.main_vbox.pack_start(self.infobar, False, False) + self.warning_box_shown = False + self.infobar.hide_all() + + self.gnome15_service = None + + # Watch for Gnome15 starting and stopping + try : + self._connect() + except dbus.exceptions.DBusException as e: + logger.debug("Failed to connect to service.", exc_info = e) + self._disconnect() + self.session_bus.add_signal_receiver(self._name_owner_changed, + dbus_interface='org.freedesktop.DBus', + signal_name='NameOwnerChanged') + + # Watch for new devices (if pyudev is installed) + g15devices.device_added_listeners.append(self._devices_changed) + g15devices.device_removed_listeners.append(self._devices_changed) + + def run(self): + ''' Set up everything and display the window + ''' + if len(self.devices) > 1: + self.main_window.set_size_request(800, 600) + else: + self.main_window.set_size_request(640, 600) + self.id = None + while True: + opt = self.main_window.run() + logger.debug("Option %s", str(opt)) + if opt != 1 and opt != 2: + break + + self.main_window.hide() + g15profile.notifier.stop() + + ''' + Private + ''' + def _devices_changed(self, device = None): + self._load_devices() + + def _name_owner_changed(self, name, old_owner, new_owner): + if name == "org.gnome15.Gnome15": + if old_owner == "" and not self.connected: + self._connect() + elif old_owner != "" and self.connected: + self._disconnect() + + def __del__(self): + self._remove_notify_handles() + + def _remove_notify_handles(self): + for h in self.notify_handles: + self.conf_client.notify_remove(h) + self.notify_handles = [] + + def _stop_service(self, event = None): + self.gnome15_service.Stop(reply_handler = self._general_dbus_reply, error_handler = self._general_dbus_error) + + def _general_dbus_reply(self, *args): + logger.info("DBUS reply %s", str(args)) + + def _general_dbus_error(self, *args): + logger.error("DBUS error %s", str(args)) + + def _starting(self): + logger.debug("Got starting signal") + self.state = STARTING + self._status_change() + + def _started(self): + logger.debug("Got started signal") + self.state = STARTED + self._status_change() + + def _stopping(self): + logger.debug("Got stopping signal") + self.state = STOPPING + self._status_change() + + def _stopped(self): + logger.debug("Got stopped signal") + self.state = STOPPED + self._status_change() + + def _disconnect(self): + for sig in self._signal_handles: + self.session_bus.remove_signal_receiver(sig) + self._signal_handles = [] + self.screen_services = {} + self.state = STOPPED + self._do_status_change() + self.connected = False + + def _connect(self): + self.gnome15_service = self.session_bus.get_object('org.gnome15.Gnome15', '/org/gnome15/Service') + + # Set initial status + logger.debug("Getting state") + if self.gnome15_service.IsStarting(): + logger.debug("State is starting") + self.state = STARTING + elif self.gnome15_service.IsStopping(): + logger.debug("State is stopping") + self.state = STOPPING + else: + logger.debug("State is started") + self.state = STARTED + for screen_name in self.gnome15_service.GetScreens(): + logger.debug("Screen added %s", screen_name) + screen_service = self.session_bus.get_object('org.gnome15.Gnome15', screen_name) + self.screen_services[screen_name] = screen_service + + # Watch for changes + self._signal_handles.append(self.session_bus.add_signal_receiver(self._starting, dbus_interface="org.gnome15.Service", signal_name='Starting')) + self._signal_handles.append(self.session_bus.add_signal_receiver(self._started, dbus_interface="org.gnome15.Service", signal_name='Started')) + self._signal_handles.append(self.session_bus.add_signal_receiver(self._stopping, dbus_interface="org.gnome15.Service", signal_name='Stopping')) + self._signal_handles.append(self.session_bus.add_signal_receiver(self._stopped, dbus_interface="org.gnome15.Service", signal_name='Stopped')) + self._signal_handles.append(self.session_bus.add_signal_receiver(self._screen_added, dbus_interface="org.gnome15.Service", signal_name='ScreenAdded')) + self._signal_handles.append(self.session_bus.add_signal_receiver(self._screen_removed, dbus_interface="org.gnome15.Service", signal_name='ScreenRemoved')) + self._signal_handles.append(self.session_bus.add_signal_receiver(self._status_change, dbus_interface="org.gnome15.Screen", signal_name='Connected')) + self._signal_handles.append(self.session_bus.add_signal_receiver(self._status_change, dbus_interface="org.gnome15.Screen", signal_name='ConnectionFailed')) + self._signal_handles.append(self.session_bus.add_signal_receiver(self._status_change, dbus_interface="org.gnome15.Screen", signal_name='Disconnected')) + self.connected = True + self._do_status_change() + + def _screen_added(self, screen_name): + logger.debug("Screen added %s", screen_name) + screen_service = self.session_bus.get_object('org.gnome15.Gnome15', screen_name) + self.screen_services[screen_name] = screen_service + gobject.idle_add(self._do_status_change) + + def _screen_removed(self, screen_name): + logger.debug("Screen removed %s", screen_name) + if screen_name in self.screen_services: + del self.screen_services[screen_name] + self._do_status_change() + + def _status_change(self, arg1 = None, arg2 = None, arg3 = None): + gobject.idle_add(self._do_status_change) + + def _do_status_change(self): + if not self.gnome15_service or self.state == STOPPED: + self.stop_service_button.set_sensitive(False) + logger.debug("Stopped") + self._show_message(gtk.MESSAGE_WARNING, _("The Gnome15 desktop service is not running. It is recommended " + \ + "you add g15-desktop-service as a Startup Application.")) + elif self.state == STARTING: + logger.debug("Starting up") + self.stop_service_button.set_sensitive(False) + self._show_message(gtk.MESSAGE_WARNING, _("The Gnome15 desktop service is starting up. Please wait"), False) + elif self.state == STOPPING: + logger.debug("Stopping") + self.stop_service_button.set_sensitive(False) + self._show_message(gtk.MESSAGE_WARNING, _("The Gnome15 desktop service is stopping."), False) + else: + logger.debug("Started - Checking status") + connected = 0 + first_error = "" + for screen in self.screen_services: + try: + if self.screen_services[screen].IsConnected(): + connected += 1 + else: + first_error = self.screen_services[screen].GetLastError() + except dbus.DBusException as e: + logger.debug("D-Bus communication error", exc_info = e) + pass + + logger.debug("Found %d of %d connected", connected, len(self.screen_services)) + screen_count = len(self.screen_services) + if connected != screen_count and first_error is not None and first_error != "": + if len(self.screen_services) == 1: + self._show_message(gtk.MESSAGE_WARNING, _("The Gnome15 desktop service is running, but failed to connect " + \ + "to the keyboard driver. The error message given was %s") % first_error, False) + else: + mesg = ("The Gnome15 desktop service is running, but only %d out of %d keyboards are connected. The first error message given was %s") % ( connected, screen_count, first_error ) + self._show_message(gtk.MESSAGE_WARNING, mesg, False) + else: + self._hide_warning() + self.stop_service_button.set_sensitive(True) + + def _hide_warning(self): + self.warning_box_shown = False + self.infobar.hide_all() + self.main_window.check_resize() + + def _start_service(self, widget): + widget.set_sensitive(False) + g15os.run_script("g15-desktop-service", ["-f"]) + + def _show_message(self, type, text, start_service_button = True): + self.infobar.set_message_type(type) + if self.start_button != None: + self.start_button.set_sensitive(True) + self.start_button.set_visible(start_service_button) + self.warning_label.set_text(text) + self.warning_label.set_use_markup(True) + + if type == gtk.MESSAGE_WARNING: + self.warning_image.set_from_stock(gtk.STOCK_DIALOG_WARNING, gtk.ICON_SIZE_DIALOG) +# self.warning_label.modify_fg(gtk.STATE_NORMAL, gtk.gdk.Color(0, 0, 0)) + + self.main_window.check_resize() + self.infobar.show_all() + if self.start_button != None and not start_service_button: + self.start_button.hide() + self.warning_box_shown = True + + def _open_site(self, widget): + subprocess.Popen(['xdg-open',widget.get_uri()]) + + def _to_rgb(self, string_rgb): + rgb = string_rgb.split(",") + return (int(rgb[0]), int(rgb[1]), int(rgb[2])) + + def _to_color(self, rgb): + return gtk.gdk.Color(rgb[0] <<8, rgb[1] <<8,rgb[2] <<8) + + def _color_chosen(self, widget, control): + color = widget.color + self.conf_client.set_string(self._get_full_key(control.id), "%d,%d,%d" % ( color[0],color[1],color[2])) + + def _control_changed(self, widget, control): + if control.hint & g15driver.HINT_SWITCH != 0: + val = 0 + if widget.get_active(): + val = 1 + self.conf_client.set_int(self._get_full_key(control.id), val) + else: + self.conf_client.set_int(self._get_full_key(control.id), int(widget.get_value())) + + def _show_preferences(self, widget): + plugin = self._get_selected_plugin() + plugin.show_preferences(self.main_window, self.driver, self.conf_client, self._get_full_key("plugins/%s" % plugin.id)) + + def _show_about_plugin(self, widget): + plugin = self._get_selected_plugin() + dialog = self.widget_tree.get_object("AboutPluginDialog") + dialog.set_title("About %s" % plugin.name) + dialog.run() + dialog.hide() + + def _load_macro_state(self): + device_info = g15devices.get_device_info(self.driver.get_model_name()) if self.driver is not None else None + self.macros_tab.set_visible(device_info is not None and device_info.macros) + self.macros_tab_label.set_visible(device_info is not None and device_info.macros) + + # Hide memory bank if there are no M-Keys + self.memory_bank.set_visible(self.driver != None and self.driver.has_memory_bank()) + + def _load_plugins(self): + """ + Loads what drivers and plugins are appropriate for the selected + device + """ + self.plugin_model.clear() + if self.selected_device: + # Plugins appropriate + for mod in sorted(g15pluginmanager.imported_plugins, key=lambda key: key.name): + key = self._get_full_key("plugins/%s/enabled" % mod.id ) + if self.driver and self.driver.get_model_name() in g15pluginmanager.get_supported_models(mod) and not g15pluginmanager.is_global_plugin(mod): + enabled = self.conf_client.get_bool(key) + self.plugin_model.append([enabled, mod.name, mod.id]) + if mod.id == self.selected_id: + self.plugin_tree.get_selection().select_path(self.plugin_model.get_path(self.plugin_model.get_iter(len(self.plugin_model) - 1))) + if len(self.plugin_model) > 0 and self._get_selected_plugin() == None: + self.plugin_tree.get_selection().select_path(self.plugin_model.get_path(self.plugin_model.get_iter(0))) + + self._select_plugin(None) + self._set_tab_status() + + def _load_drivers(self): + self.driver_model.clear() + if self.selected_device: + for driver_mod_key in list(g15drivermanager.imported_drivers): + driver_mod = g15drivermanager.imported_drivers[driver_mod_key] + try: + driver = driver_mod.Driver(self.selected_device) + if self.selected_device.model_id in driver.get_model_names(): + self.driver_model.append((driver_mod.id, driver_mod.name)) + except Exception as e: + logger.info("Failed to load driver.", exc_info = e) + + self.driver_combo.set_sensitive(len(self.driver_model) > 1) + self._set_driver_from_configuration() + + def _get_selected_plugin(self): + (model, path) = self.plugin_tree.get_selection().get_selected() + if path != None: + return g15pluginmanager.get_module_for_id(model[path][2]) + + def _toggle_enabled_profile_plugins(self, widget, path): + row = self.enabled_profile_plugins_model[path] + plugin_id = row[2] + plugin = g15pluginmanager.get_module_for_id(plugin_id) + if plugin != None: + if plugin.id in self.selected_profile.selected_plugins: + self.selected_profile.selected_plugins.remove(plugin.id) + else: + self.selected_profile.selected_plugins.append(plugin.id) + self._load_enabled_profile_plugins() + self._save_profile(self.selected_profile) + + def _toggle_plugin(self, widget, path): + plugin = g15pluginmanager.get_module_for_id(self.plugin_model[path][2]) + if plugin != None: + key = self._get_full_key("plugins/%s/enabled" % plugin.id ) + self.conf_client.set_bool(key, not self.conf_client.get_bool(key)) + + def _select_plugin(self, widget): + plugin = self._get_selected_plugin() + if plugin != None: + self.selected_id = plugin.id + self.widget_tree.get_object("PluginNameLabel").set_text(plugin.name) + self.widget_tree.get_object("DescriptionLabel").set_text(plugin.description) + self.widget_tree.get_object("DescriptionLabel").set_use_markup(True) + self.widget_tree.get_object("AuthorLabel").set_text(plugin.author) + self.widget_tree.get_object("SupportedLabel").set_text(", ".join(g15pluginmanager.get_supported_models(plugin)).upper()) + self.widget_tree.get_object("CopyrightLabel").set_text(plugin.copyright) + self.widget_tree.get_object("SiteLabel").set_uri(plugin.site) + self.widget_tree.get_object("SiteLabel").set_label(plugin.site) + self.widget_tree.get_object("PreferencesButton").set_sensitive(plugin.has_preferences and self.driver is not None) + self.widget_tree.get_object("PluginDetails").set_visible(True) + + themes = g15theme.get_themes(self.selected_device.model_id, plugin) + self.theme_model.clear() + if len(themes) > 1: + key = self._get_full_key("plugins/%s/theme" % plugin.id ) + plugin_theme = self.conf_client.get_string(key) + if plugin_theme is None: + plugin_theme = "default" + for i, t in enumerate(themes): + self.theme_model.append([t.theme_id,t.name]) + if t.theme_id == plugin_theme: + self.theme_combo.set_active(i) + self.theme_label.set_visible(True) + self.theme_combo.set_visible(True) + else: + self.theme_label.set_visible(False) + self.theme_combo.set_visible(False) + else: + self.widget_tree.get_object("PluginDetails").set_visible(False) + + # List the keys that are required for each action + for c in self.key_table.get_children(): + self.key_table.remove(c) + actions = g15pluginmanager.get_actions(plugin, self.selected_device) + rows = len(actions) + if rows > 0: + self.key_table.set_property("n-rows", rows) + row = 0 + active_profile = g15profile.get_active_profile(self.driver.device) if self.driver is not None else None + if active_profile is None: + logger.warning("No active profile found. It's possible the profile no longer exists, or is supplied with a plugin that cannot be found.") + else: + bindings = [] + for action_id in actions: + # First try the active profile to see if the action has been re-mapped + action_binding = None + for state in [ g15driver.KEY_STATE_UP, g15driver.KEY_STATE_HELD ]: + action_binding = active_profile.get_binding_for_action(state, action_id) + if action_binding is None: + # No other keys bound to action, try the device defaults + device_info = g15devices.get_device_info(self.driver.get_model_name()) + if action_id in device_info.action_keys: + action_binding = device_info.action_keys[action_id] + break + else: + break + + if action_binding is not None: + bindings.append(action_binding) + else: + logger.warning("Plugin %s requires an action that is not available (%s)", + plugin.id, action_id) + + bindings = sorted(bindings) + + for action_binding in bindings: + # If hold + label = gtk.Label("") + label.set_size_request(40, -1) + if action_binding.state == g15driver.KEY_STATE_HELD: + label.set_text(_("Hold")) + label.set_use_markup(True) + label.set_alignment(0.0, 0.5) + self.key_table.attach(label, 0, 1, row, row + 1, xoptions = gtk.FILL, xpadding = 4, ypadding = 2); + label.show() + + # Keys + keys = gtk.HBox(spacing = 4) + for k in action_binding.keys: + fname = os.path.abspath("%s/key-%s.png" % (g15globals.image_dir, k)) + pixbuf = gtk.gdk.pixbuf_new_from_file(fname) + pixbuf = pixbuf.scale_simple(22, 14, gtk.gdk.INTERP_BILINEAR) + img = gtk.image_new_from_pixbuf(pixbuf) + img.show() + keys.add(img) + keys.show() + self.key_table.attach(keys, 1, 2, row, row + 1, xoptions = gtk.FILL, xpadding = 4, ypadding = 2) + + # Text + label = gtk.Label(actions[action_binding.action]) + label.set_alignment(0.0, 0.5) + label.show() + self.key_table.attach(label, 2, 3, row, row + 1, xoptions = gtk.FILL, xpadding = 4, ypadding = 2) + row += 1 + + + if row > 0: + self.key_frame.set_visible(True) + else: + self.key_frame.set_visible(False) + + def _macro_profile_dropped(self, widget, context, x, y, selection, info, timestamp): +# print '\n'.join([str(t) for t in context.targets]) + return True + + def _set_cycle_seconds_value_from_configuration(self): + val = self.conf_client.get(self._get_full_key("cycle_seconds")) + time = 10 + if val != None: + time = val.get_int() + if time != self.cycle_seconds.get_value(): + self.cycle_seconds.set_value(time) + + def _set_cycle_screens_value_from_configuration(self): + val = g15gconf.get_bool_or_default(self.conf_client, self._get_full_key("cycle_screens"), True) + self.cycle_seconds_widget.set_sensitive(val) + if val != self.cycle_screens.get_active(): + self.cycle_screens.set_active(val) + + def _control_configuration_changed(self, client, connection_id, entry, args): + widget = args[1] + control = args[0] + if isinstance(control.value, int): + if control.hint & g15driver.HINT_SWITCH != 0: + widget.set_active(entry.value.get_int() == 1) + else: + widget.set_value(entry.value.get_int()) + else: + widget.set_color(self._to_rgb(entry.value.get_string())) + + def _cycle_screens_configuration_changed(self, client, connection_id, entry, args): + self._set_cycle_screens_value_from_configuration() + + def _cycle_seconds_configuration_changed(self, client, connection_id, entry, args): + self._set_cycle_seconds_value_from_configuration() + + def _plugins_changed(self, client, connection_id, entry, args): + self._load_plugins() + self._load_macro_state() + self._load_drivers() + self._load_enabled_profile_plugins() + + def _cycle_screens_changed(self, widget=None): + self.conf_client.set_bool(self._get_full_key("cycle_screens"), self.cycle_screens.get_active()) + + def _cycle_seconds_changed(self, widget): + val = int(self.cycle_seconds.get_value()) + self.conf_client.set_int(self._get_full_key("cycle_seconds"), val) + + def _create_color_icon(self, color): + draw = gtk.Image() + pixmap = gtk.gdk.Pixmap(None, 16, 16, 24) + cr = pixmap.cairo_create() + cr.set_source_rgb(float(color[0]) / 255.0, float(color[1]) / 255.0, float(color[2]) / 255.0) + cr.rectangle(0, 0, 16, 16) + cr.fill() + draw.set_from_pixmap(pixmap, None) + return draw + + def _active_profile_changed(self, client, connection_id, entry, args): + self._load_profile_list() + + def _send_delays_changed(self, widget=None): + if not self.adjusting: + self.selected_profile.send_delays = self.send_delays.get_active() + self.selected_profile.fixed_delays = self.fixed_delays.get_active() + self.selected_profile.press_delay = int(self.press_delay_adjustment.get_value() * 1000) + self.selected_profile.release_delay = int(self.release_delay_adjustment.get_value() * 1000) + self._save_profile(self.selected_profile) + self._set_delay_state() + + def _set_delay_state(self): + self.fixed_delays.set_sensitive(self.selected_profile.send_delays) + self.press_delay.set_sensitive(self.selected_profile.fixed_delays and self.selected_profile.send_delays) + self.release_delay.set_sensitive(self.selected_profile.fixed_delays and self.selected_profile.send_delays) + + def _activate_on_focus_changed(self, widget=None): + if not self.adjusting: + self.selected_profile.activate_on_focus = widget.get_active() + self._set_available_profile_actions() + self._save_profile(self.selected_profile) + + def _parent_profile_changed(self, widget): + if not self.adjusting: + sel = self.parent_profile_combo.get_active() + self.selected_profile.base_profile = self.parent_profile_model[sel][0] if sel > 0 else None + self._save_profile(self.selected_profile) + + def _window_name_changed(self, widget): + if isinstance(widget, gtk.ComboBox): + active = widget.get_active() + if active >= 0: + self.window_name.set_text(self.window_model[active][0]) + else: + if widget.get_text() != self.selected_profile.window_name: + self.selected_profile.window_name = widget.get_text() + if self.bamf_matcher != None: + for window in self.bamf_matcher.RunningApplications(): + app = self.session_bus.get_object("org.ayatana.bamf", window) + view = dbus.Interface(app, 'org.ayatana.bamf.view') + if view.Name() == self.selected_profile.window_name: + icon = view.Icon() + if icon != None: + icon_path = g15icontools.get_icon_path(icon) + if icon_path != None: + # We need to copy the icon as it may be temporary + copy_path = os.path.join(icons_dir, os.path.basename(icon_path)) + shutil.copy(icon_path, copy_path) + self.selected_profile.icon = copy_path + self._set_image(self.profile_icon, copy_path) + else: + import wnck + for window in wnck.screen_get_default().get_windows(): + if window.get_name() == self.selected_profile.window_name: + icon = window.get_icon() + if icon != None: + filename = os.path.join(icons_dir,"%d.png" % self.selected_profile.id) + icon.save(filename, "png") + self.selected_profile.icon = filename + self._set_image(self.profile_icon, filename) + + self._save_profile(self.selected_profile) + + def _driver_configuration_changed(self, *args): + self._set_driver_from_configuration() + self._load_plugins() + self._add_controls() + + def _set_driver_from_configuration(self): + selected_driver = self.conf_client.get_string(self._get_full_key("driver")) + i = 0 + sel = False + for ( driver_id, driver_name ) in self.driver_model: + if driver_id == selected_driver: + self.driver_combo.set_active(i) + sel = True + i += 1 + if len(self.driver_model) > 0 and not sel: + self.conf_client.set_string(self._get_full_key("driver"), self.driver_model[0][0]) + else: + driver_mod = g15drivermanager.get_driver_mod(selected_driver) + + # Show or hide the Keyboard / Plugins tab depending on if there is a driver that matches + if not driver_mod: + self.no_driver_available.set_label(_("There is no appropriate driver for the " + \ + "device %s.\nDo you have all the " + \ + "required packages installed?") \ + % self.selected_device.model_fullname) + self.tabs.set_visible(False) + self.no_driver_available.set_visible(True) + else: + self.driver_options.set_sensitive(driver_mod.has_preferences) + self.tabs.set_visible(True) + self.no_driver_available.set_visible(False) + + def _show_driver_options(self, widget): + selected_driver = self.conf_client.get_string(self._get_full_key("driver")) + driver_mod = g15drivermanager.get_driver_mod(selected_driver) + driver_mod.show_preferences(self.selected_device, + self.main_window, + self.conf_client) + + def _set_tab_status(self): + self.keyboard_tab.set_visible(self._controls_visible) + self.plugins_tab.set_visible(len(self.plugin_model) > 0) + self.profile_plugins_tab.set_visible(len(self.plugin_model) > 0) + + def _driver_options_changed(self): + self._add_controls() + self._load_plugins() + self._load_macro_state() + self._hide_warning() + + def _device_enabled_configuration_changed(self, client, connection_id, entry, args): + self._set_enabled_value_from_configuration() + + def _set_enabled_value_from_configuration(self): + enabled = g15devices.is_enabled(self.conf_client, self.selected_device) if self.selected_device != None else False + self.device_enabled.set_active(enabled) + self.device_enabled.set_sensitive(self.selected_device != None) + self.tabs.set_sensitive(enabled) + + def _device_enabled_changed(self, widget = None): + gobject.idle_add(self._set_device) + + def _theme_changed(self, widget = None): + if not self.adjusting: + sel = widget.get_active() + if sel >= 0: + key = self._get_full_key("plugins/%s/theme" % self._get_selected_plugin().id ) + path = self.theme_model.get_iter(sel) + self.conf_client.set_string(key, self.theme_model[path][0]) + + def _driver_changed(self, widget = None): + if len(self.driver_model) > 0: + sel = self.driver_combo.get_active() + if sel >= 0: + row = self.driver_model[sel] + current = self.conf_client.get_string(self._get_full_key("driver")) + if not current or row[0] != current: + self.conf_client.set_string(self._get_full_key("driver"), row[0]) + + def _set_device(self): + if self.selected_device: + g15devices.set_enabled(self.conf_client, self.selected_device, self.device_enabled.get_active()) + + def _memory_changed(self, widget): + self._load_profile(self.selected_profile) + + def _device_selection_changed(self, widget): + self._load_device() + if self.selected_device: + self.conf_client.set_string("/apps/gnome15/config_device_name", self.selected_device.uid) + self.device_settings.set_visible(True) + self.no_device_selected.set_visible(False) + else: + self.device_settings.set_visible(False) + self.no_device_selected.set_visible(True) + + def _load_device(self): + sel_items = self.device_view.get_selected_items() + sel_idx = sel_items[0][0] if len(sel_items) > 0 else -1 + self.selected_device = self.devices[sel_idx] if sel_idx > -1 and sel_idx < len(self.devices) else None + if self.selected_device: + self._load_drivers() + self._remove_notify_handles() + self.device_title.set_text(self.selected_device.model_fullname if self.selected_device else "") + self._set_enabled_value_from_configuration() + if self.selected_device != None: + self.conf_client.add_dir(self._get_device_conf_key(), gconf.CLIENT_PRELOAD_NONE) + self.notify_handles.append(self.conf_client.notify_add(self._get_full_key("cycle_seconds"), self._cycle_seconds_configuration_changed)); + self.notify_handles.append(self.conf_client.notify_add(self._get_full_key("cycle_screens"), self._cycle_screens_configuration_changed)); + self.notify_handles.append(self.conf_client.notify_add(self._get_full_key("plugins"), self._plugins_changed)) + self.notify_handles.append(self.conf_client.notify_add(self._get_full_key("active_profile"), self._active_profile_changed)) + self.notify_handles.append(self.conf_client.notify_add(self._get_full_key("locked"), self._active_profile_changed)) + self.notify_handles.append(self.conf_client.notify_add(self._get_full_key("enabled"), self._device_enabled_configuration_changed)) + self.notify_handles.append(self.conf_client.notify_add(self._get_full_key("driver"), self._driver_configuration_changed)) + self.selected_profile = g15profile.get_active_profile(self.selected_device) + self._set_cycle_seconds_value_from_configuration() + self._set_cycle_screens_value_from_configuration() + self.selected_profile = None + self._add_controls() + self.main_window.show_all() + self._load_profile_list() + self._load_plugins() + self._load_macro_state() + self._load_windows() + self._do_status_change() + self._set_tab_status() + + def _get_device_conf_key(self): + return "/apps/gnome15/%s" % self.selected_device.uid + + def _get_full_key(self, key): + return "%s/%s" % (self._get_device_conf_key(), key) + + def _select_profile(self, widget): + (model, path) = self.profiles_tree.get_selection().get_selected() + self.selected_profile = g15profile.get_profile(self.selected_device, model[path][2]) + self._load_profile(self.selected_profile) + + def _select_macro(self, widget): + self._set_available_actions() + + def _set_available_actions(self): + (_, path) = self.macro_list.get_selection().get_selected() + self.delete_macro_button.set_sensitive(path != None and not self.selected_profile.read_only) + self.macro_properties_button.set_sensitive(path != None) + + def _set_available_profile_actions(self): + sel = self.profile_plugins_mode.get_active() + path = self.profile_plugins_mode_model.get_iter(sel) + self.enabled_profile_plugins.set_sensitive(not self.selected_profile.read_only and self.profile_plugins_mode_model[path][0] == g15profile.SELECTED_PLUGINS) + self.window_name.set_sensitive(not self.selected_profile.read_only and self.selected_profile.activate_on_focus) + self.window_select.set_sensitive(not self.selected_profile.read_only and self.selected_profile.activate_on_focus) + + def _activate(self, widget): + (model, path) = self.profiles_tree.get_selection().get_selected() + self._make_active(g15profile.get_profile(self.selected_device, model[path][2])) + + def _make_active(self, profile): + profile.make_active() + self._load_profile_list() + + def _profile_plugins_mode_changed(self, widget = None): + if not self.adjusting: + sel = widget.get_active() + path = self.profile_plugins_mode_model.get_iter(sel) + self.selected_profile.plugins_mode = self.profile_plugins_mode_model[path][0] + self._set_available_profile_actions() + self._save_profile(self.selected_profile) + + def _clear_icon(self, widget): + if widget == self.clear_icon_button: + self.selected_profile.icon = "" + self._set_image(self.profile_icon, "") + else: + self.selected_profile.background = "" + self._set_image(self.background, "") + self._save_profile(self.selected_profile) + + def _add_macro_filters(self, dialog): + macros_filter = gtk.FileFilter() + macros_filter.set_name("Macro Archives") + macros_filter.add_pattern("*.mzip") + dialog.add_filter(macros_filter) + all_filter = gtk.FileFilter() + all_filter.set_name("All files") + all_filter.add_pattern("*") + dialog.add_filter(all_filter) + + def _import_profile(self, widget): + dialog = gtk.FileChooserDialog("Import..", + None, + gtk.FILE_CHOOSER_ACTION_OPEN, + (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, + gtk.STOCK_OPEN, gtk.RESPONSE_OK)) + dialog.set_default_response(gtk.RESPONSE_OK) + dialog.set_transient_for(self.main_window) + self._add_macro_filters(dialog) + response = dialog.run() + dialog.hide() + if response == gtk.RESPONSE_OK: + import_filename = dialog.get_filename() + profile_dir = g15profile.get_profile_dir(self.selected_device) + file = zipfile.ZipFile(import_filename, "r") + + profile_id = g15profile.generate_profile_id() + + try: + everything_ok = False + error = "" + + zip_contents = file.namelist(); + + # Check if there is a macro file + macro_filename = "" + for filename in zip_contents: + if filename.endswith(".macros"): + everything_ok = True + macro_filename = filename + break + else: + error = "Invalid archive (missing .macros file)" + + if everything_ok: + # Parse and handle the macro file + file_split = macro_filename.split(".", 1) + + dest_name = "%d.%s" % ( profile_id, file_split[1]) + + # Read the profile so we can adjust for the new environment + profiles = g15profile.get_profiles(self.selected_device) + macro_file = file.open(macro_filename, 'r') + try: + imported_profile = g15profile.G15Profile(self.selected_device) + imported_profile.load(None, macro_file) + imported_profile.set_id(profile_id) + finally: + macro_file.close() + + if self.selected_device.model_id not in imported_profile.models: + everything_ok = False + error = "The profile you imported was made for another device." + + if everything_ok: + # Find the best new name for the profile + new_name = imported_profile.name + idx = 1 + while True: + found = False + for p in profiles: + if new_name == p.name: + found = True + break + if found: + idx += 1 + new_name = "%s (%d)" % (imported_profile.name, idx) + else: + break + imported_profile.name = new_name + + # Set the icons + if imported_profile.icon: + imported_profile.icon = "%s/%d.%s" % ( profile_dir, profile_id, imported_profile.icon.split(".", 1)[1] ) + if imported_profile.background: + imported_profile.background = "%s/%d.%s" % ( profile_dir, profile_id, imported_profile.background.split(".", 1)[1] ) + + # Actually save + g15profile.create_profile(imported_profile) + + # Import the other files + for filename in zip_contents: + file_split = filename.split(".", 1) + + dest_name = "%d.%s" % ( profile_id, file_split[1]) + + if not dest_name.endswith(".macros"): + # Just extract all other files + dest_dir = os.path.join(profile_dir, os.path.dirname(dest_name)) + g15os.mkdir_p(dest_dir) + macro_file = file.open(filename, 'r') + try: + out_file = open(os.path.join(dest_dir, os.path.basename(dest_name)), 'w') + try: + out_file.write(macro_file.read()) + finally: + out_file.close() + finally: + macro_file.close() + + # If there was an error when importing display an error message + if not everything_ok: + import_profile_error_dialog = self.widget_tree.get_object("ImportProfileError") + import_profile_error_dialog.set_transient_for(self.main_window) + import_profile_error_dialog.format_secondary_text(error) + import_profile_error_dialog_close_button = self.widget_tree.get_object("ImportProfileErrorCloseButton") + import_profile_error_dialog_close_button.connect("clicked", lambda x: import_profile_error_dialog.hide()) + import_profile_error_dialog.run() + + finally: + file.close() + + dialog.destroy() + + def _lock_profile(self, widget): + if g15profile.is_locked(self.selected_device): + g15profile.set_locked(self.selected_device, False) + if not self.selected_profile.is_active(): + self.selected_profile.make_active() + g15profile.set_locked(self.selected_device, True) + + def _unlock_profile(self, widget): + g15profile.set_locked(self.selected_device, False) + + def _export(self, widget): + dialog = gtk.FileChooserDialog("Export..", + None, + gtk.FILE_CHOOSER_ACTION_SAVE, + (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, + gtk.STOCK_SAVE, gtk.RESPONSE_OK)) + dialog.set_default_response(gtk.RESPONSE_OK) + dialog.set_transient_for(self.main_window) + dialog.set_filename(os.path.expanduser("~/%s.mzip" % self.selected_profile.name)) + self._add_macro_filters(dialog) + response = dialog.run() + if response == gtk.RESPONSE_OK: + export_file = dialog.get_filename() + if not export_file.lower().endswith(".mzip"): + export_file += ".mzip" + + self.selected_profile.export(export_file) + dialog.destroy() + + def _browse_for_icon(self, widget): + dialog = gtk.FileChooserDialog("Open..", + None, + gtk.FILE_CHOOSER_ACTION_OPEN, + (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, + gtk.STOCK_OPEN, gtk.RESPONSE_OK)) + dialog.set_default_response(gtk.RESPONSE_OK) + dialog.set_transient_for(self.main_window) + if widget == self.icon_browse_button: + dialog.set_filename(self.selected_profile.icon) + else: + dialog.set_filename(self.selected_profile.background) + filter = gtk.FileFilter() + filter.set_name("All files") + filter.add_pattern("*") + + dialog.add_filter(filter) + + filter = gtk.FileFilter() + filter.set_name("Images") + filter.add_mime_type("image/png") + filter.add_mime_type("image/jpeg") + filter.add_mime_type("image/gif") + filter.add_pattern("*.png") + filter.add_pattern("*.jpg") + filter.add_pattern("*.jpeg") + filter.add_pattern("*.gif") + dialog.add_filter(filter) + + response = dialog.run() + + if response == gtk.RESPONSE_OK: + if widget == self.icon_browse_button: + self.selected_profile.icon = dialog.get_filename() + self._set_image(self.profile_icon, self.selected_profile.icon) + else: + self.selected_profile.background = dialog.get_filename() + self._set_image(self.background, self.selected_profile.background) + self._save_profile(self.selected_profile) + + dialog.destroy() + + def _remove_profile(self, widget): + dialog = self.widget_tree.get_object("ConfirmRemoveProfileDialog") + dialog.set_transient_for(self.main_window) + response = dialog.run() + dialog.hide() + if response == 1: + active_profile = g15profile.get_active_profile(self.selected_device) + if active_profile is not None and self.selected_profile.id == active_profile.id: + if g15profile.is_locked(self.selected_device): + g15profile.set_locked(self.selected_device, False) + self._make_active(g15profile.get_profile(self.selected_device, 0)) + self.selected_profile.delete() + self.profiles.remove(self.selected_profile) + if len(self.profiles) > 0: + self.selected_profile = self.profiles[0] + self._load_profile_list() + + def _profile_author_changed(self, widget): + if not self.adjusting: + self.selected_profile.author = widget.get_text() + self._save_profile(self.selected_profile) + + def _new_macro(self, widget): + memory = self._get_memory_number() + + # Find the next free G-Key + use = None + for row in self.driver.get_key_layout(): + if not use: + for key in row: + reserved = g15devices.are_keys_reserved(self.driver.get_model_name(), list(key)) + in_use = self.selected_profile.are_keys_in_use(g15driver.KEY_STATE_UP, memory, [ key ]) + if not in_use and not reserved: + use = key + break + + if use: + macro = self.selected_profile.create_macro(memory, [use], + _("Macro %s") % " ".join(g15driver.get_key_names([use])), + g15profile.MACRO_SIMPLE, + "", + g15driver.KEY_STATE_UP) + self._edit_macro(macro) + else: + logger.warning("No free keys") + + def _macro_properties(self, widget): + self._edit_macro(self._get_selected_macro()) + + def _get_selected_macro(self): + (model, path) = self.macro_list.get_selection().get_selected() + if model and path: + row = model[path] + return self.selected_profile.get_macro(row[4], + self._get_memory_number(), + g15profile.get_keys_from_key(row[2])) + + def _select_window(self, widget): + dialog = self.widget_tree.get_object("SelectWindowDialog") + dialog.set_transient_for(self.main_window) + dialog.run() + dialog.hide() + + def _edit_macro(self, macro): + macro_editor = g15macroeditor.G15MacroEditor(self.main_window) + macro_editor.set_driver(self.driver) + macro_editor.set_macro(macro) + macro_editor.run() + + def _remove_macro(self, widget): + memory = self._get_memory_number() + (model, path) = self.macro_list.get_selection().get_selected() + key_list_key = model[path][2] + activate_on = model[path][4] + dialog = self.widget_tree.get_object("ConfirmRemoveMacroDialog") + dialog.set_transient_for(self.main_window) + response = dialog.run() + dialog.hide() + if response == 1: + keys = g15profile.get_keys_from_key(key_list_key) + self.selected_profile.delete_macro(activate_on, memory, keys) + self._load_profile_list() + + def _save_profile(self, profile): + if not self.adjusting: + if self.profile_save_timer is not None: + self.profile_save_timer.cancel() + self.profile_save_timer = g15scheduler.schedule("SaveProfile", 2, self._do_save_profile, profile) + + def _do_save_profile(self, profile): + logger.info("Saving profile %s", profile.name) + profile.save() + + global_config = None + def _show_global_options(self, widget): + if self.global_config is None: + self.global_config = G15GlobalConfig(self.main_window, self.widget_tree, self.conf_client) + self.global_config.run() + + def _add_profile(self, widget): + dialog = self.widget_tree.get_object("NewProfileDialog") + dialog.set_transient_for(self.main_window) + response = dialog.run() + dialog.hide() + if response == 1: + new_profile_name = self.widget_tree.get_object("NewProfileName").get_text() + new_profile = g15profile.G15Profile(self.selected_device, g15profile.generate_profile_id()) + new_profile.name = new_profile_name + g15profile.create_profile(new_profile) + self.selected_profile = g15profile.get_profile(self.selected_device, new_profile.id) + self._load_profile_list() + + def _copy_profile(self, widget): + dupe_profile = g15profile.get_profile(self.selected_device, self.selected_profile.id) + dialog = self.widget_tree.get_object("CopyProfileDialog") + dialog.set_transient_for(self.main_window) + + # Choose a default name for the copy + default_name = self.selected_profile.name + last_cb = default_name.rfind(")") + last_ob = default_name.rfind("(") + i = 0 + if last_cb >=0 and last_ob >0: + i = int(default_name[last_ob + 1:last_cb]) + default_name = default_name[:last_ob].strip() + new_name = default_name + while True: + p = g15profile.get_profile_by_name(self.selected_device, new_name) + if p is None: + break + i += 1 + new_name = "%s (%i)" % ( default_name, i ) + + self.widget_tree.get_object("CopiedProfileName").set_text(new_name) + response = dialog.run() + dialog.hide() + if response == 1: + dupe_profile.set_id(g15profile.generate_profile_id()) + dupe_profile.name = self.widget_tree.get_object("CopiedProfileName").get_text() + dupe_profile.save() + self.selected_profile = dupe_profile + self._load_profile_list() + + def _get_memory_number(self): + if self.m1.get_active(): + return 1 + elif self.m2.get_active(): + return 2 + elif self.m3.get_active(): + return 3 + + def _load_devices(self): + self.device_model.clear() + self.selected_device = None + self.devices = g15devices.find_all_devices() + previous_sel_device_name = self._default_device_name + sel_device_name = None + idx = 0 + for device in self.devices: + if device.model_id == 'virtual': + icon_file = g15icontools.get_icon_path(["preferences-system-window", "preferences-system-windows", "gnome-window-manager", "window_fullscreen"]) + else: + icon_file = g15icontools.get_app_icon(self.conf_client, device.model_id) + pixb = gtk.gdk.pixbuf_new_from_file(icon_file) + self.device_model.append([pixb.scale_simple(96, 96, gtk.gdk.INTERP_BILINEAR), device.model_fullname, 96, gtk.WRAP_WORD, pango.ALIGN_CENTER]) + if previous_sel_device_name is not None and device.uid == previous_sel_device_name: + sel_device_name = device.uid + self.device_view.select_path((idx,)) + idx += 1 + if sel_device_name is None and len(self.devices) > 0: + sel_device_name = self.devices[0].uid + self.device_view.select_path((0,)) + + if idx != self._last_no_devices: + if idx == 1: + self.widget_tree.get_object("MainScrolledWindow").set_visible(False) + self.widget_tree.get_object("DeviceDetails").set_visible(False) + else: + self.widget_tree.get_object("MainScrolledWindow").set_visible(True) + self.widget_tree.get_object("DeviceDetails").set_visible(True) + # Hide the device settings if no device is selected + if sel_device_name is None: + self.device_settings.set_visible(False) + self.no_device_selected.set_visible(True) + + def _load_profile_list(self): + current_selection = self.selected_profile + self.profiles_model.clear() + if self.selected_device != None: + tree_selection = self.profiles_tree.get_selection() + active = g15profile.get_active_profile(self.selected_device) + active_id = "" + if active != None: + active_id = active.id + self.selected_profile = None + default_profile = g15profile.get_default_profile(self.selected_device) + self.profiles = g15profile.get_profiles(self.selected_device) + locked = g15profile.is_locked(self.selected_device) + for profile in self.profiles: + weight = 400 + selected = profile.id == active_id + if selected: + weight = 700 + lock_icon = gtk.gdk.pixbuf_new_from_file(os.path.join(g15globals.image_dir, "locked.png")) if locked and selected else None + self.profiles_model.append([profile.name, weight, profile.id, profile == default_profile, not profile.read_only, lock_icon ]) + if current_selection != None and profile.id == current_selection.id: + tree_selection.select_path(self.profiles_model.get_path(self.profiles_model.get_iter(len(self.profiles_model) - 1))) + self.selected_profile = profile + if self.selected_profile == None: + tree_selection.select_path(self.profiles_model.get_path(self.profiles_model.get_iter(0))) + self.selected_profile = self.profiles[0] + if self.selected_profile != None: + self._load_profile(self.selected_profile) + + def _load_parent_profiles(self): + self.parent_profile_model.clear() + self.parent_profile_model.append([-1, "" ]) + if self.selected_device != None: + for profile in self.profiles: + if profile.id != self.selected_profile.id: + self.parent_profile_model.append([profile.id, profile.name ]) + + def _profiles_changed(self, device_uid, macro_profile_id): + gobject.idle_add(self._load_profile_list) + + def _profile_name_edited(self, widget, row, value): + profile = self.profiles[int(row)] + if value != profile.name and not profile.read_only: + profile.name = value + self._save_profile(profile) + + def _macro_list_clicked(self, widget, event): + if event.type == gtk.gdk._2BUTTON_PRESS: + self._macro_properties(event) + + def _macro_name_edited(self, widget, row, value): + macro = self._get_sorted_list()[int(row)] + if value != macro.name: + macro.name = value + macro.save() + self._load_profile(self.selected_profile) + + def _comparator(self, o1, o2): + return o1.compare(o2) + + def _get_sorted_list(self): + sm = list(self.selected_profile.get_sorted_macros(None, self._get_memory_number())) + return sm + + def _load_profile(self, profile): + self.adjusting = True + try : + current_selection = self._get_selected_macro() + tree_selection = self.macro_list.get_selection() + name = profile.window_name + if name == None: + name = "" + self.macros_model.clear() + selected_macro = None + macros = self._get_sorted_list() + + # Build the macro model and set the initial selection + for macro in macros: + if macro.activate_on == g15driver.KEY_STATE_HELD: + on_name = _("Hold") + elif macro.activate_on == g15driver.KEY_STATE_DOWN: + on_name = _("Press") + else: + on_name = _("Release") + row = [", ".join(g15driver.get_key_names(macro.keys)), + macro.name, + macro.key_list_key, + not profile.read_only, + macro.activate_on, + on_name ] + self.macros_model.append(row) + if current_selection != None and macro.key_list_key == current_selection.key_list_key: + tree_selection.select_path(self.macros_model.get_path(self.macros_model.get_iter(len(self.macros_model) - 1))) + selected_macro = macro + if selected_macro == None and len(macros) > 0: + tree_selection.select_path(self.macros_model.get_path(self.macros_model.get_iter(0))) + + + # Various enabled / disabled and visible / invisible states are + # adjusted depending on the selected profile + self.new_macro_button.set_sensitive(not profile.read_only) + self.delete_macro_button.set_sensitive(not profile.read_only) + self.information_content.set_sensitive(not profile.read_only) + self.delays_content.set_sensitive(not profile.read_only) + self.activation_content.set_sensitive(not profile.read_only) + self.profile_plugins_mode.set_sensitive(not profile.read_only) + + if profile.get_default(): + self.activate_on_focus.set_visible(False) + self.launch_pattern_box.set_visible(False) + self.activate_on_launch.set_visible(False) + self.window_label.set_visible(False) + self.window_select.set_visible(False) + self.parent_profile_label.set_visible(False) + self.parent_profile_box.set_visible(False) + self.window_name.set_visible(False) + self.activate_by_default.set_visible(True) + self.context_remove_profile.set_sensitive(False) + else: + self._load_windows() +# self.launch_pattern_box.set_visible(True) +# self.activate_on_launch.set_visible(True) + self.launch_pattern_box.set_visible(False) + self.activate_on_launch.set_visible(False) + + self.window_name.set_visible(True) + self.parent_profile_label.set_visible(True) + self.parent_profile_box.set_visible(True) + self.window_select.set_visible(True) + self.activate_on_focus.set_visible(True) + self.window_label.set_visible(True) + self.activate_by_default.set_visible(False) + self.context_remove_profile.set_sensitive(not profile.read_only) + + # Set actions available based on locked state + locked = g15profile.is_locked(self.selected_device) + self.context_activate_profile.set_sensitive(not locked and not profile.is_active()) + self.context_unlock_profile.set_sensitive(profile.is_active() and locked) + self.context_lock_profile.set_sensitive(not profile.is_active() or ( profile.is_active() and not locked ) ) + self.activate_on_launch.set_active(profile.activate_on_launch) + self.launch_pattern.set_sensitive(self.activate_on_launch.get_active()) + + # Background button state + self.background_browse_button.set_visible(self.driver is not None and self.driver.get_bpp() > 1) + self.background_label.set_visible(self.driver is not None and self.driver.get_bpp() > 1) + self.clear_background_button.set_visible(self.driver is not None and self.driver.get_bpp() > 1) + + # Set the values of the widgets + self.launch_pattern.set_text("" if profile.launch_pattern is None else profile.launch_pattern) + self.profile_author.set_text(profile.author) + self.activate_by_default.set_active(profile.activate_on_focus) + if profile.window_name != None: + self.window_name.set_text(profile.window_name) + else: + self.window_name.set_text("") + self.send_delays.set_active(profile.send_delays) + self.fixed_delays.set_active(profile.fixed_delays) + self._set_delay_state() + self.press_delay_adjustment.set_value(float(profile.press_delay) / 1000.0) + self.release_delay_adjustment.set_value(float(profile.release_delay) / 1000.0) + self._set_image(self.profile_icon, profile.get_profile_icon_path(48)) + self._set_image(self.background, profile.get_resource_path(profile.background)) + self.activate_on_focus.set_active(profile.activate_on_focus) + self.window_combo.set_sensitive(self.activate_on_focus.get_active()) + + # Set up colors + if self.color_button != None: + rgb = profile.get_mkey_color(self._get_memory_number()) + if rgb == None: + self.enable_color_for_m_key.set_active(False) + self.color_button.set_sensitive(False) + self.color_button.set_color(g15convert.to_color((255, 255, 255))) + else: + self.color_button.set_sensitive(True and not profile.read_only) + self.color_button.set_color(g15convert.to_color(rgb)) + self.enable_color_for_m_key.set_active(True) + self.enable_color_for_m_key.set_sensitive(not profile.read_only) + + # Plugins + self._load_enabled_profile_plugins() + + # Parent profile + self._load_parent_profiles() + self.parent_profile_combo.set_active(0) + for i in range(0, len(self.parent_profile_model)): + if ( profile.base_profile == None and i == 0 ) or \ + ( i > 0 and profile.base_profile == self.parent_profile_model[i][0] ): + self.parent_profile_combo.set_active(i) + + # Inital state based on macro and profile selection + self._set_available_actions() + self._set_available_profile_actions() + finally: + self.adjusting = False + + def _load_enabled_profile_plugins(self): + for i in range(0, len(self.profile_plugins_mode_model)): + if self.selected_profile.plugins_mode == self.profile_plugins_mode_model[i][0]: + self.profile_plugins_mode.set_active(i) + self.enabled_profile_plugins_model.clear() + if self.selected_device: + for mod in sorted(g15pluginmanager.imported_plugins, key=lambda key: key.name): + key = self._get_full_key("plugins/%s/enabled" % mod.id ) + if self.driver and self.driver.get_model_name() in g15pluginmanager.get_supported_models(mod) and not g15pluginmanager.is_global_plugin(mod): + enabled = self.conf_client.get_bool(key) + if enabled: + self.enabled_profile_plugins_model.append([mod.id in self.selected_profile.selected_plugins, mod.name, mod.id]) + + def _set_image(self, widget, path): + if path == None or path == "" or not os.path.exists(path): + widget.set_from_stock(gtk.STOCK_MISSING_IMAGE, gtk.ICON_SIZE_DIALOG) + else: + widget.set_from_pixbuf(gtk.gdk.pixbuf_new_from_file_at_size(path, 48, 48)) + + def _load_windows(self): + self.window_model.clear() + window_name = self.window_name.get_text() + i = 0 + if self.bamf_matcher != None: + for window in self.bamf_matcher.RunningApplications(): + app = self.session_bus.get_object("org.ayatana.bamf", window) + view = dbus.Interface(app, 'org.ayatana.bamf.view') + vn = view.Name() + self.window_model.append([vn, window]) + if window_name != None and vn == window_name: + self.window_combo.set_active(i) + i += 1 + else: + apps = {} + for window in self.screen.get_windows(): + if not window.is_skip_pager(): + app = window.get_application() + if app and not app.get_name() in apps: + apps[app.get_name()] = app + for app in apps: + self.window_model.append([app, app]) + if window_name != None and app == window_name: + self.window_combo.set_active(i) + i += 1 + + def _add_controls(self): + + self._controls_visible = False + + # Remove previous notify handles + for nh in self.control_notify_handles: + self.conf_client.notify_remove(nh) + + driver_controls = None + if self.selected_device != None: + # Driver. We only need this to get the controls. Perhaps they should be moved out of the driver + # class and the values stored separately + try : + self.driver = g15drivermanager.get_driver(self.conf_client, self.selected_device) + self.driver.on_driver_options_change = self._driver_options_changed + + # Controls + driver_controls = self.driver.get_controls() + for control in driver_controls: + control.set_from_configuration(self.driver.device, self.conf_client) + + except Exception as e: + logger.error("Failed to load driver to query controls.", exc_info = e) + + if not driver_controls: + driver_controls = [] + + # Remove current components + controls = self.widget_tree.get_object("ControlsBox") + for c in controls.get_children(): + controls.remove(c) + for c in self.memory_bank_vbox.get_children(): + self.memory_bank_vbox.remove(c) + self.memory_bank_vbox.add(self.widget_tree.get_object("MemoryBanks")) + + # Slider and Color controls + table = gtk.Table(rows = max(1, len(driver_controls)), columns = 2) + table.set_row_spacings(4) + row = 0 + for control in driver_controls: + val = control.value + if isinstance(val, int): + if ( control.hint & g15driver.HINT_SWITCH ) == 0 and ( control.hint & g15driver.HINT_MKEYS ) == 0: + label = gtk.Label(control.name) + label.set_alignment(0.0, 0.5) + label.show() + table.attach(label, 0, 1, row, row + 1, xoptions = gtk.FILL, xpadding = 8, ypadding = 4); + + hscale = gtk.HScale() + hscale.set_value_pos(gtk.POS_RIGHT) + hscale.set_digits(0) + hscale.set_range(control.lower,control.upper) + hscale.set_value(control.value) + hscale.connect("value-changed", self._control_changed, control) + hscale.show() + + halign = gtk.Alignment(0, 0, 1.0, 1.00) + halign.add(hscale) + + table.attach(halign, 1, 2, row, row + 1, xoptions = gtk.EXPAND | gtk.FILL) + self.control_notify_handles.append(self.conf_client.notify_add(self._get_full_key(control.id), self._control_configuration_changed, [ control, hscale ])) + else: + label = gtk.Label(control.name) + label.set_alignment(0.0, 0.5) + label.show() + table.attach(label, 0, 1, row, row + 1, xoptions = gtk.FILL, xpadding = 8, ypadding = 4); + + picker = colorpicker.ColorPicker(redblue = control.hint & g15driver.HINT_RED_BLUE_LED != 0) + picker.set_color(control.value) + picker.connect("color-chosen", self._color_chosen, control) + table.attach(picker, 1, 2, row, row + 1) + + self.control_notify_handles.append(self.conf_client.notify_add(self._get_full_key(control.id), self._control_configuration_changed, [ control, picker])); + + row += 1 + if row > 0: + self._controls_visible = True + controls.add(table) + controls.show_all() + + # Switch controls + controls = self.widget_tree.get_object("SwitchesBox") + for c in controls.get_children(): + controls.remove(c) + table.set_row_spacings(4) + row = 0 + for control in driver_controls: + val = control.value + if isinstance(val, int): + if control.hint & g15driver.HINT_SWITCH != 0: + check_button = gtk.CheckButton(control.name) + check_button.set_active(control.value == 1) + check_button.set_alignment(0.0, 0.0) + check_button.show() + controls.pack_start(check_button, False, False, 4) + check_button.connect("toggled", self._control_changed, control) + self.notify_handles.append(self.conf_client.notify_add(self._get_full_key(control.id), self._control_configuration_changed, [ control, check_button ])); + row += 1 + if row > 0: + self._controls_visible = True + + controls.show_all() + self.widget_tree.get_object("SwitchesFrame").set_child_visible(row > 0) + + # Hide the cycle screens if the device has no screen + if self.driver != None and self.driver.get_bpp() == 0: + self.cycle_screens.hide() + self.cycle_screens_options.hide() + else: + self._controls_visible = True + self.cycle_screens.show() + self.cycle_screens_options.show() + + # If the keyboard has a colour dimmer, allow colours to be assigned to memory banks + control = self.driver.get_control_for_hint(g15driver.HINT_DIMMABLE) if self.driver != None else None + if control != None and not isinstance(control.value, int): + self._controls_visible = True + hbox = gtk.HBox() + self.enable_color_for_m_key = gtk.CheckButton(_("Set backlight colour")) + self.enable_color_for_m_key.connect("toggled", self._color_for_mkey_enabled) + hbox.pack_start(self.enable_color_for_m_key, True, False) + self.color_button = gtk.ColorButton() + self.color_button.set_sensitive(False) + self.color_button.connect("color-set", self._profile_color_changed) + hbox.pack_start(self.color_button, True, False) + self.memory_bank_vbox.add(hbox) + hbox.show_all() + else: + self.color_button = None + self.enable_color_for_m_key = None + + def _profile_color_changed(self, widget): + if not self.adjusting: + self.selected_profile.set_mkey_color(self._get_memory_number(), + g15convert.color_to_rgb(widget.get_color()) if self.enable_color_for_m_key.get_active() else None) + self._save_profile(self.selected_profile) + + def _color_for_mkey_enabled(self, widget): + self.color_button.set_sensitive(widget.get_active()) + self._profile_color_changed(self.color_button) + + def _show_profile_list_context(self, treeview, event): + if event.button == 3: + x = int(event.x) + y = int(event.y) + time = event.time + pthinfo = treeview.get_path_at_pos(x, y) + if pthinfo is not None: + path, col, cellx, celly = pthinfo + treeview.grab_focus() + treeview.set_cursor( path, col, 0) + self.profiles_context_menu.popup( None, None, None, event.button, time) + return True \ No newline at end of file diff --git a/src/gnome15/g15dbus.py b/src/gnome15/g15dbus.py new file mode 100644 index 0000000..042227e --- /dev/null +++ b/src/gnome15/g15dbus.py @@ -0,0 +1,988 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2010 Brett Smith +# +# 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 . + +import dbus.service +import g15globals +import g15theme +import util.g15scheduler as g15scheduler +import util.g15gconf as g15gconf +import util.g15cairo as g15cairo +import util.g15icontools as g15icontools +import g15driver +import g15devices +import gobject + + +from cStringIO import StringIO + +BUS_NAME="org.gnome15.Gnome15" +NAME="/org/gnome15/Service" +PAGE_NAME="/org/gnome15/Page" +CONTROL_ACQUISITION_NAME="/org/gnome15/Control" +SCREEN_NAME="/org/gnome15/Screen" +DEVICE_NAME="/org/gnome15/Device" +IF_NAME="org.gnome15.Service" +PAGE_IF_NAME="org.gnome15.Page" +CONTROL_ACQUISITION_IF_NAME="org.gnome15.Control" +SCREEN_IF_NAME="org.gnome15.Screen" +DEVICE_IF_NAME="org.gnome15.Device" + +# Logging +import logging +logger = logging.getLogger(__name__) + +class AbstractG15DBUSService(dbus.service.Object): + + def __init__(self, conn=None, object_path=None, bus_name=None): + dbus.service.Object.__init__(self, conn, object_path, bus_name) + self._reserved_keys = [] + + def action_performed(self, binding): + self.Action(binding.action) + + def handle_key(self, keys, state, post): + if not post: + p = [] + for k in keys: + if k in self._reserved_keys: + p.append(k) + if len(p) > 0: + if state == g15driver.KEY_STATE_UP: + gobject.idle_add(self.KeysReleased,p) + elif state == g15driver.KEY_STATE_DOWN: + gobject.idle_add(self.KeysPressed, p) + return True + + def _set_receive_actions(self, enabled): + if enabled and self in self._screen.key_handler.action_listeners: + raise Exception("Already receiving actions") + elif not enabled and not self in self._screen.key_handler.action_listeners: + raise Exception("Not receiving actions") + if enabled: + self._screen.key_handler.action_listeners.append(self) + else: + self._screen.key_handler.action_listeners.remove(self) + +class G15DBUSDeviceService(AbstractG15DBUSService): + + def __init__(self, dbus_service, device): + AbstractG15DBUSService.__init__(self, dbus_service._bus_name, "%s/%s" % ( DEVICE_NAME, device.uid ) ) + self._dbus_service = dbus_service + self._service = dbus_service._service + self._device = device + + @dbus.service.signal(DEVICE_IF_NAME, signature='s') + def ScreenAdded(self, screen_name): + pass + + @dbus.service.signal(DEVICE_IF_NAME, signature='s') + def ScreenRemoved(self, screen_name): + pass + + @dbus.service.method(DEVICE_IF_NAME, in_signature='b') + def SetReceiveActions(self, enabled): + self._set_receive_actions(enabled) + + @dbus.service.method(DEVICE_IF_NAME, in_signature='', out_signature='s') + def GetScreen(self): + for screen_path in self._dbus_service._dbus_screens: + screen = self._dbus_service._dbus_screens[screen_path] + if screen._screen.device.uid == self._device.uid: + return "%s/%s" % ( SCREEN_NAME, self._device.uid) + return "" + + @dbus.service.method(DEVICE_IF_NAME, in_signature='', out_signature='') + def Disable(self): + g15devices.set_enabled(self._service.conf_client, self._device, False) + + @dbus.service.method(DEVICE_IF_NAME, in_signature='', out_signature='') + def Enable(self): + g15devices.set_enabled(self._service.conf_client, self._device, True) + + @dbus.service.method(DEVICE_IF_NAME, in_signature='', out_signature='s') + def GetModelFullName(self): + return self._device.model_fullname + + @dbus.service.method(DEVICE_IF_NAME, in_signature='', out_signature='s') + def GetModelId(self): + return self._device.model_id + + @dbus.service.method(DEVICE_IF_NAME, in_signature='', out_signature='s') + def GetUID(self): + return self._device.uid + + @dbus.service.method(DEVICE_IF_NAME, in_signature='', out_signature='s') + def GetUsbID(self): + return "%s:%s" % ( hex(self._device.controls_usb_id[0]), hex(self._device.controls_usb_id[1]) ) + + @dbus.service.method(DEVICE_IF_NAME, in_signature='', out_signature='u') + def GetBPP(self): + return self._device.bpp + + @dbus.service.method(DEVICE_IF_NAME, in_signature='', out_signature='uu') + def GetSize(self): + return self._device.size + +class G15DBUSClient(): + + def __init__(self, bus_name): + self.bus_name = bus_name + self.pages = [] + self.acquisitions = [] + + def cleanup(self): + for p in list(self.pages): + p.delete() + for a in list(self.acquisitions): + a.release() + +class G15DBUSScreenService(AbstractG15DBUSService): + + def __init__(self, dbus_service, screen): + self._bus_name = "%s/%s" % ( SCREEN_NAME, screen.device.uid ) + AbstractG15DBUSService.__init__(self, dbus_service._bus_name, self._bus_name ) + self._dbus_service = dbus_service + self._service = dbus_service._service + self._screen = screen + self._screen.add_screen_change_listener(self) + self._screen.key_handler.key_handlers.append(self) + self._notify_handles = [] + self._dbus_pages = {} + self._clients = {} + + self._notify_handles.append(self._screen.conf_client.notify_add("/apps/gnome15/%s/cycle_screens" % self._screen.device.uid, self._cycle_screens_option_changed)) + + ''' + screen change listener and action listener + ''' + + def memory_bank_changed(self, new_memory_bank): + if g15scheduler.run_on_gobject(self.memory_bank_changed, new_memory_bank): + return + logger.debug("Sending memory bank changed signel (%d)", new_memory_bank) + self.MemoryBankChanged(new_memory_bank) + + def attention_cleared(self): + if g15scheduler.run_on_gobject(self.attention_cleared): + return + logger.debug("Sending attention cleared signal") + self.AttentionCleared() + logger.debug("Sent attention cleared signal") + + def attention_requested(self, message): + if g15scheduler.run_on_gobject(self.attention_requested, message): + return + logger.debug("Sending attention requested signal") + self.AttentionRequested(message if message != None else "") + logger.debug("Sent attention requested signal") + + def driver_connected(self, driver): + if g15scheduler.run_on_gobject(self.driver_connected, driver): + return + logger.debug("Sending driver connected signal") + self.Connected(driver.get_name()) + logger.debug("Sent driver connected signal") + + def driver_connection_failed(self, driver, exception): + if g15scheduler.run_on_gobject(self.driver_connection_failed, driver, exception): + return + logger.debug("Sending driver connection failed signal") + self.ConnectionFailed(driver.get_name(), str(exception)) + logger.debug("Sent driver connection failed signal") + + def driver_disconnected(self, driver): + if g15scheduler.run_on_gobject(self.driver_disconnected, driver): + return + logger.debug("Sending driver disconnected signal") + self.Disconnected(driver.get_name()) + logger.debug("Sent driver disconnected signal") + + def page_changed(self, page): + if g15scheduler.run_on_gobject(self.page_changed, page): + return + logger.debug("Sending page changed signal for %s", page.id) + if page.id in self._dbus_pages: + dbus_page = self._dbus_pages[page.id] + self.PageChanged(dbus_page._bus_name) + logger.debug("Sent page changed signal for %s", page.id) + else: + logger.warning("Got page_changed event when no such page (%s) exists", page.id) + + def new_page(self, page): + if g15scheduler.run_on_gobject(self.new_page, page): + return + logger.debug("Sending new page signal for %s", page.id) + if page.id in self._dbus_pages: + raise Exception("Page %s already in DBUS service.", page.id) + dbus_page = G15DBUSPageService(self, page, self._dbus_service._page_sequence_number) + self._dbus_pages[page.id] = dbus_page + self.PageCreated(dbus_page._bus_name, page.title) + self._dbus_service._page_sequence_number += 1 + logger.debug("Sent new page signal for %s" % page.id) + + def title_changed(self, page, title): + if g15scheduler.run_on_gobject(self.title_changed, page, title): + return + logger.debug("Sending title changed signal for %s", page.id) + dbus_page = self._dbus_pages[page.id] + self.PageTitleChanged(dbus_page._bus_name, title) + logger.debug("Sent title changed signal for %s", page.id) + + def deleting_page(self, page): + if g15scheduler.run_on_gobject(self.deleting_page, page): + return + logger.debug("Sending page deleting signal for %s", page.id) + + for client_bus_name in self._clients: + client = self._clients[client_bus_name] + if page in client.pages: + client.pages.remove(page) + + if page.id in self._dbus_pages: + dbus_page = self._dbus_pages[page.id] + if dbus_page in page.key_handlers: + page.key_handlers.remove(dbus_page) + self.PageDeleting(dbus_page._bus_name, ) + else: + logger.warning("DBUS Page %s is deleting, but it never existed. Huh? %s", + page.id, + str(self._dbus_pages)) + logger.debug("Sent page deleting signal for %s", page.id) + + def deleted_page(self, page): + if g15scheduler.run_on_gobject(self.deleted_page, page): + return + logger.debug("Sending page deleted signal for %s", page.id) + if page.id in self._dbus_pages: + dbus_page = self._dbus_pages[page.id] + self.PageDeleted(dbus_page._bus_name) + dbus_page.remove_from_connection() + del self._dbus_pages[page.id] + else: + logger.warning("DBUS Page %s was deleted, but it never existed. Huh? %s", + page.id, + str(self._dbus_pages)) + logger.debug("Sent page deleted signal for %s", page.id) + + """ + DBUS Functions + """ + + @dbus.service.method(SCREEN_IF_NAME, in_signature='sds', out_signature='s', sender_keyword = "sender") + def AcquireControl(self, control_id, release_after, value, sender = None): + control = self._screen.driver.get_control(control_id) + if control is None: + raise Exception("No control with ID of %s" % control_id) + if value == "": + initial_value = None + elif isinstance(control.value, int): + if control.hint & g15driver.HINT_SWITCH != 0: + initial_value = 1 if value == "true" else 0 + else: + initial_value = int(value) + else: + sp = value.split(",") + initial_value = (int(sp[0]), int(sp[1]), int(sp[2])) + control_acquisition = self._screen.driver.acquire_control(control, None if release_after == 0 else release_after, initial_value) + dbus_control_acquisition = G15DBUSControlAcquisition(self, control_acquisition, self._dbus_service._acquire_sequence_number) + self._get_client(sender).acquisitions.append(control_acquisition) + control_acquisition.on_release = dbus_control_acquisition._notify_release + self._dbus_service._acquire_sequence_number += 1 + return dbus_control_acquisition._bus_name + + @dbus.service.method(SCREEN_IF_NAME, in_signature='', out_signature='s') + def GetMessage(self): + return self._screen.attention_message + + @dbus.service.method(SCREEN_IF_NAME, in_signature='', out_signature='') + def ClearAttention(self): + return self._screen.clear_attention() + + @dbus.service.method(SCREEN_IF_NAME, in_signature='s', out_signature='') + def RequestAttention(self, message): + self._screen.request_attention(message) + + @dbus.service.method(SCREEN_IF_NAME, in_signature='ssn', out_signature='s', sender_keyword = 'sender') + def CreatePage(self, page_id, title, priority, sender = None): + page = g15theme.G15Page(page_id, self._screen, priority = priority) + self._screen.add_page(page) + page.set_title(title) + self._get_client(sender).pages.append(page) + return self.GetPageForID(page_id) + + @dbus.service.method(SCREEN_IF_NAME, in_signature='', out_signature='b') + def IsCyclingEnabled(self): + return g15gconf.get_bool_or_default(self._service.conf_client, "/apps/gnome15/%s/cycle_screens" % self._screen.device.uid, True); + + @dbus.service.method(SCREEN_IF_NAME, in_signature='b', out_signature='') + def SetCyclingEnabled(self, enabled): + self._service.conf_client.set_bool("/apps/gnome15/%s/cycle_screens" % self._screen.device.uid, enabled); + + @dbus.service.method(SCREEN_IF_NAME, in_signature='', out_signature='b') + def IsReceiveActions(self): + return self in self._screen.key_handler.action_listeners + + @dbus.service.method(SCREEN_IF_NAME, in_signature='s') + def ReserveKey(self, key_name): + if key_name in self._reserved_keys: + raise Exception("Already reserved") + self._reserved_keys.add(key_name) + + @dbus.service.method(SCREEN_IF_NAME, in_signature='s') + def UnreserveKey(self, key_name): + if not key_name in self._reserved_keys: + raise Exception("Not reserved") + self._reserved_keys.remove(key_name) + + @dbus.service.method(SCREEN_IF_NAME, in_signature='', out_signature='ssss') + def GetDeviceInformation(self): + device = self._screen.device + return ( device.uid, device.model_id, "%s:%s" % ( hex(device.controls_usb_id[0]),hex(device.controls_usb_id[1]) ), device.model_fullname ) + + @dbus.service.method(SCREEN_IF_NAME, in_signature='', out_signature='ssnnn') + def GetDriverInformation(self): + driver = self._screen.driver + return ( driver.get_name(), driver.get_model_name(), driver.get_size()[0], driver.get_size()[1], driver.get_bpp() ) if driver != None else None + + @dbus.service.method(SCREEN_IF_NAME, in_signature='', out_signature='s') + def GetDeviceUID(self): + return self._screen.device.uid + + @dbus.service.method(SCREEN_IF_NAME, in_signature='', out_signature='as') + def GetControlIds(self): + c = [] + for control in self._screen.driver.get_controls(): + c.append(control.id) + return c + + @dbus.service.method(SCREEN_IF_NAME, in_signature='', out_signature='b') + def IsConnected(self): + return self._screen.driver.is_connected() if self._screen.driver != None else False + + @dbus.service.method(SCREEN_IF_NAME, in_signature='n') + def CycleKeyboard(self, value): + for c in self._get_dimmable_controls(): + if isinstance(c.value, int): + self._screen.cycle_level(value, c) + else: + self._screen.cycle_color(value, c) + + @dbus.service.method(SCREEN_IF_NAME, in_signature='s', out_signature='s') + def GetPageForID(self, page_id): + return self._dbus_pages[page_id]._bus_name + + @dbus.service.method(SCREEN_IF_NAME, out_signature='s') + def GetVisiblePage(self): + return self.GetPageForID(self._screen.get_visible_page().id) + + @dbus.service.method(SCREEN_IF_NAME, out_signature='as') + def GetPages(self): + l = [] + for page in self._dbus_pages.values(): + l.append(page._bus_name) + return l + + @dbus.service.method(SCREEN_IF_NAME, in_signature='n', out_signature='as') + def GetPagesBelowPriority(self, priority): + logger.warning("The GetPagesBelowPriority is deprecated. Use GetPages instead.") + l = [] + for page in self._dbus_pages.values(): + if page._page.priority >= priority: + l.append(page._bus_name) + return l + + @dbus.service.method(SCREEN_IF_NAME, in_signature='', out_signature='s') + def GetLastError(self): + err = self._screen.get_last_error() + if err is None: + return "" + return str(err) + + @dbus.service.method(SCREEN_IF_NAME, in_signature='', out_signature='') + def ClearPopup(self): + return self._screen.clear_popup() + + @dbus.service.method(SCREEN_IF_NAME, in_signature='n', out_signature='') + def Cycle(self, cycle): + return self._screen.cycle(cycle) + + @dbus.service.method(SCREEN_IF_NAME, in_signature='', out_signature='b') + def IsAttentionRequested(self): + return self._screen.attention + + @dbus.service.method(SCREEN_IF_NAME, in_signature='b') + def SetReceiveActions(self, enabled): + self._set_receive_actions(enabled) + + """ + DBUS Signals + """ + + @dbus.service.signal(SCREEN_IF_NAME, signature='s') + def Disconnected(self, driver_name): + pass + + @dbus.service.signal(SCREEN_IF_NAME, signature='s') + def Connected(self, driver_name): + pass + + @dbus.service.signal(SCREEN_IF_NAME, signature='ss') + def ConnectionFailed(self, driver_name, exception_text): + pass + + @dbus.service.signal(SCREEN_IF_NAME, signature='u') + def MemoryBankChanged(self, new_memory_bank): + pass + + @dbus.service.signal(SCREEN_IF_NAME, signature='s') + def PageChanged(self, page_path): + pass + + @dbus.service.signal(SCREEN_IF_NAME, signature='ss') + def PageCreated(self, page_path, title): + pass + + @dbus.service.signal(SCREEN_IF_NAME, signature='ss') + def PageTitleChanged(self, page_path, new_title): + pass + + @dbus.service.signal(SCREEN_IF_NAME, signature='s') + def PageDeleted(self, page_path): + pass + + @dbus.service.signal(SCREEN_IF_NAME, signature='s') + def PageDeleting(self, page_path): + pass + + @dbus.service.signal(SCREEN_IF_NAME, signature='s') + def AttentionRequested(self, message): + pass + + @dbus.service.signal(SCREEN_IF_NAME, signature='') + def AttentionCleared(self): + pass + + @dbus.service.signal(SCREEN_IF_NAME, signature='as') + def KeysPressed(self, keys): + pass + + @dbus.service.signal(SCREEN_IF_NAME, signature='as') + def KeysReleased(self, keys): + pass + + @dbus.service.signal(SCREEN_IF_NAME, signature='s') + def Action(self, binding): + pass + + @dbus.service.signal(SCREEN_IF_NAME, signature='b') + def CyclingChanged(self, cycle): + pass + + """ + Private + """ + def _removing(self): + for h in self._notify_handles: + self._service.conf_client.notify_remove(h) + + def _cycle_screens_option_changed(self, client, connection_id, entry, args): + self.CyclingChanged(entry.value.get_bool()) + + def _get_dimmable_controls(self): + controls = [] + for c in self._screen.driver.get_controls(): + if c.hint & g15driver.HINT_DIMMABLE != 0: + controls.append(c) + return controls + + def _get_dimmable_control_values(self): + values = [] + for c in self._get_dimmable_controls(): + values.append(c.value) + return values + + def _get_screen_path(self): + return "%s/%s" % ( SCREEN_NAME, self._screen.device.uid ) + + def _get_client(self, sender): + if sender in self._clients: + return self._clients[sender] + else: + c = G15DBUSClient(sender) + self._clients[sender] = c + return c + +class G15DBUSControlAcquisition(AbstractG15DBUSService): + + def __init__(self, screen_service, acquisition, sequence_number): + self._bus_name = "%s%s" % ( CONTROL_ACQUISITION_NAME , str( sequence_number ) ) + AbstractG15DBUSService.__init__(self, screen_service._dbus_service._bus_name, self._bus_name ) + self._screen_service = screen_service + self._sequence_number = sequence_number + self._acquisition = acquisition + + @dbus.service.method(CONTROL_ACQUISITION_IF_NAME, out_signature='s') + def GetValue(self): + control = self._acquisition.control + value = self._acquisition.val + if isinstance(value, int): + if control.hint & g15driver.HINT_SWITCH != 0: + return "true" if value else "false" + else: + return str(value) + else: + return "%d,%d,%d" % value + + @dbus.service.method(CONTROL_ACQUISITION_IF_NAME, out_signature='t') + def GetHint(self): + return self._acquisition.control.hint + + @dbus.service.method(CONTROL_ACQUISITION_IF_NAME, in_signature='sd', out_signature='') + def SetValue(self, value, reset_after): + control = self._acquisition.control + reset_after = None if reset_after == 0.0 else reset_after + if isinstance(control.value, int): + if control.hint & g15driver.HINT_SWITCH != 0: + self._acquisition.set_value(1 if value == "true" else 0, reset_after) + else: + self._acquisition.set_value(int(value), reset_after) + else: + sp = value.split(",") + self._acquisition.set_value((int(sp[0]), int(sp[1]), int(sp[2])), reset_after) + + @dbus.service.method(CONTROL_ACQUISITION_IF_NAME, in_signature='ddb', out_signature='') + def Fade(self, percentage, duration, release): + self._acquisition.fade(percentage, duration, release) + + @dbus.service.method(CONTROL_ACQUISITION_IF_NAME, in_signature='ddd', out_signature='') + def Blink(self, off_val, delay, duration): + self._acquisition.blink(off_val, delay, None if duration == 0 else duration) + + @dbus.service.method(CONTROL_ACQUISITION_IF_NAME) + def Reset(self): + self._acquisition.reset() + + @dbus.service.method(CONTROL_ACQUISITION_IF_NAME) + def CancelReset(self): + self._acquisition.cancel_reset() + + @dbus.service.method(CONTROL_ACQUISITION_IF_NAME) + def Release(self): + self._screen_service._screen.driver.release_control(self._acquisition) + + """ + Private + """ + def _notify_release(self): + logger.info("Release acquisition of control %s", self._acquisition.control.id) + for client_bus_name in self._screen_service._clients: + client = self._screen_service._clients[client_bus_name] + if self._acquisition in client.acquisitions: + client.acquisitions.remove(self._acquisition) + self.remove_from_connection() + +class G15DBUSPageService(AbstractG15DBUSService): + + def __init__(self, screen_service, page, sequence_number): + self._bus_name = "%s%s" % ( PAGE_NAME , str( sequence_number ) ) + AbstractG15DBUSService.__init__(self, screen_service._dbus_service._bus_name, self._bus_name ) + self._screen_service = screen_service + self._screen = self._screen_service._screen + self._sequence_number = sequence_number + self._page = page + self._timer = None + self._page.key_handlers.append(self) + + @dbus.service.method(PAGE_IF_NAME, in_signature='b') + def SetReceiveActions(self, enabled): + if enabled and self in self._screen_service._screen.action_listeners: + raise Exception("Already receiving actions") + elif not enabled and not self in self._screen_service._screen.action_listeners: + raise Exception("Not receiving actions") + if enabled: + self._screen_service._screen.action_listeners.append(self) + else: + self._screen_service._screen.action_listeners.remove(self) + + @dbus.service.method(PAGE_IF_NAME, in_signature='', out_signature='b') + def GetReceiveActions(self): + return self in self._screen_service._screen.action_listeners + + @dbus.service.method(PAGE_IF_NAME, out_signature='n') + def GetPriority(self): + return self._page.priority + + @dbus.service.method(PAGE_IF_NAME, out_signature='s') + def GetTitle(self): + return self._page.title + + @dbus.service.method(PAGE_IF_NAME, out_signature='s') + def GetId(self): + return self._page.id + + @dbus.service.method(PAGE_IF_NAME, out_signature='b') + def IsVisible(self): + return self._page.is_visible() + + @dbus.service.method(PAGE_IF_NAME, in_signature='', out_signature='b') + def IsReceiveActions(self): + return self in self._screen.key_handler.action_listeners + + @dbus.service.method(PAGE_IF_NAME, in_signature='') + def Delete(self): + self._screen_service._screen.del_page(self._page) + + @dbus.service.method(PAGE_IF_NAME, in_signature='') + def Raise(self): + self._screen_service._screen.raise_page(self._page) + + @dbus.service.method(PAGE_IF_NAME, in_signature='', out_signature='') + def CycleTo(self): + self._screen_service._screen.cycle_to(self._page) + + @dbus.service.method(PAGE_IF_NAME, in_signature='') + def NewSurface(self): + self._page.new_surface() + + @dbus.service.method(PAGE_IF_NAME, in_signature='') + def Save(self): + self._page.save() + + @dbus.service.method(PAGE_IF_NAME, in_signature='') + def Restore(self): + self._page.restore() + + @dbus.service.method(PAGE_IF_NAME, in_signature='') + def DrawSurface(self): + self._page.draw_surface() + + @dbus.service.method(PAGE_IF_NAME, in_signature='d') + def SetLineWidth(self, line_width): + self._page.set_line_width(line_width) + + @dbus.service.method(PAGE_IF_NAME, in_signature='dddd') + def Line(self, x1, y1, x2, y2): + self._page.line(x1, y1, x2, y2) + + @dbus.service.method(PAGE_IF_NAME, in_signature='ddddb') + def Rectangle(self, x, y, width, height, fill): + self._page.rectangle(x, y, width, height, fill) + + @dbus.service.method(PAGE_IF_NAME, in_signature='dddb') + def Circle(self, x, y, radius, fill): + self._page.arc(x, y, radius, 0, 360, fill) + + @dbus.service.method(PAGE_IF_NAME, in_signature='dddddb') + def Arc(self, x, y, radius, startAngle, endAngle, fill): + self._page.arc(x, y, radius, startAngle, endAngle, fill) + + @dbus.service.method(PAGE_IF_NAME, in_signature='nnnn') + def Foreground(self, r, g, b, a): + self._page.foreground(r, g, b, a) + + @dbus.service.method(PAGE_IF_NAME, in_signature='dsss') + def SetFont(self, font_size = 12.0, font_family = "Sans", font_style = "normal", font_weight = "normal"): + self._page.set_font(font_size, font_family, font_style, font_weight) + + @dbus.service.method(PAGE_IF_NAME, in_signature='sdddds') + def Text(self, text, x, y, width, height, contraints = "left"): + self._page.text(text, x, y, width, height, contraints) + + @dbus.service.method(PAGE_IF_NAME, in_signature='sdddd') + def Image(self, path, x, y, width, height): + if not "/" in path: + path = g15icontools.get_icon_path(path, width if width != 0 else 128) + + size = None if width == 0 or height == 0 else (width, height) + + img_surface = g15cairo.load_surface_from_file(path, size) + self._page.image(img_surface, x, y) + + @dbus.service.method(PAGE_IF_NAME, in_signature='aydd') + def ImageData(self, image_data, x, y): + file_str = StringIO(str(image_data)) + img_surface = g15cairo.load_surface_from_file(file_str, None) + file_str.close() + self._page.image(img_surface, x, y) + + @dbus.service.method(PAGE_IF_NAME, in_signature='') + def CancelTimer(self): + self._timer.cancel() + + @dbus.service.method(PAGE_IF_NAME, in_signature='') + def Redraw(self): + self._screen_service._screen.redraw(self._page) + + @dbus.service.method(PAGE_IF_NAME, in_signature='ss') + def LoadTheme(self, theme_dir, variant): + self._page.set_theme(g15theme.G15Theme(theme_dir, variant)) + + @dbus.service.method(PAGE_IF_NAME, in_signature='s') + def SetThemeSVG(self, svg_text): + self._page.set_theme(g15theme.G15Theme(None, None, svg_text = svg_text)) + + @dbus.service.method(PAGE_IF_NAME, in_signature='ss') + def SetThemeProperty(self, name, value): + self._page.theme_properties[name] = value + + @dbus.service.method(PAGE_IF_NAME, in_signature='a{ss}') + def SetThemeProperties(self, properties): + self._page.theme_properties = properties + + @dbus.service.method(PAGE_IF_NAME, in_signature='ndd') + def SetPriority(self, priority, revert_after, delete_after): + self._timer = self._screen_service._screen.set_priority(self._page, priority, revert_after, delete_after) + + @dbus.service.signal(PAGE_IF_NAME, signature='as') + def KeysPressed(self, keys): + pass + + @dbus.service.signal(PAGE_IF_NAME, signature='as') + def KeysReleased(self, keys): + pass + + @dbus.service.signal(PAGE_IF_NAME, signature='s') + def Action(self, binding): + pass + + @dbus.service.method(PAGE_IF_NAME, in_signature='s') + def ReserveKey(self, key_name): + if key_name in self._reserved_keys: + raise Exception("Already reserved") + self._reserved_keys.add(key_name) + + @dbus.service.method(PAGE_IF_NAME, in_signature='s') + def UnreserveKey(self, key_name): + if not key_name in self._reserved_keys: + raise Exception("Not reserved") + self._reserved_keys.remove(key_name) + + """ + Callbacks + """ + def action_performed(self, binding): + if self.IsVisible(): + AbstractG15DBUSService.action_performed(self, binding) + +class G15DBUSService(AbstractG15DBUSService): + + def __init__(self, service): + AbstractG15DBUSService.__init__(self) + self._service = service + logger.debug("Getting Session DBUS") + self._bus = dbus.SessionBus() + self._page_sequence_number = 1 + self._acquire_sequence_number = 1 + logger.debug("Exposing service") + self._bus_name = dbus.service.BusName(BUS_NAME, bus=self._bus, replace_existing=False, allow_replacement=False, do_not_queue=True) + dbus.service.Object.__init__(self, self._bus_name, NAME) + self._service.service_listeners.append(self) + logger.debug("DBUS service ready") + self._dbus_screens = {} + self._dbus_devices = [] + self._dbus_device_map = {} + for device in g15devices.find_all_devices(): + dbus_device = G15DBUSDeviceService(self, device) + self._dbus_devices.append(dbus_device) + self._dbus_device_map[device.uid] = dbus_device + g15devices.device_added_listeners.append(self._device_added) + g15devices.device_removed_listeners.append(self._device_removed) + + self._bus.add_signal_receiver(self._name_owner_changed, + dbus_interface='org.freedesktop.DBus', + signal_name='NameOwnerChanged') + + def _device_removed(self, device): + if device.uid in self._dbus_device_map: + dbus_device = self._dbus_device_map[device.uid] + self._dbus_devices.remove(dbus_device) + del self._dbus_device_map[device.uid] + logger.info("Removed DBUS device %s/%s", DEVICE_NAME, device.uid) + self.DeviceRemoved("%s/%s" % ( DEVICE_NAME, device.uid )) + self._silently_remove_from_connector(dbus_device) + else: + logger.warning("DBUS service did not know about a device for some reason (%s)", device.uid) + + def _device_added(self, device): + dbus_device = G15DBUSDeviceService(self, device) + self._dbus_devices.append(dbus_device) + self._dbus_device_map[device.uid] = dbus_device + logger.info("Added DBUS device %s/%s", DEVICE_NAME, device.uid) + self.DeviceAdded("%s/%s" % ( DEVICE_NAME, device.uid )) + + def stop(self): + g15devices.device_added_listeners.remove(self._device_added) + g15devices.device_removed_listeners.remove(self._device_removed) + for dbus_device in self._dbus_devices: + self._silently_remove_from_connector(dbus_device) + for screen in self._dbus_screens: + self._silently_remove_from_connector(self._dbus_screens[screen]) + self._silently_remove_from_connector(self) + + def _silently_remove_from_connector(self, obj): + try: + obj.remove_from_connection() + except Exception as e: + logger.debug("Error silently removing obj from connection.", exc_info = e) + pass + + ''' + service listener + ''' + def screen_added(self, screen): + if g15scheduler.run_on_gobject(self.screen_added, screen): + return + logger.debug("Screen added for %s", screen.device.model_id) + screen_service = G15DBUSScreenService(self, screen) + self._dbus_screens[screen.device.uid] = screen_service + self.ScreenAdded("%s/%s" % ( SCREEN_NAME, screen.device.uid )) + dbus_device = self._dbus_device_map[screen.device.uid] + dbus_device.ScreenAdded("%s/%s" % ( SCREEN_NAME, screen.device.uid )) + + def screen_removed(self, screen): + if g15scheduler.run_on_gobject(self.screen_removed, screen): + return + logger.debug("Screen removed for %s", screen.device.model_id) + self.ScreenRemoved("%s/%s" % ( SCREEN_NAME, screen.device.uid )) + if screen.device.uid in self._dbus_device_map: + dbus_device = self._dbus_device_map[screen.device.uid] + dbus_device.ScreenRemoved("%s/%s" % ( SCREEN_NAME, screen.device.uid )) + try: + screen_service = self._dbus_screens[screen.device.uid] + screen_service._removing() + screen_service.remove_from_connection() + except Exception as e: + logger.debug("Error removing screen object.", exc_info = e) + # May happen on shutdown + pass + del self._dbus_screens[screen.device.uid] + + def service_stopping(self): + if g15scheduler.run_on_gobject(self.service_stopping): + return + logger.debug("Sending stopping down signal") + self.Stopping() + + def service_stopped(self): + if g15scheduler.run_on_gobject(self.service_stopped): + return + logger.debug("Sending stopped down signal") + self.Stopped() + + def service_starting_up(self): + if g15scheduler.run_on_gobject(self.service_starting_up): + return + logger.debug("Sending starting up signal") + self.Starting() + + def service_started_up(self): + if g15scheduler.run_on_gobject(self.service_started_up): + return + logger.debug("Sending started up signal") + self.Started() + + ''' + DBUS Signals + ''' + + @dbus.service.signal(IF_NAME) + def Stopping(self): + pass + + @dbus.service.signal(IF_NAME) + def Stopped(self): + pass + + @dbus.service.signal(IF_NAME) + def Starting(self): + pass + + @dbus.service.signal(IF_NAME) + def Started(self): + pass + + @dbus.service.signal(IF_NAME, signature='s') + def ScreenAdded(self, screen_name): + pass + + @dbus.service.signal(IF_NAME, signature='s') + def ScreenRemoved(self, screen_name): + pass + + @dbus.service.signal(IF_NAME, signature='s') + def DeviceAdded(self, device_name): + pass + + @dbus.service.signal(IF_NAME, signature='s') + def DeviceRemoved(self, device_name): + pass + + ''' + DBUS methods + ''' + @dbus.service.method(IF_NAME, in_signature='', out_signature='ssss') + def GetServerInformation(self): + return ( g15globals.name, "Gnome15 Project", g15globals.version, "2.1" ) + + @dbus.service.method(IF_NAME, in_signature='', out_signature='') + def Stop(self): + g15scheduler.queue("serviceQueue", "dbusShutdown", 0, self._service.shutdown) + + @dbus.service.method(IF_NAME, in_signature='', out_signature='b') + def IsStarting(self): + return self._service.starting_up + + @dbus.service.method(IF_NAME, in_signature='', out_signature='b') + def IsStarted(self): + started = not self._service.starting_up and not self._service.shutting_down + return started + + @dbus.service.method(IF_NAME, in_signature='', out_signature='b') + def IsStopping(self): + return self._service.shutting_down + + @dbus.service.method(IF_NAME, out_signature='as') + def GetDevices(self): + l = [] + for device in self._dbus_devices: + l.append("%s/%s" % (DEVICE_NAME, device._device.uid ) ) + return l + + @dbus.service.method(IF_NAME, out_signature='as') + def GetScreens(self): + l = [] + for screen in self._dbus_screens: + l.append("%s/%s" % (SCREEN_NAME, screen ) ) + return l + + @dbus.service.method(IF_NAME, in_signature='ssas') + def Launch(self, profile_name, screen_id, args): + logger.info("Launch under profile %s, screen %s, args = %s", + profile_name, + screen_id, + str(args)) + + """ + Private + """ + def _name_owner_changed(self, name, old_owner, new_owner): + for screen in self._dbus_screens.values(): + if name in screen._clients and old_owner and not new_owner: + logger.info("Cleaning up DBUS client %s", name) + client = screen._clients[name] + client.cleanup() + del screen._clients[name] + diff --git a/src/gnome15/g15dconf.py b/src/gnome15/g15dconf.py new file mode 100644 index 0000000..ecc47ea --- /dev/null +++ b/src/gnome15/g15dconf.py @@ -0,0 +1,122 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2012 Brett Smith +# +# 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 . + + +""" +This class provides a way of retrieving and monitoring dconf without using +the GI bindings, which may no longer be mixed with static bindings (that +Gnome15 uses). + +This class is stop gap until a better solution can be found +""" + +import dbus +import os +import gobject + +# Logging +import logging +logger = logging.getLogger(__name__) + +PASSIVE_MATCH_STRING="type='method_call',interface='ca.desrt.dconf.Writer',member='Change'" +EAVESDROP_MATCH_STRING="eavesdrop='true',%s" % PASSIVE_MATCH_STRING + +class GSettingsCallback(): + + def __init__(self, handle, key, callback): + self.handle = handle + self.key = key + self.callback = callback + +class GSettings(): + + def __init__(self, schema_id): + self.schema_id = schema_id + self._handle = 1 + # DBUS session instance must be private or monitoring will not work properly + self._session_bus = dbus.SessionBus(private=True) + self._writer = dbus.Interface(self._session_bus.get_object("ca.desrt.dconf", "/ca/desrt/dconf/Writer/user"), "ca.desrt.dconf.Writer") + self._monitors = {} + + self._match_string = EAVESDROP_MATCH_STRING + try: + self._session_bus.add_match_string(self._match_string) + except Exception as e: + logger.debug('Could not add EAVESDROP match rule. Trying PASSIVE', exc_info = e) + self._match_string = PASSIVE_MATCH_STRING + self._session_bus.add_match_string(self._match_string) + self._session_bus.add_message_filter(self._msg_cb) + + def connect(self, key, callback): + l = key.split(":") + if l[0] != "changed": + raise Exception("Only currently supported changed events") + key = l[2] + handle = self._handle + self._handle += 1 + self._monitors[handle] = GSettingsCallback(handle, key, callback) + return handle + + def disconnect(self, handle): + if handle in self._monitors: + del self._monitors[handle] + + def get_string(self, key): + _, result = self._get_status_output("gsettings get %s %s" % (self.schema_id, key)) + if len(result) > 0: + result = result.replace("\n", "") + if result.startswith("'"): + return result[1:-1] + return result + + def _get_status_output(self, cmd): + pipe = os.popen('{ ' + cmd + '; } 2>/dev/null', 'r') + try: + text = pipe.read() + finally: + sts = pipe.close() + if sts is None: + sts = 0 + if text[-1:] == '\n': + text = text[:-1] + return sts, text + + def _changed(self, key): + s = "" + for b in key: + if b == 0: + break + else: + s += chr(b) + li = s.rfind("/") + if li > 0: + s_id = s[:li][1:].replace("/", ".") + k = s[li + 1:].replace("-", "_") + if s_id == self.schema_id: + for m in self._monitors: + mon = self._monitors[m] + if mon.key == k: + # Bit rubbish, but we need to give dconf time to update + gobject.timeout_add(1000, mon.callback) + + def _msg_cb(self, bus, msg): + # Only interested in method calls + if isinstance(msg, dbus.lowlevel.MethodCallMessage): + if msg.get_member() == "Change": + self._changed(*msg.get_args_list()) + + def __del__(self): + self._session_bus.remove_match_string(self._match_string) diff --git a/src/gnome15/g15debug.py b/src/gnome15/g15debug.py new file mode 100644 index 0000000..95fcfb2 --- /dev/null +++ b/src/gnome15/g15debug.py @@ -0,0 +1,34 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2012 Brett Smith +# +# 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 . + +import gc +import weakref +import objgraph + +#gc.set_debug(gc.DEBUG_LEAK) + + +import time +if __name__ == "__main__": + print "Creating snapshot1" + snapshot1 = take_snapshot() + print "Creating some objects" + l = [ "A", "B", "C", "D", "E" ] + print "Creating snapshot2" + snapshot2 = take_snapshot() + print "Comparing" + compare_snapshots(snapshot1, snapshot2) + \ No newline at end of file diff --git a/src/gnome15/g15desktop.py b/src/gnome15/g15desktop.py new file mode 100644 index 0000000..9d4e97f --- /dev/null +++ b/src/gnome15/g15desktop.py @@ -0,0 +1,1540 @@ +# coding: utf-8 + +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2011 Brett Smith +# +# 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 . + +""" +Helper classes for implementing desktop components that can monitor and control some functions +of the desktop service. It is used for Indicators, System tray icons and panel applets and +deals with all the hard work of connecting to DBus and monitoring events. +""" + +import gnome15.g15locale as g15locale +_ = g15locale.get_translation("gnome15").ugettext + +import sys +import pygtk +pygtk.require('2.0') +import gtk +import subprocess +import gconf +import gobject +import shutil +import gnome15.g15globals as g15globals +import gnome15.g15screen as g15screen +import gnome15.util.g15pythonlang as g15pythonlang +import gnome15.util.g15gconf as g15gconf +import gnome15.util.g15os as g15os +import gnome15.g15notify as g15notify +import gnome15.util.g15icontools as g15icontools +import dbus +import os.path +import operator +import xdg.DesktopEntry +import xdg.BaseDirectory + +# Logging +import logging +logger = logging.getLogger(__name__) + +from threading import RLock +from threading import Thread + +icon_theme = gtk.icon_theme_get_default() +if g15globals.dev: + icon_theme.prepend_search_path(g15globals.icons_dir) + +# Private +__browsers = { } + +""" +Some constants +""" +AUTHORS=["Brett Smith ", "Nuno Araujo", "Ciprian Ciubotariu", "Andrea Calabrò" ] +GPL=""" + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + 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. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 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 . + +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: + + Copyright (C) + 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 +. + + 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 +. +""" + +def autostart_path_for(application_name): + """ + Returns the autostart path of the application_name desktop file + """ + return os.path.join(xdg.BaseDirectory.xdg_config_home, + "autostart", + "%s.desktop" % application_name) + +def is_desktop_application_installed(application_name): + """ + Get if a desktop file is installed for a particular application + + Keyword arguments: + application_name -- name of application + """ + for directory in xdg.BaseDirectory.xdg_config_dirs: + desktop_file = os.path.join(directory, + "autostart", + "%s.desktop" % application_name) + if os.path.exists(desktop_file): + return True + return False + +def is_autostart_application(application_name): + """ + Get whether the application is set to autostart + """ + installed = is_desktop_application_installed(application_name) + path = autostart_path_for(application_name) + if os.path.exists(path): + desktop_entry = xdg.DesktopEntry.DesktopEntry(path) + autostart = len(desktop_entry.get('X-GNOME-Autostart-enabled')) == 0 or desktop_entry.get('X-GNOME-Autostart-enabled', type="boolean") + hidden = desktop_entry.getHidden() + return autostart and not hidden + else: + # There is no config file, so enabled if installed + return installed + +def set_autostart_application(application_name, enabled): + """ + Set whether an application is set to autostart + + Keyword arguments: + application_name -- application name + enabled -- enabled or not + """ + path = autostart_path_for(application_name) + if enabled and os.path.exists(path): + os.remove(path) + elif not enabled: + app_path = "/etc/xdg/autostart/%s.desktop" % application_name + if not os.path.exists(path): + shutil.copy(app_path, path) + desktop_entry = xdg.DesktopEntry.DesktopEntry(path) + desktop_entry.set("X-GNOME-Autostart-enabled", "false") + desktop_entry.set("Hidden", "false") + desktop_entry.write() + +def get_desktop(): + ''' + Utility function to get the name of the current desktop environment. The list + of detectable desktop environments is not complete, but hopefully this will + improve over time. Currently no attempt is made to determine the version of + the desktop in use. + + Will return :- + + gnome GNOME Desktop + gnome-shell GNOME Shell Desktop + kde KDE + [None] No known desktop + ''' + + evars = os.environ + + # GNOME Shell (need a better way) + if ( "DESKTOP_SESSION" in evars and evars["DESKTOP_SESSION"] == "gnome-shell" ) or \ + ( "GJS_DEBUG_OUTPUT" in evars ): + return "gnome-shell" + + # XDG_CURRENT_DESKTOP + dt = { "LXDE" : "lxde", "GNOME" : "gnome"} + if "XDG_CURRENT_DESKTOP" in evars: + val = evars["XDG_CURRENT_DESKTOP"] + if val in dt: + return dt[val] + + # Environment variables that suggest the use of GNOME + for i in [ "GNOME_DESKTOP_SESSION_ID", "GNOME_KEYRING_CONTROL" ]: + if i in evars: + return "gnome" + + # Environment variables that suggest the use of KDE + for i in [ "KDE_FULL_SESSION", "KDE_SESSION_VERSION", "KDE_SESSION_UID" ]: + if i in evars: + return "kde" + + # Environment variables that suggest the use of LXDE + for i in [ "_LXSESSION_PID" ]: + if i in evars: + return "lxde" + +def is_shell_extension_installed(extension): + """ + Get whether a GNOME Shell extension is installed. + + Keyword arguments: + extension -- extension name + """ + for prefix in xdg.BaseDirectory.xdg_data_dirs: + extension_path = os.path.join(prefix, "gnome-shell", "extensions", extension) + if os.path.exists(extension_path): + return True + return False + +def is_gnome_shell_extension_enabled(extension): + """ + Get whether a GNOME Shell extension is enabled. This uses the + gsettings command. Python GSettings bindings (GObject introspected ones) + are not used, as well already use PyGTK and the two don't mix + + Keyword arguments: + extension -- extension name + """ + status, text = g15os.get_command_output("gsettings get org.gnome.shell enabled-extensions") + if status == 0: + try: + return extension in eval(text) + except Exception as e: + logger.debug("Failed testing if extension is enabled.", exc_info = e) + + return False + +def set_gnome_shell_extension_enabled(extension, enabled): + """ + Enable or disable a GNOME Shell extension is enabled. This uses the + gsettings command. Python GSettings bindings (GObject introspected ones) + are not used, as well already use PyGTK and the two don't mix + + Keyword arguments: + extension -- extension name + enabled -- enabled + """ + status, text = g15os.get_command_output("gsettings get org.gnome.shell enabled-extensions") + if status == 0: + try: + extensions = eval(text) + except Exception as e: + logger.debug('No gnome-shell extensions enabled.', exc_info = e) + # No extensions available, so init an empty array + extensions = [] + pass + contains = extension in extensions + if contains and not enabled: + extensions.remove(extension) + elif not contains and enabled: + extensions.append(extension) + s = "" + for c in extensions: + if len(s) >0: + s += "," + s += "'%s'" % c + try: + status, text = g15os.get_command_output("gsettings set org.gnome.shell enabled-extensions \"[%s]\"" % s) + except Exception as e: + logger.debug("Failed to set extension enabled.", exc_info = e) + +def browse(url): + """ + Open the configured browser + + Keyword arguments: + url -- URL + """ + b = g15gconf.get_string_or_default(gconf.client_get_default(), \ + "/apps/gnome15/browser", "default") + if not b in __browsers and not b == "default": + logger.warning("Could not find browser %s, falling back to default", b) + b = "default" + if not b in __browsers: + raise Exception("Could not find browser %s" % b) + __browsers[b].browse(url) + +def add_browser(browser): + """ + Register a new browser. The object must extend G15Browser + + Keyword arguments: + browser -- browser object. + """ + if browser.browser_id in __browsers: + raise Exception("Browser already registered") + if not isinstance(browser, G15Browser): + raise Exception("Not a G15Browser instance") + __browsers[browser.browser_id] = browser + +class G15Browser(): + def __init__(self, browser_id, name): + self.name = name + self.browser_id = browser_id + + def browse(self, url): + raise Exception("Not implemented") + +class G15DefaultBrowser(G15Browser): + def __init__(self): + G15Browser.__init__(self, "default", _("Default system browser")) + + def browse(self, url): + logger.info("xdg-open '%s'", url) + subprocess.Popen(['xdg-open', url]) + +add_browser(G15DefaultBrowser()) + +class G15AbstractService(Thread): + + def __init__(self): + Thread.__init__(self) + # Start this thread, which runs the gobject loop. This is + # run first, and in a thread, as starting the Gnome15 will send + # DBUS events (which are sent on the loop). + self.loop = gobject.MainLoop() + self.start() + + def start_loop(self): + logger.info("Starting GLib loop") + g15pythonlang.set_gobject_thread() + try: + self.loop.run() + except Exception as e: + logger.debug('Error while running GLib loop', exc_info = e) + logger.info("Exited GLib loop") + + def start_service(self): + raise Exception("Not implemented") + + def run(self): + # Now start the service, which will connect to all devices and + # start their plugins + self.start_service() + + +class G15Screen(): + """ + Client side representation of a remote screen. Holds general details such + as model name, UID and the pages that screen is currently showing. + """ + + def __init__(self, path, device_model_fullname, device_uid): + self.path = path + self.device_model_fullname = device_model_fullname + self.device_uid = device_uid + self.items = {} + self.message = None + +class G15DesktopComponent(): + """ + Helper class for implementing desktop components that can monitor and control some functions + of the desktop service. It is used for Indicators, System tray icons and panel applets and + deals with all the hard work of connecting to DBus and monitoring events. + """ + + def __init__(self): + self.screens = {} + self.service = None + self.start_service_item = None + self.attention_item = None + self.pages = [] + self.lock = RLock() + self.attention_messages = {} + self.connected = False + + # Connect to DBus and GConf + self.conf_client = gconf.client_get_default() + self.session_bus = dbus.SessionBus() + + # Enable monitoring of Gnome15 GConf settings + self.conf_client.add_dir("/apps/gnome15", gconf.CLIENT_PRELOAD_NONE) + + # Initialise desktop component + self.initialise_desktop_component() + self.icons_changed() + + def start_service(self): + """ + Start the desktop component. An attempt will be made to connect to Gnome15 over + DBus. If this fails, the component should stay active until the service becomes + available. + """ + + # Try and connect to the service now + try : + self._connect() + except dbus.exceptions.DBusException as e: + logger.debug("Error while starting the service.", exc_info = e) + self._disconnect() + + # Start watching various events + self.conf_client.notify_add("/apps/gnome15/indicate_only_on_error", self._indicator_options_changed) + gtk_icon_theme = gtk.icon_theme_get_default() + gtk_icon_theme.connect("changed", self._theme_changed) + + # Watch for Gnome15 starting and stopping + self.session_bus.add_signal_receiver(self._name_owner_changed, + dbus_interface='org.freedesktop.DBus', + signal_name='NameOwnerChanged') + + """ + Pulic functions + """ + def is_attention(self): + return len(self.attention_messages) > 0 + + def get_icon_path(self, icon_name): + """ + Helper function to get an icon path or it's name, given the name. + """ + if g15globals.dev: + # Because the icons aren't installed in this mode, they must be provided + # using the full filename. Unfortunately this means scaling may be a bit + # blurry in the indicator applet + path = g15icontools.get_icon_path(icon_name, 128) + logger.debug("Dev mode icon %s is at %s", icon_name, path) + return path + else: + if not isinstance(icon_name, list): + icon_name = [ icon_name ] + for i in icon_name: + p = g15icontools.get_icon_path(i, -1) + if p is not None: + return i + + def show_configuration(self, arg = None): + """ + Show the configuration user interface + """ + g15os.run_script("g15-config") + + def stop_desktop_service(self, arg = None): + """ + Stop the desktop service + """ + self.session_bus.get_object('org.gnome15.Gnome15', '/org/gnome15/Service').Stop() + + def start_desktop_service(self, arg = None): + """ + Start the desktop service + """ + g15os.run_script("g15-desktop-service", ["-f"]) + + def show_page(self, path): + """ + Show a page, given its path + """ + self.session_bus.get_object('org.gnome15.Gnome15', path).CycleTo() + + def check_attention(self): + """ + Check the current state of attention, either clearing it or setting it and displaying + a new message + """ + if len(self.attention_messages) == 0: + self.clear_attention() + else: + for i in self.attention_messages: + message = self.attention_messages[i] + self.attention(message) + break + + """ + Functions that must be implemented + """ + + def initialise_desktop_component(self): + """ + This function is called during construction and should create initial desktop component + """ + raise Exception("Not implemented") + + def rebuild_desktop_component(self): + """ + This function is called every time the list of screens or pages changes + in someway. The desktop component should be rebuilt to reflect the + new state + """ + raise Exception("Not implemented") + + def clear_attention(self): + """ + Clear any "Attention" state indicators + """ + raise Exception("Not implemented") + + def attention(self, message = None): + """ + Display an "Attention" state indicator with a message + + Keyword Arguments: + message -- message to display + """ + raise Exception("Not implemented") + + def icons_changed(self): + """ + Invoked once a start up, and then whenever the desktop icon theme changes. Implementations + should do whatever required to change any themed icons they are displayed + """ + raise Exception("Not implemented") + + def options_changed(self): + """ + Invoked when any global desktop component options change. + """ + raise Exception("Not implemented") + + ''' + DBUS Event Callbacks + ''' + def _name_owner_changed(self, name, old_owner, new_owner): + if name == "org.gnome15.Gnome15": + if old_owner == "": + if self.service == None: + self._connect() + else: + if self.service != None: + self.connected = False + self._disconnect() + + def _page_created(self, page_path, page_title, path = None): + screen_path = path + logger.debug("Page created (%s) %s = %s", screen_path, page_path, page_title) + page = self.session_bus.get_object('org.gnome15.Gnome15', page_path ) + self.lock.acquire() + try : + if page.GetPriority() >= g15screen.PRI_LOW: + self._add_page(screen_path, page_path, page) + finally : + self.lock.release() + + def _page_title_changed(self, page_path, title, path = None): + screen_path = path + self.lock.acquire() + try : + self.screens[screen_path].items[page_path] = title + self.rebuild_desktop_component() + finally : + self.lock.release() + + def _page_deleting(self, page_path, path = None): + screen_path = path + self.lock.acquire() + logger.debug("Destroying page (%s) %s", screen_path, page_path) + try : + items = self.screens[screen_path].items + if page_path in items: + del items[page_path] + self.rebuild_desktop_component() + finally : + self.lock.release() + + def _attention_cleared(self, path = None): + screen_path = path + if screen_path in self.attention_messages: + del self.attention_messages[screen_path] + self.rebuild_desktop_component() + + def _attention_requested(self, message = None, path = None): + screen_path = path + if not screen_path in self.attention_messages: + self.attention_messages[screen_path] = message + self.rebuild_desktop_component() + + """ + Private + """ + + def _enable(self, widget, device): + device.Enable() + + def _disable(self, widget, device): + device.Disable() + + def _cycle_screens_option_changed(self, client, connection_id, entry, args): + self.rebuild_desktop_component() + + def _remove_screen(self, screen_path): + print "*** removing %s from %s" % ( str(screen_path), str(self.screens)) + if screen_path in self.screens: + try : + del self.screens[screen_path] + except dbus.DBusException as e: + logger.debug("Error removing screen '%s'", screen_path, exc_info = e) + pass + self.rebuild_desktop_component() + + def _add_screen(self, screen_path): + logger.debug("Screen added %s", screen_path) + remote_screen = self.session_bus.get_object('org.gnome15.Gnome15', screen_path) + ( device_uid, device_model_name, device_usb_id, device_model_fullname ) = remote_screen.GetDeviceInformation() + screen = G15Screen(screen_path, device_model_fullname, device_uid) + self.screens[screen_path] = screen + if remote_screen.IsAttentionRequested(): + screen.message = remote_screen.GetMessage() + + def _device_added(self, screen_path): + self.rebuild_desktop_component() + + def _device_removed(self, screen_path): + self.rebuild_desktop_component() + + def _connect(self): + logger.debug("Connecting") + self._reset_attention() + self.service = self.session_bus.get_object('org.gnome15.Gnome15', '/org/gnome15/Service') + self.connected = True + logger.debug("Connected") + + # Load the initial screens + self.lock.acquire() + try : + for screen_path in self.service.GetScreens(): + logger.debug("Adding %s", screen_path) + self._add_screen(screen_path) + remote_screen = self.session_bus.get_object('org.gnome15.Gnome15', screen_path) + for page_path in remote_screen.GetPages(): + page = self.session_bus.get_object('org.gnome15.Gnome15', page_path) + if page.GetPriority() >= g15screen.PRI_LOW and page.GetPriority() < g15screen.PRI_HIGH: + self._add_page(screen_path, page_path, page) + finally : + self.lock.release() + + # Listen for events + self.session_bus.add_signal_receiver(self._device_added, dbus_interface = "org.gnome15.Service", signal_name = "DeviceAdded") + self.session_bus.add_signal_receiver(self._device_removed, dbus_interface = "org.gnome15.Service", signal_name = "DeviceRemoved") + self.session_bus.add_signal_receiver(self._add_screen, dbus_interface = "org.gnome15.Service", signal_name = "ScreenAdded") + self.session_bus.add_signal_receiver(self._remove_screen, dbus_interface = "org.gnome15.Service", signal_name = "ScreenRemoved") + self.session_bus.add_signal_receiver(self._page_created, dbus_interface = "org.gnome15.Screen", signal_name = "PageCreated", path_keyword = 'path') + self.session_bus.add_signal_receiver(self._page_title_changed, dbus_interface = "org.gnome15.Screen", signal_name = "PageTitleChanged", path_keyword = 'path') + self.session_bus.add_signal_receiver(self._page_deleting, dbus_interface = "org.gnome15.Screen", signal_name = "PageDeleting", path_keyword = 'path') + self.session_bus.add_signal_receiver(self._attention_requested, dbus_interface = "org.gnome15.Screen", signal_name = "AttentionRequested", path_keyword = 'path') + self.session_bus.add_signal_receiver(self._attention_cleared, dbus_interface = "org.gnome15.Screen", signal_name = "AttentionCleared", path_keyword = 'path') + + # We are now connected, so remove the start service menu item and allow cycling + self.rebuild_desktop_component() + + def _disconnect(self): + logger.debug("Disconnecting") + self.session_bus.remove_signal_receiver(self._device_added, dbus_interface = "org.gnome15.Service", signal_name = "DeviceAdded") + self.session_bus.remove_signal_receiver(self._device_removed, dbus_interface = "org.gnome15.Service", signal_name = "DeviceRemoved") + self.session_bus.remove_signal_receiver(self._add_screen, dbus_interface = "org.gnome15.Service", signal_name = "ScreenAdded") + self.session_bus.remove_signal_receiver(self._remove_screen, dbus_interface = "org.gnome15.Service", signal_name = "ScreenRemoved") + self.session_bus.remove_signal_receiver(self._page_created, dbus_interface = "org.gnome15.Screen", signal_name = "PageCreated") + self.session_bus.remove_signal_receiver(self._page_title_changed, dbus_interface = "org.gnome15.Screen", signal_name = "PageTitleChanged") + self.session_bus.remove_signal_receiver(self._page_deleting, dbus_interface = "org.gnome15.Screen", signal_name = "PageDeleting") + self.session_bus.remove_signal_receiver(self._attention_requested, dbus_interface = "org.gnome15.Screen", signal_name = "AttentionRequested") + self.session_bus.remove_signal_receiver(self._attention_cleared, dbus_interface = "org.gnome15.Screen", signal_name = "AttentionCleared") + + if self.service != None and self.connected: + for screen_path in dict(self.screens): + self._remove_screen(screen_path) + + self._reset_attention() + self._attention_requested("service", "g15-desktop-service is not running.") + + self.service = None + self.connected = False + self.rebuild_desktop_component() + + def _reset_attention(self): + self.attention_messages = {} + self.rebuild_desktop_component() + + def _add_page(self, screen_path, page_path, page): + logger.debug("Adding page %s to %s", page_path, screen_path) + items = self.screens[screen_path].items + if not page_path in items: + items[page_path] = page.GetTitle() + self.rebuild_desktop_component() + + def _indicator_options_changed(self, client, connection_id, entry, args): + self.options_changed() + + def _theme_changed(self, theme): + self.icons_changed() + + +class G15GtkMenuPanelComponent(G15DesktopComponent): + + def __init__(self): + self.screen_number = 0 + self.devices = [] + self.notify_message = None + G15DesktopComponent.__init__(self) + + def about_info(self, widget): + about = gtk.AboutDialog() + about.set_name("Gnome15") + about.set_version(g15globals.version) + about.set_license(GPL) + about.set_authors(AUTHORS) + about.set_documenters(["Brett Smith "]) + about.set_logo(gtk.gdk.pixbuf_new_from_file(g15icontools.get_app_icon(self.conf_client, "gnome15", 128))) + about.set_comments(_("Desktop integration for Logitech 'G' keyboards.")) + about.run() + about.hide() + + def scroll_event(self, widget, event): + + direction = event.direction + if direction == gtk.gdk.SCROLL_UP: + screen = self._get_active_screen_object() + self._close_notify_message() + screen.ClearPopup() + screen.Cycle(1) + elif direction == gtk.gdk.SCROLL_DOWN: + screen = self._get_active_screen_object() + self._close_notify_message() + screen.ClearPopup() + screen.Cycle(-1) + else: + """ + If there is only one device, right scroll cycles the backlight color, + otherwise toggle between the devices (used to select what to scroll with up + and down) + """ + if direction == gtk.gdk.SCROLL_LEFT: + self._get_active_screen_object().CycleKeyboard(-1) + elif direction == gtk.gdk.SCROLL_RIGHT: + if len(self.screens) > 1: + if self.screen_number >= len(self.screens) - 1: + self.screen_number = 0 + else: + self.screen_number += 1 + + self._set_active_screen_number() + else: + self._get_active_screen_object().CycleKeyboard(1) + + def rebuild_desktop_component(self): + logger.debug("Removing old menu items") + for item in self.last_items: + item.get_parent().remove(item) + item.destroy() + + self.last_items = [] + i = 0 + + # Remove the notify handles used for the previous cycle components + logger.debug("Removing old notify handles") + for h in self.notify_handles: + self.conf_client.notify_remove(h) + self.notify_handles = [] + + logger.debug("Building new menu") + if self.service and self.connected: + + item = gtk.MenuItem(_("Stop Desktop Service")) + item.connect("activate", self.stop_desktop_service) + self.add_service_item(item) + self.add_service_item(gtk.MenuItem()) + + try: + devices = self.service.GetDevices() + for device_path in devices: + remote_device = self.session_bus.get_object('org.gnome15.Gnome15', device_path) + screen_path = remote_device.GetScreen() + + screen = self.screens[screen_path] if len(screen_path) > 0 and screen_path in self.screens else None + + if screen: + if i > 0: + logger.debug("Adding separator") + self._append_item(gtk.MenuItem()) + # Disable + if len(devices) > 1: + item = gtk.MenuItem("Disable %s" % screen.device_model_fullname) + item.connect("activate", self._disable, remote_device) + self.add_service_item(item) + + # Cycle screens + item = gtk.CheckMenuItem(_("Cycle screens automatically")) + item.set_active(g15gconf.get_bool_or_default(self.conf_client, "/apps/gnome15/%s/cycle_screens" % screen.device_uid, True)) + self.notify_handles.append(self.conf_client.notify_add("/apps/gnome15/%s/cycle_screens" % screen.device_uid, self._cycle_screens_option_changed)) + item.connect("toggled", self._cycle_screens_changed, screen.device_uid) + self._append_item(item) + + # Alert message + if screen.message: + self._append_item(gtk.MenuItem(screen.message)) + + logger.debug("Adding items") + + + sorted_x = sorted(screen.items.iteritems(), key=operator.itemgetter(1)) + for item_key, text in sorted_x: + logger.debug("Adding item %s = %s ", item_key, text) + item = gtk.MenuItem(text) + item.connect("activate", self._show_page, item_key) + self._append_item(item) + else: + # Enable + if len(devices) > 1: + item = gtk.MenuItem(_("Enable %s") % remote_device.GetModelFullName()) + item.connect("activate", self._enable, remote_device) + self.add_service_item(item) + i += 1 + except Exception as e: + logger.debug("Failed to find devices, service probably stopped.", exc_info = e) + self.connected = False + self.rebuild_desktop_component() + + self.devices = devices + else: + self.devices = [] + self.add_start_desktop_service() + + self.menu.show_all() + self.check_attention() + + def add_start_desktop_service(self): + item = gtk.MenuItem(_("Start Desktop Service")) + item.connect("activate", self.start_desktop_service) + self.add_service_item(item) + + def add_service_item(self, item): + self._append_item(item) + + def initialise_desktop_component(self): + + self.last_items = [] + self.start_service_item = None + self.attention_item = None + self.notify_handles = [] + + # Indicator menu + self.menu = gtk.Menu() + self.create_component() + self.menu.show_all() + + def create_component(self): + raise Exception("Not implemented") + + def remove_attention_menu_item(self): + if self.attention_item != None: + self.menu.remove(self.attention_item) + self.attention_item.destroy() + self.menu.show_all() + self.attention_item = None + + def options_changed(self): + self.check_attention() + + """ + Private + """ + + def _get_active_screen_object(self): + screen = list(self.screens.values())[self.screen_number] + return self.session_bus.get_object('org.gnome15.Gnome15', screen.path) + + def _set_active_screen_number(self): + self._close_notify_message() + screen = list(self.screens.values())[self.screen_number] + body = _("%s is now the active keyboard. Use mouse wheel up and down to cycle screens on this device") % screen.device_model_fullname + self.notify_message = g15notify.notify(screen.device_model_fullname, body, "preferences-desktop-keyboard-shortcuts") + + def _close_notify_message(self): + if self.notify_message is not None: + try: + self.notify_message.close() + except Exception as e: + logger.debug("Failed to close message.", exc_info = e) + self.notify_message = None + + def _append_item(self, item, menu = None): + self.last_items.append(item) + if menu is None: + menu = self.menu + menu.append(item) + + def _show_page(self,event, page_path): + self.show_page(page_path) + + def _cycle_screens_changed(self, widget, device_uid): + self.conf_client.set_bool("/apps/gnome15/%s/cycle_screens" % device_uid, widget.get_active()) + +if __name__ == "__main__": + print "g15-systemtray installed = %s, enabled = %s" % ( is_desktop_application_installed("g15-systemtray"), is_autostart_application("g15-systemtray") ) + print "g15-desktop-service installed = %s, enabled = %s" % ( is_desktop_application_installed("g15-desktop-service"), is_autostart_application("g15-desktop-service") ) + print "g15-indicator installed = %s, enabled = %s" % ( is_desktop_application_installed("g15-indicator"), is_autostart_application("g15-indicator") ) + print "dropbox installed = %s, enabled = %s" % ( is_desktop_application_installed("dropbox"), is_autostart_application("dropbox") ) + print "xdropbox installed = %s, enabled = %s" % ( is_desktop_application_installed("xdropbox"), is_autostart_application("xdropbox") ) + print "nepomukserver installed = %s, enabled = %s" % ( is_desktop_application_installed("nepomukserver"), is_autostart_application("nepomukserver") ) + set_autostart_application("g15-indicator", False) + print "g15-indicator installed = %s, enabled = %s" % ( is_desktop_application_installed("g15-indicator"), is_autostart_application("g15-indicator") ) + set_autostart_application("g15-indicator", True) + print "g15-indicator installed = %s, enabled = %s" % ( is_desktop_application_installed("g15-indicator"), is_autostart_application("g15-indicator") ) diff --git a/src/gnome15/g15devices.py b/src/gnome15/g15devices.py new file mode 100644 index 0000000..1397d3d --- /dev/null +++ b/src/gnome15/g15devices.py @@ -0,0 +1,485 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2011 Brett Smith +# +# 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 . + +import gnome15.g15locale as g15locale +_ = g15locale.get_translation("gnome15").ugettext + +import usb +import g15driver +import g15actions +import util.g15pythonlang as g15pythonlang +import g15drivermanager + +# Logging +import logging +logger = logging.getLogger(__name__) + +''' +Keyboard layouts +''' + +z10_key_layout = [ + [ g15driver.G_KEY_L1, g15driver.G_KEY_L2, g15driver.G_KEY_L3, g15driver.G_KEY_L4, g15driver.G_KEY_L5 ] + ] + +z10_action_keys = { g15driver.NEXT_SELECTION: g15actions.ActionBinding(g15driver.NEXT_SELECTION, [ g15driver.G_KEY_L4 ], g15driver.KEY_STATE_UP), + g15driver.PREVIOUS_SELECTION: g15actions.ActionBinding(g15driver.PREVIOUS_SELECTION, [ g15driver.G_KEY_L3 ], g15driver.KEY_STATE_UP), + g15driver.SELECT: g15actions.ActionBinding(g15driver.SELECT, [ g15driver.G_KEY_L5 ], g15driver.KEY_STATE_UP), + g15driver.MENU: g15actions.ActionBinding(g15driver.MENU, [ g15driver.G_KEY_L1 ], g15driver.KEY_STATE_UP), + g15driver.CLEAR: g15actions.ActionBinding(g15driver.CLEAR, [ g15driver.G_KEY_L2 ], g15driver.KEY_STATE_HELD), + g15driver.VIEW: g15actions.ActionBinding(g15driver.VIEW, [ g15driver.G_KEY_L2 ], g15driver.KEY_STATE_UP), + g15driver.NEXT_PAGE: g15actions.ActionBinding(g15driver.NEXT_PAGE, [ g15driver.G_KEY_L4 ], g15driver.KEY_STATE_HELD), + g15driver.PREVIOUS_PAGE: g15actions.ActionBinding(g15driver.PREVIOUS_PAGE, [ g15driver.G_KEY_L3 ], g15driver.KEY_STATE_HELD) + } + +g11_key_layout = [ + [ g15driver.G_KEY_G1, g15driver.G_KEY_G2, g15driver.G_KEY_G3 ], + [ g15driver.G_KEY_G4, g15driver.G_KEY_G5, g15driver.G_KEY_G6 ], + [ g15driver.G_KEY_G7, g15driver.G_KEY_G8, g15driver.G_KEY_G9 ], + [ g15driver.G_KEY_G10, g15driver.G_KEY_G11, g15driver.G_KEY_G12 ], + [ g15driver.G_KEY_G13, g15driver.G_KEY_G14, g15driver.G_KEY_G15 ], + [ g15driver.G_KEY_G16, g15driver.G_KEY_G17, g15driver.G_KEY_G18 ], + [ g15driver.G_KEY_M1, g15driver.G_KEY_M2, g15driver.G_KEY_M3, g15driver.G_KEY_MR ] + ] + +g11_action_keys = { + g15driver.MEMORY_1: g15actions.ActionBinding(g15driver.MEMORY_1, [ g15driver.G_KEY_M1 ], g15driver.KEY_STATE_UP), + g15driver.MEMORY_2: g15actions.ActionBinding(g15driver.MEMORY_2, [ g15driver.G_KEY_M2 ], g15driver.KEY_STATE_UP), + g15driver.MEMORY_3: g15actions.ActionBinding(g15driver.MEMORY_3, [ g15driver.G_KEY_M3 ], g15driver.KEY_STATE_UP) + } + +g15v1_key_layout = [ + [ g15driver.G_KEY_G1, g15driver.G_KEY_G2, g15driver.G_KEY_G3 ], + [ g15driver.G_KEY_G4, g15driver.G_KEY_G5, g15driver.G_KEY_G6 ], + [ g15driver.G_KEY_G7, g15driver.G_KEY_G8, g15driver.G_KEY_G9 ], + [ g15driver.G_KEY_G10, g15driver.G_KEY_G11, g15driver.G_KEY_G12 ], + [ g15driver.G_KEY_G13, g15driver.G_KEY_G14, g15driver.G_KEY_G15 ], + [ g15driver.G_KEY_G16, g15driver.G_KEY_G17, g15driver.G_KEY_G18 ], + [ g15driver.G_KEY_L1, g15driver.G_KEY_L2, g15driver.G_KEY_L3, g15driver.G_KEY_L4, g15driver.G_KEY_L5 ], + [ g15driver.G_KEY_M1, g15driver.G_KEY_M2, g15driver.G_KEY_M3, g15driver.G_KEY_MR ] + ] + +g510_key_layout = [ + [ g15driver.G_KEY_G1, g15driver.G_KEY_G2, g15driver.G_KEY_G3 ], + [ g15driver.G_KEY_G4, g15driver.G_KEY_G5, g15driver.G_KEY_G6 ], + [ g15driver.G_KEY_G7, g15driver.G_KEY_G8, g15driver.G_KEY_G9 ], + [ g15driver.G_KEY_G10, g15driver.G_KEY_G11, g15driver.G_KEY_G12 ], + [ g15driver.G_KEY_G13, g15driver.G_KEY_G14, g15driver.G_KEY_G15 ], + [ g15driver.G_KEY_G16, g15driver.G_KEY_G17, g15driver.G_KEY_G18 ], + [ g15driver.G_KEY_L1, g15driver.G_KEY_L2, g15driver.G_KEY_L3, g15driver.G_KEY_L4, g15driver.G_KEY_L5 ], + [ g15driver.G_KEY_M1, g15driver.G_KEY_M2, g15driver.G_KEY_M3, g15driver.G_KEY_MR ] + ] + +g15v2_key_layout = [ + [ g15driver.G_KEY_G1, g15driver.G_KEY_G2, g15driver.G_KEY_G3 ], + [ g15driver.G_KEY_G4, g15driver.G_KEY_G5, g15driver.G_KEY_G6 ], + [ g15driver.G_KEY_L1, g15driver.G_KEY_L2, g15driver.G_KEY_L3, g15driver.G_KEY_L4, g15driver.G_KEY_L5 ], + [ g15driver.G_KEY_M1, g15driver.G_KEY_M2, g15driver.G_KEY_M3, g15driver.G_KEY_MR ] + ] + +g13_key_layout = [ + [ g15driver.G_KEY_G1, g15driver.G_KEY_G2, g15driver.G_KEY_G3, g15driver.G_KEY_G4, g15driver.G_KEY_G5, g15driver.G_KEY_G6, g15driver.G_KEY_G7 ], + [ g15driver.G_KEY_G8, g15driver.G_KEY_G9, g15driver.G_KEY_G10, g15driver.G_KEY_G11, g15driver.G_KEY_G12, g15driver.G_KEY_G13, g15driver.G_KEY_G14 ], + [ g15driver.G_KEY_G15, g15driver.G_KEY_G16, g15driver.G_KEY_G17, g15driver.G_KEY_G18, g15driver.G_KEY_G19 ], + [ g15driver.G_KEY_G20, g15driver.G_KEY_G21, g15driver.G_KEY_G22 ], + [ g15driver.G_KEY_L1, g15driver.G_KEY_L2, g15driver.G_KEY_L3, g15driver.G_KEY_L4, g15driver.G_KEY_L5 ], + [ g15driver.G_KEY_M1, g15driver.G_KEY_M2, g15driver.G_KEY_M3, g15driver.G_KEY_MR ] + ] + +g930_key_layout = [ + [ g15driver.G_KEY_G1, g15driver.G_KEY_G2, g15driver.G_KEY_G3 ] + ] + +""" +Unfortunately we have to leave L1 clear for g15daemon for the moment +""" +g15_action_keys = { g15driver.NEXT_SELECTION: g15actions.ActionBinding(g15driver.NEXT_SELECTION, [ g15driver.G_KEY_L4 ], g15driver.KEY_STATE_UP), + g15driver.PREVIOUS_SELECTION: g15actions.ActionBinding(g15driver.PREVIOUS_SELECTION, [ g15driver.G_KEY_L3 ], g15driver.KEY_STATE_UP), + g15driver.SELECT: g15actions.ActionBinding(g15driver.SELECT, [ g15driver.G_KEY_L5 ], g15driver.KEY_STATE_UP), + g15driver.MENU: g15actions.ActionBinding(g15driver.MENU, [ g15driver.G_KEY_L1 ], g15driver.KEY_STATE_UP), + g15driver.CLEAR: g15actions.ActionBinding(g15driver.CLEAR, [ g15driver.G_KEY_L2 ], g15driver.KEY_STATE_HELD), + g15driver.VIEW: g15actions.ActionBinding(g15driver.VIEW, [ g15driver.G_KEY_L2 ], g15driver.KEY_STATE_UP), + g15driver.NEXT_PAGE: g15actions.ActionBinding(g15driver.NEXT_PAGE, [ g15driver.G_KEY_L4 ], g15driver.KEY_STATE_HELD), + g15driver.PREVIOUS_PAGE: g15actions.ActionBinding(g15driver.PREVIOUS_PAGE, [ g15driver.G_KEY_L3 ], g15driver.KEY_STATE_HELD), + g15driver.MEMORY_1: g15actions.ActionBinding(g15driver.MEMORY_1, [ g15driver.G_KEY_M1 ], g15driver.KEY_STATE_UP), + g15driver.MEMORY_2: g15actions.ActionBinding(g15driver.MEMORY_2, [ g15driver.G_KEY_M2 ], g15driver.KEY_STATE_UP), + g15driver.MEMORY_3: g15actions.ActionBinding(g15driver.MEMORY_3, [ g15driver.G_KEY_M3 ], g15driver.KEY_STATE_UP) + } + +""" +G110 - Only actions we need really are the memory bank ones +""" +g110_action_keys = { + g15driver.MEMORY_1: g15actions.ActionBinding(g15driver.MEMORY_1, [ g15driver.G_KEY_M1 ], g15driver.KEY_STATE_UP), + g15driver.MEMORY_2: g15actions.ActionBinding(g15driver.MEMORY_2, [ g15driver.G_KEY_M2 ], g15driver.KEY_STATE_UP), + g15driver.MEMORY_3: g15actions.ActionBinding(g15driver.MEMORY_3, [ g15driver.G_KEY_M3 ], g15driver.KEY_STATE_UP) + } + +g110_key_layout = [ + [ g15driver.G_KEY_G1, g15driver.G_KEY_G7 ], + [ g15driver.G_KEY_G2, g15driver.G_KEY_G8 ], + [ g15driver.G_KEY_G3, g15driver.G_KEY_G9 ], + [ g15driver.G_KEY_G4, g15driver.G_KEY_G10 ], + [ g15driver.G_KEY_G5, g15driver.G_KEY_G11], + [ g15driver.G_KEY_G6, g15driver.G_KEY_G12 ], + [ g15driver.G_KEY_MIC_MUTE, g15driver.G_KEY_HEADPHONES_MUTE ], + [ g15driver.G_KEY_M1, g15driver.G_KEY_M2, g15driver.G_KEY_M3, g15driver.G_KEY_MR ] + ] + +""" +G19 +""" +g19_key_layout = [ + [ g15driver.G_KEY_G1, g15driver.G_KEY_G7 ], + [ g15driver.G_KEY_G2, g15driver.G_KEY_G8 ], + [ g15driver.G_KEY_G3, g15driver.G_KEY_G9 ], + [ g15driver.G_KEY_G4, g15driver.G_KEY_G10 ], + [ g15driver.G_KEY_G5, g15driver.G_KEY_G11 ], + [ g15driver.G_KEY_G6, g15driver.G_KEY_G12 ], + [ g15driver.G_KEY_UP ], + [ g15driver.G_KEY_LEFT, g15driver.G_KEY_OK, g15driver.G_KEY_RIGHT ], + [ g15driver.G_KEY_DOWN ], + [ g15driver.G_KEY_MENU, g15driver.G_KEY_BACK, g15driver.G_KEY_SETTINGS ], + [ g15driver.G_KEY_M1, g15driver.G_KEY_M2, g15driver.G_KEY_M3, g15driver.G_KEY_MR ], + ] +g19_action_keys = { g15driver.NEXT_SELECTION: g15actions.ActionBinding(g15driver.NEXT_SELECTION, [ g15driver.G_KEY_DOWN ], g15driver.KEY_STATE_DOWN), + g15driver.PREVIOUS_SELECTION: g15actions.ActionBinding(g15driver.PREVIOUS_SELECTION, [ g15driver.G_KEY_UP ], g15driver.KEY_STATE_DOWN), + g15driver.SELECT: g15actions.ActionBinding(g15driver.SELECT, [ g15driver.G_KEY_OK ], g15driver.KEY_STATE_UP), + g15driver.MENU: g15actions.ActionBinding(g15driver.MENU, [ g15driver.G_KEY_MENU ], g15driver.KEY_STATE_UP), + g15driver.CLEAR: g15actions.ActionBinding(g15driver.CLEAR, [ g15driver.G_KEY_BACK ], g15driver.KEY_STATE_UP), + g15driver.VIEW: g15actions.ActionBinding(g15driver.VIEW, [ g15driver.G_KEY_SETTINGS ], g15driver.KEY_STATE_UP), + g15driver.NEXT_PAGE: g15actions.ActionBinding(g15driver.NEXT_PAGE, [ g15driver.G_KEY_RIGHT ], g15driver.KEY_STATE_UP), + g15driver.PREVIOUS_PAGE: g15actions.ActionBinding(g15driver.PREVIOUS_PAGE, [ g15driver.G_KEY_LEFT ], g15driver.KEY_STATE_UP), + g15driver.MEMORY_1: g15actions.ActionBinding(g15driver.MEMORY_1, [ g15driver.G_KEY_M1 ], g15driver.KEY_STATE_UP), + g15driver.MEMORY_2: g15actions.ActionBinding(g15driver.MEMORY_2, [ g15driver.G_KEY_M2 ], g15driver.KEY_STATE_UP), + g15driver.MEMORY_3: g15actions.ActionBinding(g15driver.MEMORY_3, [ g15driver.G_KEY_M3 ], g15driver.KEY_STATE_UP) + } + +""" +MX5500 + +Only two keys near the LCD, so various combinations of keys and holding keys is used to +provide the 6 most basic actions +""" + +mx5500_key_layout = [ + [ g15driver.G_KEY_UP, g15driver.G_KEY_DOWN ] + ] +mx5500_action_keys = { g15driver.NEXT_SELECTION: g15actions.ActionBinding(g15driver.NEXT_SELECTION, [ g15driver.G_KEY_DOWN ], g15driver.KEY_STATE_UP), + g15driver.PREVIOUS_SELECTION: g15actions.ActionBinding(g15driver.PREVIOUS_SELECTION, [ g15driver.G_KEY_UP ], g15driver.KEY_STATE_UP), + g15driver.SELECT: g15actions.ActionBinding(g15driver.SELECT, [ g15driver.G_KEY_DOWN ], g15driver.KEY_STATE_HELD), + g15driver.MENU: g15actions.ActionBinding(g15driver.MENU, [ g15driver.G_KEY_UP ], g15driver.KEY_STATE_HELD), + g15driver.CLEAR: g15actions.ActionBinding(g15driver.CLEAR, [ g15driver.G_KEY_UP, g15driver.G_KEY_DOWN ], g15driver.KEY_STATE_HELD), + g15driver.VIEW: g15actions.ActionBinding(g15driver.VIEW, [ g15driver.G_KEY_UP, g15driver.G_KEY_DOWN ], g15driver.KEY_STATE_UP) + } + +# Registered Logitech models +device_list = { } +device_by_usb_id = { } +__cached_devices = [] + +class DeviceInfo(): + """ + Represents the characteristics of a single model of a Logitech device. + """ + def __init__(self, model_id, usb_id_list, controls_usb_id_list, key_layout, bpp, lcd_size, macros, model_fullname, action_keys): + """ + Creates a new DeviceInfo and add's it to the registered device types + + Keyword arguments + model_id -- model ID (as found in g15driver constants) + usb_id_list -- list or single tuple with vendor and product codes (this is the device searched for). + controls_usb_id_list -- tuple with vendor and product codes for the controls device + key_layout -- keyboard layout + bpp -- the number of bits per pixel (or 0 for no LCD) + lcd_size -- the size of the LCD (or None for no LCD) + macros -- the model has macro keys (G-Keys) + model_fullname -- full name of the model + action_keys -- default keybinds to use for actions + """ + self.model_id = model_id + self.key_layout = key_layout + self.macros = macros + self.bpp = bpp + self.lcd_size = lcd_size + self.action_keys = action_keys + self.model_fullname = model_fullname + device_list[self.model_id] = self + + # Some devices (1 currently) use a different USBID for controls + if controls_usb_id_list is None: + controls_usb_id_list = usb_id_list + + # Some devices may have multiple usb ID's (for audio mode) + if not isinstance(usb_id_list, list): + usb_id_list = [usb_id_list] + if not isinstance(controls_usb_id_list, list): + controls_usb_id_list = [controls_usb_id_list] + if not len(usb_id_list) == len(controls_usb_id_list): + raise Exception("Controls USB ID list is not the same length as USB ID list") + self.usb_id_list = usb_id_list + self.controls_usb_id_list = controls_usb_id_list + + # Map all the devices + for c in self.usb_id_list: + device_by_usb_id[c] = self + + # Gather all keys in the layout + self.all_keys = [] + for row in self.key_layout: + for key in row: + self.all_keys.append(key) + + def matches(self, usb_id): + return usb_id in self.controls_usb_id_list or usb_id in self.usb_id_list + + def __repr__(self): + return "DeviceInfo [%s/%s] model: %s (%s). Has a %d BPP screen of %dx%d. " % \ + ( str(self.usb_id_list), str(self.controls_usb_id_list), self.model_id, self.model_fullname, self.bpp, self.lcd_size[0], self.lcd_size[1]) + +class Device(): + """ + Represents a single discovered device. + + Keyword arguments + usb_id -- the actual ID + usb_device -- the USB device + device_info -- DeviceInfo object containing static model information + """ + def __init__(self, usb_id, controls_usb_id, usb_device, index, device_info): + self.usb_id = usb_id + self.controls_usb_id = controls_usb_id + + self.usb_device = usb_device + self.index = index + self.uid = "%s_%d" % ( device_info.model_id, index ) + self.model_id = device_info.model_id + self.key_layout = device_info.key_layout + self.bpp = device_info.bpp + self.lcd_size = device_info.lcd_size + self.model_fullname = device_info.model_fullname + self.all_keys = device_info.all_keys + self.action_keys = device_info.action_keys + + def get_key_index(self, key): + if key in self.all_keys: + self.all_keys.index(key) + + def __hash__(self): + return self.ui.__hash() + + def __eq__(self, o): + try: + return o is not None and self.uid == o.uid + except AttributeError as e: + logger.debug('AttributeError when comparing Device', exc_info = e) + return False + + def __repr__(self): + usb_str = hex(self.usb_id[0]) if self.usb_id is not None and len(self.usb_id) > 0 else "Unknown" + usb_str2 = hex(self.usb_id[1]) if self.usb_id is not None and len(self.usb_id) > 1 else "Unknown" + sz1 = self.lcd_size[0] if self.lcd_size is not None and len(self.lcd_size) > 0 else "??" + sz2 = self.lcd_size[1] if self.lcd_size is not None and len(self.lcd_size) > 1 else "??" + return "Device [%s] %s model: %s (%s) on USB ID %s:%s. Has a %d BPP screen of %dx%d. " % \ + ( str(self.usb_device), self.uid, self.model_id, self.model_fullname, usb_str, usb_str2, self.bpp, sz1, sz2) + +def are_keys_reserved(model_id, keys): + if len(keys) < 1: + raise Exception("Empty key list provided") + device_info = get_device_info(model_id) + if device_info is None: + raise Exception("No device with ID of %s" % model_id) + for action_binding in device_info.action_keys.values(): + if sorted(keys) == sorted(action_binding.keys): + return True + return False + +def get_device_info(model_id): + return device_list[model_id] + +def is_enabled(conf_client, device): + val = conf_client.get("/apps/gnome15/%s/enabled" % device.uid) + return ( val == None and device.model_id != "virtual" ) or ( val is not None and val.get_bool() ) + +def set_enabled(conf_client, device, enabled): + conf_client.set_bool("/apps/gnome15/%s/enabled" % device.uid, enabled) + +def get_device(uid): + """ + Find the device with the specified UID. + """ + for d in find_all_devices(): + if d.uid == uid: + return d + +def find_all_devices(do_cache = True): + global __cached_devices + + """ + Get a list of Device objects, one for each supported device that is plugged in. + There may be more than one device of the same type. + """ + + # If we have pydev, we can cache the devices + if do_cache and have_udev and len(__cached_devices) != 0: + return __cached_devices + + device_map = {} + + # Find all supported devices plugged into USB + for bus in usb.busses(): + for usb_device in bus.devices: + key = ( usb_device.idVendor, usb_device.idProduct ) + # Is a supported device + if not key in device_map: + device_map[key] = [] + device_map[key].append(usb_device) + + # Turn the found USB devices into Device objects + devices = [] + indices = {} + + for device_key in device_map: + usb_devices = device_map[device_key] + for usb_device in usb_devices: + if device_key in device_by_usb_id: + device_info = device_by_usb_id[device_key] + """ + Take the quirk of the G11/G15 into account. This check means that only one of each + type can exist at a time, but any more is pretty unlikely + """ + if device_info.model_id == g15driver.MODEL_G15_V1 and not (0x046d, 0xc222) in device_map: + # Actually a G11, will be detected with id (0x046d, 0xc225) + continue + elif device_info.model_id == g15driver.MODEL_G11 and (0x046d, 0xc222) in device_map: + # Actually a G15v1 + device_info = device_list[g15driver.MODEL_G15_V1] + + """ + Now create the device instance that will be used by the caller + """ + index = 0 if not device_key in indices else indices[device_key] + controls_usb_id = device_info.controls_usb_id_list[device_info.usb_id_list.index(device_key)] + devices.append(Device(device_key, controls_usb_id, usb_device, index, device_info)) + indices[device_key] = index + 1 + + + """ + If the GTK driver is installed, add a virtual device as well + """ + if g15drivermanager.get_driver_mod("gtk"): + devices.append(Device((0x0000, 0x0000), (0x0000, 0x0000), None, 0, device_list['virtual'])) + + # If we have pydev, we can cache the devices + if have_udev and do_cache: + __cached_devices += devices + + return devices + +def find_device(models): + for lg_model in find_all_devices(): + for model in models: + if lg_model.model_name == model: + return lg_model + +def _get_cached_device_by_usb_id(usb_id): + for c in __cached_devices: + if c.usb_id == usb_id: + return c + +""" +Register all supported models +""" +if g15drivermanager.get_driver_mod("gtk"): + DeviceInfo('virtual', (0x0000, 0x0000), None, [], 0, ( 0, 0 ), False, _("Virtual LCD Window"), None) +DeviceInfo(g15driver.MODEL_G11, (0x046d, 0xc225), None, g11_key_layout, 0, ( 0, 0 ), True, _("Logitech G11 Keyboard"), g11_action_keys) +DeviceInfo(g15driver.MODEL_G19, (0x046d, 0xc229), None, g19_key_layout, 16, ( 320, 240 ), True, _("Logitech G19 Gaming Keyboard"), g19_action_keys) +DeviceInfo(g15driver.MODEL_G15_V1, (0x046d, 0xc221), (0x046d, 0xc222), g15v1_key_layout, 1, ( 160, 43 ), True, _("Logitech G15 Gaming Keyboard (version 1)"), g15_action_keys) +DeviceInfo(g15driver.MODEL_G15_V2, (0x046d, 0xc227), None, g15v2_key_layout, 1, ( 160, 43 ), True, _("Logitech G15 Gaming Keyboard (version 2)"), g15_action_keys) +DeviceInfo(g15driver.MODEL_G13, (0x046d, 0xc21c), None, g13_key_layout, 1, ( 160, 43 ), True, _("Logitech G13 Advanced Gameboard"), g15_action_keys) +DeviceInfo(g15driver.MODEL_G510, [ (0x046d, 0xc22d), + (0x046d, 0xc22e) ], None, g510_key_layout, 1, ( 160, 43 ), True, _("Logitech G510 Keyboard"), g15_action_keys) +DeviceInfo(g15driver.MODEL_Z10, (0x046d, 0x0a07), None, z10_key_layout, 1, ( 160, 43 ), False, _("Logitech Z10 Speakers"), z10_action_keys) +DeviceInfo(g15driver.MODEL_G110, (0x046d, 0xc22b), None, g110_key_layout, 0, ( 0, 0 ), True, _("Logitech G110 Keyboard"), g110_action_keys) +DeviceInfo(g15driver.MODEL_GAMEPANEL, (0x046d, 0xc251), None, g15v1_key_layout, 1, ( 160, 43 ), True, _("Logitech GamePanel"), g15_action_keys) +DeviceInfo(g15driver.MODEL_G930, (0x046d, 0xa1f), None, g930_key_layout, 0, ( 0, 0 ), True, _("Logitech G930 Headphones"), {}) +DeviceInfo(g15driver.MODEL_G35, (0x046d, 0xa15), None, g930_key_layout, 0, ( 0, 0 ), True, _("Logitech G35 Headphones"), {}) + +# When I get hold of an MX5500, I will add Bluetooth detection as well +DeviceInfo(g15driver.MODEL_MX5500, (0x0000, 0x0000), (0x0000, 0x0000), mx5500_key_layout, 1, ( 136, 32 ), False, _("Logitech MX5500"), mx5500_action_keys) + +# If we have pyudev, we can monitor for devices being plugged in and unplugged +have_udev = False +device_added_listeners = [] +device_removed_listeners = [] + +def __device_added(observer, device): + uevent_attr = device.attributes.get('uevent', None) + if uevent_attr != None: + uevent = g15pythonlang.parse_as_properties(uevent_attr) + if "PRODUCT" in uevent: + if device.attributes.get("subsystem", None) == "usb": + major,minor,_ = uevent["PRODUCT"].split("/") + else: + _,major,minor,_ = uevent["PRODUCT"].split("/") + for c in device_list: + device_info = device_list[c] + usb_id = (int(major, 16), int(minor, 16)) + if device_info.matches(usb_id): + if not _get_cached_device_by_usb_id(usb_id): + del __cached_devices[:] + find_all_devices() + for r in reversed(__cached_devices): + if r.usb_id == usb_id: + logger.info("Added device %s", r) + for l in device_added_listeners: + l(r) + break + break + +def __device_removed(observer, device): + current_devices = list(__cached_devices) + new_devices = find_all_devices(do_cache = False) + found = False + for d in current_devices: + for e in new_devices: + if e.uid == d.uid: + found = True + break + if not found: + if d in __cached_devices: + __cached_devices.remove(d) + for l in device_removed_listeners: + l(d) + break + +try: + import pyudev.glib + __context = pyudev.Context() + __monitor = pyudev.Monitor.from_netlink(__context) + __observer = pyudev.glib.GUDevMonitorObserver(__monitor) + __observer.connect('device-added', __device_added) + __observer.connect('device-removed', __device_removed) + find_all_devices() + have_udev = True + __monitor.start() +except Exception as e: + logger.info("Failed to get PyUDev context, hot plugging support not available", exc_info = e) + +if __name__ == "__main__": + for device in find_all_devices(): + print str(device) + \ No newline at end of file diff --git a/src/gnome15/g15driver.py b/src/gnome15/g15driver.py new file mode 100644 index 0000000..cbc75d2 --- /dev/null +++ b/src/gnome15/g15driver.py @@ -0,0 +1,794 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2010 Brett Smith +# +# 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 . + +""" +Default actions + +TODO MOVE THESE TO G15ACTIONS +""" +NEXT_SELECTION = "next-sel" +PREVIOUS_SELECTION = "prev-sel" +NEXT_PAGE = "next-page" +PREVIOUS_PAGE = "prev-page" +SELECT = "select" +VIEW = "view" +CLEAR = "clear" +MENU = "menu" +MEMORY_1 = "memory-1" +MEMORY_2 = "memory-2" +MEMORY_3 = "memory-3" + +""" +Bitmask values for setting the M key LED lights. See set_mkey_lights() +""" +MKEY_LIGHT_1 = 1<<0 +MKEY_LIGHT_2 = 1<<1 +MKEY_LIGHT_3 = 1<<2 +MKEY_LIGHT_MR = 1<<3 + +""" +Constants for key codes +""" +KEY_STATE_UP = 0 +KEY_STATE_DOWN = 1 +KEY_STATE_HELD = 2 + +""" +G keys + +G15v1 - G1-G18 +G15v1 - G1-G18 +G13 - G1-22 +G19 - G1-G12 +""" +G_KEY_G1 = "g1" +G_KEY_G2 = "g2" +G_KEY_G3 = "g3" +G_KEY_G4 = "g4" +G_KEY_G5 = "g5" +G_KEY_G6 = "g6" +G_KEY_G7 = "g7" +G_KEY_G8 = "g8" +G_KEY_G9 = "g9" +G_KEY_G10 = "g10" +G_KEY_G11 = "g11" +G_KEY_G12 = "g12" +G_KEY_G13 = "g13" +G_KEY_G14 = "g14" +G_KEY_G15 = "g15" +G_KEY_G16 = "g16" +G_KEY_G17 = "g17" +G_KEY_G18 = "g18" +G_KEY_G19 = "g19" +G_KEY_G20 = "g20" +G_KEY_G21 = "g21" +G_KEY_G22 = "g22" + +""" +Joystick (G13) +""" +G_KEY_JOY_LEFT = "jl" +G_KEY_JOY_DOWN = "jd" +G_KEY_JOY_CENTER = "jc" +G_KEY_JOY = "js" + + +""" +Display keys +""" +G_KEY_BACK = "back" +G_KEY_DOWN = "down" +G_KEY_LEFT = "left" +G_KEY_MENU = "menu" +G_KEY_OK = "ok" +G_KEY_RIGHT = "right" +G_KEY_SETTINGS = "settings" +G_KEY_UP = "up" + +""" +M keys. On all models +""" +G_KEY_M1 = "m1" +G_KEY_M2 = "m2" +G_KEY_M3 = "m3" +G_KEY_MR = "mr" + +""" +L-Keys. On g15v1, v2, g13 and g19. NOT on g110 +""" +G_KEY_L1 = "l1" +G_KEY_L2 = "l2" +G_KEY_L3 = "l3" +G_KEY_L4 = "l4" +G_KEY_L5 = "l5" + +""" +Light key. On all models +""" +G_KEY_LIGHT = "light" + +""" +Multimedia keys +""" +G_KEY_WINKEY_SWITCH = "win" +G_KEY_NEXT = "next" +G_KEY_PREV = "prev" +G_KEY_STOP = "stop" +G_KEY_PLAY = "play" +G_KEY_MUTE = "mute" +G_KEY_VOL_UP = "vol-up" +G_KEY_VOL_DOWN = "vol-down" + +""" +G110 only +""" +G_KEY_MIC_MUTE = "mic-mute" +G_KEY_HEADPHONES_MUTE = "headphones-mute" + +""" +Models +""" +MODEL_G15_V1 = "g15v1" +MODEL_G15_V2 = "g15v2" +MODEL_G11 = "g11" +MODEL_G13 = "g13" +MODEL_G19 = "g19" +MODEL_G930 = "g930" +MODEL_G35 = "g35" +MODEL_G510 = "g510" +MODEL_G110 = "g110" +MODEL_Z10 = "z10" +MODEL_MX5500 = "mx5500" +MODEL_GAMEPANEL = "gamepanel" + +MODELS = [ MODEL_G15_V1, MODEL_G15_V2, MODEL_G11, MODEL_G13, MODEL_G19, MODEL_G510, MODEL_G110, MODEL_Z10, MODEL_MX5500, MODEL_GAMEPANEL, MODEL_G930, MODEL_G35 ] + +""" +Control hints +""" +HINT_DIMMABLE = 1 << 0 +HINT_SHADEABLE = 1 << 1 +HINT_FOREGROUND = 1 << 2 +HINT_BACKGROUND = 1 << 3 +HINT_HIGHLIGHT = 1 << 4 +HINT_SWITCH = 1 << 5 +HINT_MKEYS = 1 << 6 +HINT_VIRTUAL = 1 << 7 +HINT_RED_BLUE_LED = 1 << 8 + +# 16bit 565 +CAIRO_IMAGE_FORMAT=4 + +FX_QUEUE = "ControlEffects" + +import util.g15scheduler as g15scheduler +import time +import colorsys +from threading import Lock +from threading import Event +import logging +logger = logging.getLogger(__name__) + +seq_no = 0 + +def get_key_names(keys): + """ + Get the string name of the key given it's code + """ + key_names = [] + for key in keys: + key_names.append((key[:1].upper() + key[1:].lower()).replace('-',' ')) + return key_names + +def zeroize(val): + """ + Zeroise a control value (will be used for the fully off value). The type + returned will be the same as the type provided + + Keyword arguments: + val -- current value + """ + if isinstance(val, int): + return 0 + elif isinstance(val, tuple): + return (0, 0, 0) + else: + return False + +def get_mask_for_memory_bank(bank): + """ + Get the M_KEY_LIGHT_* mask given the memory bank number (1,2 or 3) + + Keyword arguments: + bank -- memory bank + """ + if bank == 1: + return MKEY_LIGHT_1 + elif bank == 2: + return MKEY_LIGHT_2 + elif bank == 3: + return MKEY_LIGHT_3 + +def get_memory_bank_for_mask(val): + """ + Get the memory bank that is activated given the M_KEY_LIGHT_* mask + + Keyword arguments: + val -- light key mask value + """ + if val & MKEY_LIGHT_3 != 0: + return 3 + elif val & MKEY_LIGHT_2 != 0: + return 2 + elif val & MKEY_LIGHT_1 != 0: + return 1 + return 0 + +class Control(): + """ + Represents a single "Control". Each control represents a feature of the + device that may be adjusted. For example, the red, green and blue LEDs of + the keyboard backlight on a G19. + + A control's value will be of different classes depending on the type of + control this instance represents. + + A On/Off or single value controls (such as brightness of a single LED is + represented as as int). + + A colour control is a tuple consisting of R,G and B intensity values. + + Other controls are boolean (currently not used) + """ + + def __init__(self, id, name, value = 0.0, lower = 0.0, upper = 255.0, hint = 0): + """ + Constructor + + Keyword arguments: + id -- unique control ID + name -- an english name for the control + value -- the initial value (int,tuple or bool) + lower -- minimum value the control may be + upper -- maximum value the control may be + hint -- bitmask of HINT_ consants describing the control + """ + self.id = id + self.hint = hint + self.name = name + self.lower = lower + self.upper = upper + self.value = value + self.default_value = value + + def set_from_configuration(self, device, conf_client): + """ + Configure this control's value from it's gconf entry for the provided device + + Keyword arguments: + device device the control is associated with + conf_client configuration client instance + """ + entry = conf_client.get("/apps/gnome15/%s/%s" % ( device.uid, self.id )) + if entry != None: + if isinstance(self.default_value, int): + self.value = entry.get_int() + elif isinstance(self.default_value, tuple): + rgb = entry.get_string().split(",") + self.value = (int(rgb[0]),int(rgb[1]),int(rgb[2])) + else: + self.value = entry.get_bool() + else: + # Use the default value + self.value = self.default_value + + def zeroize(self): + """ + Set the control to it's OFF value e.g no power to any LED (R, G or B) + for an RGB control + """ + self.value = zeroize(self.default_value) + +class AbstractControlAcquisition(object): + + def __init__(self, driver): + self.driver = driver + self.val = None + self.on_released = None + self.reset_timer = None + self.fade_timer = None + self.on = False + self._released = False + self._waiting = False + self._condition = Event() + + def wait(self): + if self._waiting: + raise Exception("Already waiting") + self._waiting = True + self._condition.wait() + self._waiting = False + + def fade(self, percentage = 100.0, duration = 1.0, release = False, step = 1): + target_val = self.get_target_value(self.val, percentage) + if self.val != target_val: + self.fade_cancelled = False + self._reduce( ( duration / float( self.val - target_val ) ) * step, target_val, release, step) + elif release: + self.driver.release_control(self) + + def get_target_value(self, val, percentage): + return val - int ( ( float(val) / 100.0 ) * percentage ) + + + def blink(self, off_val = 0, delay = 0.5, duration = None, blink_started = None): + if blink_started == None: + blink_started = time.time() + self.cancel_fade() + self.cancel_reset() + if self.on: + self.adjust(self.val) + else: + self.adjust(off_val if isinstance(off_val, int) or isinstance(off_val, tuple) else off_val()) + self.on = not self.on + if duration == None or time.time() < blink_started + duration: + self.reset_timer = g15scheduler.queue(FX_QUEUE, "Blink", delay, self.blink, off_val, delay, duration, blink_started) + return self.reset_timer + + def is_active(self): + raise Exception("Not implemented") + + def adjust(self, val): + raise Exception("Not implemented") + + def set_value(self, val, reset_after = None): + old_val = val + if self.val is None or val != self.val or reset_after is not None: + if self.val is None: + logger.debug("Initial value of control is %s", str(val)) + self.reset_val = val + + self.val = val + self.on = True + self.cancel_fade() + self.adjust(val) + self.cancel_reset() + + logger.debug("Set value of control to %s", str(val)) + + if reset_after: + self.reset_val = old_val + self.reset_timer = g15scheduler.queue(FX_QUEUE, "LEDReset", reset_after, self.reset) + return self.reset_timer + + def reset(self): + self.set_value(self.reset_val) + + def cancel_reset(self): + if self.reset_timer: + self.reset_timer.cancel() + self.reset_timer = None + + def get_value(self): + return self.val + + def cancel_fade(self): + if self.fade_timer is not None: + self.fade_cancelled = True + + def _cleanup(self): + self.cancel_reset() + self.cancel_fade() + + """ + Private + """ + def _reduce(self, interval, target_val, release, step): + if not self.fade_cancelled: + if self.val > target_val: + self.set_value(self.val - step) + if self.val == target_val: + if release: + self.driver.release_control(self) + else: + self.fade_timer = g15scheduler.queue(FX_QUEUE, "Fade", interval, self._reduce, interval, target_val, release, step) + + def _notify_released(self): + if self._released: + raise Exception("Already released") + if self.on_released: + self.on_released() + self._released = True + self._condition.set() + +class ControlAcquisition(AbstractControlAcquisition): + + def __init__(self, driver, control): + self.control = control + AbstractControlAcquisition.__init__(self, driver) + + def is_active(self): + if self.control.id in self.driver.acquired_controls: + ctrls = self.driver.acquired_controls[self.control.id] + return len(ctrls) > 0 and self in ctrls and ctrls.index(self) == len(ctrls) - 1 + + def blink(self, off_val = None, delay = 0.5, duration = None, blink_started = None): + AbstractControlAcquisition.blink(self, ( (0,0,0) if isinstance(self.control.value, tuple) else 0 ) if off_val is None else off_val , delay, duration, blink_started) + + def release(self): + self.driver.release_control(self) + + def adjust(self, val): + if self.is_active(): + self.control.value = val + self.driver.update_control(self.control) + + def fade(self, percentage = 100.0, duration = 1.0, release = False, step = 1): + if isinstance(self.val, int): + AbstractControlAcquisition.fade(self, percentage, duration, release) + else: + self.fade_cancelled = False + target_val = self.get_target_value(self.val, percentage) + h, s, v = self.rgb_to_hsv(self.val) + t_h, t_s, t_v = self.rgb_to_hsv(target_val) + diff = float( v - t_v ) + if diff > 0: + self._reduce( ( duration / diff ) * step, target_val, release, step) + elif release: + self.driver.release_control(self) + + def get_target_value(self, val, percentage): + if isinstance(self.val, int): + return AbstractControlAcquisition.get_target_value(self, val, percentage) + else: + h, s, v = self.rgb_to_hsv(val) + return self.hsv_to_rgb(( h, s, AbstractControlAcquisition.get_target_value(self, v, percentage) )) + + def rgb_to_hsv(self, val): + r, g, b = val + h, s, v = colorsys.rgb_to_hsv(float(r) / 255.0, float(g) / 255.0, float(b) / 255.0) + return ( int(h * 255.0), int(s * 255.0), int(v * 255.0 )) + + def hsv_to_rgb(self, val): + h, s, v = val + r, g, b = colorsys.hsv_to_rgb(float(h) / 255.0, float(s) / 255.0, float(v) / 255.0) + return ( int(r * 255.0), int(g * 255.0), int(b * 255.0 )) + + """ + Private + """ + def _reduce(self, interval, target_val, release, step): + if not self.fade_cancelled: + if isinstance(self.val, int): + AbstractControlAcquisition._reduce(self, interval, target_val, release, step) + else: + if self.val > target_val: + h, s, v = self.rgb_to_hsv(self.val) + v = max(0, v - step) + new_rgb = self.hsv_to_rgb((h, s, v)) + if new_rgb == self.val: + new_rgb = target_val + self.set_value(new_rgb) + if self.val <= target_val: + if release and not self._released: + self.driver.release_control(self) + else: + g15scheduler.queue(FX_QUEUE, "Fade", interval, self._reduce, interval, target_val, release, step) + +class AbstractDriver(object): + + def __init__(self, id): + self.id = id + global seq_no + self.on_driver_options_change = None + seq_no += 1 + self.seq = seq_no + self.disconnecting = False + self.connecting = False + self.all_off_on_disconnect = True + self.allow_multiple = True + self._reset_state() + + def has_memory_bank(self): + for l in self.get_key_layout(): + if G_KEY_M1 in l or G_KEY_M2 in l or G_KEY_M3 in l: + return True + return False + + def release_all_acquisitions(self): + self.acquired_controls = {} + values = dict(self.initial_acquired_control_values) + for k in values: + c = self.get_control(k) + if c: + if k in self.acquired_controls: + self.acquired_controls[k]._cleanup() + c.value = values[k] + self.update_control(c) + + def zeroize_all_controls(self): + for c in self.get_controls(): + c.zeroize() + + def acquire_control(self, control, release_after = None, val = None, on_release = None): + control_acquisitions = self.acquired_controls[control.id] if control.id in self.acquired_controls else [] + self.acquired_controls[control.id] = control_acquisitions + if len(control_acquisitions) == 0: + self.initial_acquired_control_values[control.id] = control.value + + control_acquisition = ControlAcquisition(self, control) + control_acquisition.on_release = on_release + control_acquisitions.append(control_acquisition) + + # Only set the value when active (i.e. in the control_acquisitions list) + control_acquisition.set_value(val if val is not None else control.value) + + if release_after: + g15scheduler.queue(FX_QUEUE, "ReleaseControl", release_after, self._release_control, control_acquisition) + return control_acquisition + + def acquire_control_with_hint(self, hint, release_after = None, val = None): + control = self.get_control_for_hint(hint) + if control: + return self.acquire_control(control, release_after, val) + + def release_control(self, control_acquisition): + logger.info("Releasing %s", control_acquisition.control.id) + if control_acquisition.control.id in self.acquired_controls: + control_acquisitions = self.acquired_controls[control_acquisition.control.id] + control_acquisition._notify_released() + control_acquisition.cancel_reset() + control_acquisition.cancel_fade() + control_acquisitions.remove(control_acquisition) + ctrls = len(control_acquisitions) + if ctrls > 0: + control_acquisition.control.value = control_acquisitions[ctrls - 1].val + self.update_control(control_acquisition.control) + else: + control_acquisition.control.value = self.initial_acquired_control_values[control_acquisition.control.id] + self.update_control(control_acquisition.control) + + def release_mkey_lights(self, control_acquisition): + logger.warning("DEPRECATED call to release_mkey_lights, use release_control") + self.release_control(control_acquisition) + + def disconnect(self): + """ + Disconnect the driver. Callers should use this, and s + subclasses should override on_disconnect. + """ + try: + self.disconnecting = True + logger.info("Disconnecting driver") + if self.all_off_on_disconnect: + for c in self.initial_acquired_control_values: + self.initial_acquired_control_values[c] = zeroize(self.initial_acquired_control_values[c]) + self.release_all_acquisitions() + if self.all_off_on_disconnect: + for c in self.get_controls(): + if isinstance(c.value, int): + c.value = 0 + elif isinstance(c.value, tuple): + c.value = (0, 0, 0) + self.update_control(c) + self._on_disconnect() + finally: + self.disconnecting = False + self._reset_state() + + def _on_disconnect(self): + """ + For subclasses to implemented disconnection logic + """ + raise NotImplementedError( "Not implemented" ) + + def connect(self): + """ + Start the driver + """ + if self.is_connected(): + raise Exception("Already connected") + logger.info("Connecting driver %s", self.get_name()) + self.connecting = True + try: + self._on_connect() + finally: + self.connecting = False + + def _on_connect(self): + raise NotImplementedError( "Not implemented" ) + + def is_connected(self): + """ + Get if driver is connected + """ + raise NotImplementedError( "Not implemented" ) + + def reconnect(self): + """ + Disconnected (if connected), then reconnect + """ + if self.is_connected(): + self.disconnect() + self.connect() + + + def get_name(self): + """ + Get the name of the driver + """ + raise NotImplementedError( "Not implemented" ) + + + def get_model_names(self): + """ + Get a list of the model names this driver supports + """ + raise NotImplementedError( "Not implemented" ) + + + def get_model_name(self): + """ + Get the model name that this driver is connected to + """ + raise NotImplementedError( "Not implemented" ) + + + def get_size(self): + """ + Get the size of the screen. Returns a tuple of (width, height) + """ + raise NotImplementedError( "Not implemented" ) + + def get_key_layout(self): + """ + Get the grid the extra keys available on this keyboard. This is currently only a hint for the Gtk driver + """ + raise NotImplementedError( "Not implemented" ) + + + def get_bpp(self): + """ + Get the bits per pixel. 1 would be monochrome + """ + raise NotImplementedError( "Not implemented") + + + def get_controls(self): + """ + Get the all of the controls available. This would include things such as LCD contrast, LCD brightness, + keyboard colour, keyboard backlight etc + """ + raise NotImplementedError( "Not implemented") + + + def paint(self, image): + """ + Repaint the screen. + """ + raise NotImplementedError( "Not implemented" ) + + + def update_control(self, control): + """ + Synchronize a control with the keyboard. For example, if the control was for the + keyboard colour, the keyboard colour would actually change when this function + is invoked + + Subclasses should not override this function, instead they should implement + on_update_control() + + Keyword arguments: + control -- control to update + """ + if self.check_control(control): + for l in self.control_update_listeners: + l.control_updated(control) + self.on_update_control(control) + + def check_control(self, control): + if isinstance(control.value, int): + if control.value > control.upper: + control.value = control.upper + elif control.value < control.lower: + control.value = control.lower + return True + + def on_update_control(self, control): + raise NotImplementedError( "Not implemented" ) + + + + def grab_keyboard(self, callback): + """ + Start receiving events when the additional keys (G keys, L keys and M keys) + are pressed and released. The provided callback will be invoked with two + arguments, the first being the key code (see the constants G_KEY_xx) + and the second being the key state (KEY_STATE_DOWN or KEY_STATE_UP). + + Keyword arguments: + callback -- invoked when keys are pressed + """ + raise NotImplementedError( "Not implemented" ) + + + def process_svg(self, document): + """ + Give the driver a chance to alter a theme's SVG. This has been introduced to work + around a problem of Inkscape (the recommended 'IDE' for designing themes), + does not saving bitmap font names + """ + raise NotImplementedError( "Not implemented" ) + + def get_mkey_lights(self): + return self.lights + + def get_control(self, control_id): + controls = self.get_controls() + if controls: + for control in self.get_controls(): + if control_id == control.id: + return control + + def get_control_for_hint(self, hint): + controls = self.get_controls() + if controls: + for control in self.get_controls(): + if ( hint & control.hint ) == hint: + return control + + def update_controls(self): + for control in self.get_controls(): + if control.hint & HINT_VIRTUAL == 0: + self.update_control(control) + + def get_color_as_ratios(self, hint, default): + fg_control = self.get_control_for_hint(hint) + fg_rgb = default + if fg_control != None: + fg_rgb = fg_control.value + return ( float(fg_rgb[0]) / 255.0,float(fg_rgb[1]) / 255.0,float(fg_rgb[2]) / 255.0 ) + + def get_color_as_hexrgb(self, hint, default): + fg_control = self.get_control_for_hint(hint) + fg_rgb = default + if fg_control != None: + fg_rgb = fg_control.value + return rgb_to_hex(fg_rgb) + + def get_color(self, hint, default): + fg_control = self.get_control_for_hint(hint) + fg_rgb = default + if fg_control != None: + fg_rgb = fg_control.value + return fg_rgb + + """ + Private + """ + + def _release_control(self, control_acquisition): + if control_acquisition.is_active(): + self.release_control(control_acquisition) + + def _reset_state(self): + self.lights = 0 + self.control_update_listeners = [] + self.acquired_controls = {} + self.initial_acquired_control_values = {} + +def rgb_to_hex(rgb): + return '#%02x%02x%02x' % rgb diff --git a/src/gnome15/g15drivermanager.py b/src/gnome15/g15drivermanager.py new file mode 100644 index 0000000..7a08bd0 --- /dev/null +++ b/src/gnome15/g15drivermanager.py @@ -0,0 +1,118 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2010 Brett Smith +# +# 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 . + +''' +This module is responsible for loading the available hardware drivers, and +choosing the best one to use. + +The best driver is always used when no driver is yet configured, other the +configured driver is always used if possible. If it is not available, +or no longer supports the model for the associated device, then it will revert +back to using the best driver. + +The "best driver" is simply the first driver that supports the associated +device. +''' + +import os +import logging +logger = logging.getLogger(__name__) + +# Find all drivers +drivers_dir = os.path.realpath(os.path.join(os.path.dirname(__file__), "drivers")) +logger.info("Loading drivers from %s", drivers_dir) +imported_drivers = {} + +driverfiles = [fname[:-3] for fname in os.listdir(drivers_dir) if fname.endswith(".py") and fname.startswith("driver_")] +for d in driverfiles: + try : + driver_mod = __import__("gnome15.drivers.%s" % d , fromlist=[]) + mod = getattr(getattr(driver_mod, "drivers"), d) + imported_drivers[d] = mod + except Exception as e: + logger.warning("Failed to load driver.", exc_info = e) + + +def get_driver_mod(driver_id): + ''' + Get a driver module given it's ID. + + Keyword arguments: + driver_id -- driver ID + ''' + for driver_mod in imported_drivers.values(): + if driver_mod.id == driver_id: + return driver_mod + +def get_driver(conf_client, device, on_close = None): + ''' + Called by clients to create the configured driver. If the configured + driver is not available, it will fallback to using the "best driver". + + Keyword arguments: + conf_client -- configuration client + device -- device to find driver for + on_close -- callback passed to driver that is executed when the driver closes. + ''' + driver_name = conf_client.get_string("/apps/gnome15/%s/driver" % device.uid) + if not driver_name: + # If no driver has yet been configured, always use the best driver + driver = _get_best_driver(device, on_close) + if driver == None: + raise Exception(_("No drivers support the model %s") % device.model_id) + + logger.info("Using first available driver for %s, %s", device.model_id, driver.get_name()) + return driver + + driver_mod_key = "driver_" + driver_name + if not driver_mod_key in imported_drivers: + # If the previous driver is no longer installed, get the best remaining driver + driver = _get_best_driver(device, on_close) + if driver == None: + raise Exception(_("Driver %s is not available. Do you have to appropriate package installed?") % driver_name) + else: + logger.info("Configured driver %s is not available, using %s instead", + driver_mod_key, + driver.get_name()) + else: + driver = imported_drivers[driver_mod_key].Driver(device, on_close = on_close) + + if not device.model_id in driver.get_model_names(): + # If the configured driver is now incorrect for the device model, just use the best driver + # If no driver has yet been configured, always use the best driver + driver = _get_best_driver(device, on_close) + if driver == None: + raise Exception(_("No drivers support the model %s") % device.model_id) + logger.warning("Ignoring configured driver %s, as the model is not supported by it." \ + "Looking for best driver", driver) + return driver + else: + # Configured driver is OK to use + return driver + +def _get_best_driver(device, on_close = None): + ''' + Get the "best driver" available. This will be the first driver that + supports the provided device. + + Keyword arguments: + device -- device to find driver for + on_close -- callback passed to driver that is executed when the driver closes. + ''' + for driver_mod_key in imported_drivers: + driver = imported_drivers[driver_mod_key].Driver(device, on_close = on_close) + if device.model_id in driver.get_model_names(): + return driver \ No newline at end of file diff --git a/src/gnome15/g15exceptions.py b/src/gnome15/g15exceptions.py new file mode 100644 index 0000000..e9b19e1 --- /dev/null +++ b/src/gnome15/g15exceptions.py @@ -0,0 +1,27 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2010 Brett Smith +# +# 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 . + +import gnome15.g15locale as g15locale +_ = g15locale.get_translation("gnome15").ugettext + +class NotConnectedException(Exception): + def __init__(self, message = _("Failed to connect.")): + Exception.__init__(self, message) + +class RetryException(Exception): + def __init__(self, message = _("Retry.")): + Exception.__init__(self, message) + \ No newline at end of file diff --git a/src/gnome15/g15globals.py.in b/src/gnome15/g15globals.py.in new file mode 100644 index 0000000..b0d077b --- /dev/null +++ b/src/gnome15/g15globals.py.in @@ -0,0 +1,53 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2010 Brett Smith +# +# 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 . + +import os +import xdg.BaseDirectory + +name = "@PACKAGE_NAME@" +version = "@PACKAGE_VERSION@" + +package_dir = os.path.abspath(os.path.dirname(__file__)) +image_dir = os.path.join(package_dir, "..", "..", "data", "images") +dev = False +if os.path.exists(image_dir): + dev = True + ui_dir = os.path.realpath(os.path.join(package_dir, "..", "..", "data", "ui")) + font_dir = os.path.realpath(os.path.join(package_dir, "..", "..", "data", "fonts")) + icons_dir = os.path.realpath(os.path.join(package_dir, "..", "..", "data", "icons")) + ukeys_dir = os.path.realpath(os.path.join(package_dir, "..", "..", "data", "ukeys")) + plugin_dir = os.path.realpath(os.path.join(package_dir, "..", "plugins")) + scripts_dir = os.path.realpath(os.path.join(package_dir, "..", "scripts")) + themes_dir = os.path.realpath(os.path.join(package_dir, "..", "..", "data", "themes")) + i18n_dir = os.path.realpath(os.path.join(package_dir, "..", "i18n")) +else: + image_dir = "@prefix@/share/gnome15/images" + ui_dir = "@prefix@/share/gnome15/ui" + font_dir = "@prefix@/share/gnome15" + plugin_dir = "@prefix@/share/gnome15/plugins" + themes_dir = "@prefix@/share/gnome15/themes" + ukeys_dir = "@prefix@/share/gnome15/ukeys" + i18n_dir = "@prefix@/share/gnome15/i18n" + icons_dir = "@prefix@/share/icons" + scripts_dir = "@prefix@/bin" + +user_config_dir = os.path.join(xdg.BaseDirectory.xdg_config_home, "gnome15") +user_data_dir = os.path.join(xdg.BaseDirectory.xdg_data_home, "gnome15") +user_cache_dir = os.path.join(xdg.BaseDirectory.xdg_cache_home, "gnome15") + +# Differs from distro to distro, so it can changed as a ./configure option +# by setting the FIXED_SIZE_FONT environment variable. +fixed_size_font_name = "@FIXED_SIZE_FONT@" \ No newline at end of file diff --git a/src/gnome15/g15gtk.py b/src/gnome15/g15gtk.py new file mode 100644 index 0000000..beb8b33 --- /dev/null +++ b/src/gnome15/g15gtk.py @@ -0,0 +1,287 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2010 Brett Smith +# +# 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 . + +''' +A top level GTK windows that draws on the LCD +''' +import gtk +import gobject +import g15driver +import util.g15cairo as g15cairo +import util.g15pythonlang as g15pythonlang +from threading import Lock +from threading import Semaphore +import g15theme +import g15screen +import cairo +import ctypes + + +_initialized = False +def create_cairo_font_face_for_file (filename, faceindex=0, loadoptions=0): + global _initialized + global _freetype_so + global _cairo_so + global _ft_lib + global _surface + + CAIRO_STATUS_SUCCESS = 0 + FT_Err_Ok = 0 + + if not _initialized: + + # find shared objects + _freetype_so = ctypes.CDLL ("libfreetype.so.6") + _cairo_so = ctypes.CDLL ("libcairo.so.2") + + _cairo_so.cairo_ft_font_face_create_for_ft_face.restype = ctypes.c_void_p + _cairo_so.cairo_ft_font_face_create_for_ft_face.argtypes = [ ctypes.c_void_p, ctypes.c_int ] + _cairo_so.cairo_set_font_face.argtypes = [ ctypes.c_void_p, ctypes.c_void_p ] + _cairo_so.cairo_font_face_status.argtypes = [ ctypes.c_void_p ] + _cairo_so.cairo_status.argtypes = [ ctypes.c_void_p ] + + # initialize freetype + _ft_lib = ctypes.c_void_p () + if FT_Err_Ok != _freetype_so.FT_Init_FreeType (ctypes.byref (_ft_lib)): + raise "Error initialising FreeType library." + + class PycairoContext(ctypes.Structure): + _fields_ = [("PyObject_HEAD", ctypes.c_byte * object.__basicsize__), + ("ctx", ctypes.c_void_p), + ("base", ctypes.c_void_p)] + + _surface = cairo.ImageSurface (cairo.FORMAT_A8, 0, 0) + + _initialized = True + + # create freetype face + ft_face = ctypes.c_void_p() + cairo_ctx = cairo.Context (_surface) + cairo_t = PycairoContext.from_address(id(cairo_ctx)).ctx + + if FT_Err_Ok != _freetype_so.FT_New_Face (_ft_lib, filename, faceindex, ctypes.byref(ft_face)): + raise Exception("Error creating FreeType font face for " + filename) + + # create cairo font face for freetype face + cr_face = _cairo_so.cairo_ft_font_face_create_for_ft_face (ft_face, loadoptions) + if CAIRO_STATUS_SUCCESS != _cairo_so.cairo_font_face_status (cr_face): + raise Exception("Error creating cairo font face for " + filename) + + _cairo_so.cairo_set_font_face (cairo_t, cr_face) + if CAIRO_STATUS_SUCCESS != _cairo_so.cairo_status (cairo_t): + raise Exception("Error creating cairo font face for " + filename) + + face = cairo_ctx.get_font_face () + + return face + + +class G15OffscreenWindow(g15theme.Component): + + def __init__(self, component_id): + g15theme.Component.__init__(self, component_id) + self.window = None + self.content = None + + def on_configure(self): + g15theme.Component.on_configure(self) + gobject.idle_add(self._create_window) + self.get_screen().key_handler.action_listeners.append(self) + + def notify_remove(self): + g15theme.Component.notify_remove(self) + self.get_screen().key_handler.action_listeners.remove(self) + + def set_content(self, content): + self.content = content + if self.window is not None: + gobject.idle_add(self._do_set_content) + + def action_performed(self, binding): + if self.is_visible(): + if binding.action == g15driver.NEXT_SELECTION: + gobject.idle_add(self.window.focus_next) + elif binding.action == g15driver.PREVIOUS_SELECTION: + gobject.idle_add(self.window.focus_previous) + if binding.action == g15driver.NEXT_PAGE: + gobject.idle_add(self.window.change_widget) + elif binding.action == g15driver.PREVIOUS_PAGE: + gobject.idle_add(self.window.change_widget, None, True) + elif binding.action == g15driver.SELECT: + pass + + def paint(self, canvas): + g15theme.Component.paint(self, canvas) + if self.window is not None: + self.window.paint(canvas) + + + """ + Private + """ + + def _do_set_content(self): + self.window.set_content(self.content) + + def _create_window(self): + screen = self.get_screen() + window = G15Window(screen, self.get_root(), self.view_bounds[0], self.view_bounds[1], \ + self.view_bounds[2], self.view_bounds[3]) + if self.content is not None: + self.window.set_content(self.content) + self.window = window + screen.redraw(self.get_root()) + +class G15Window(gtk.OffscreenWindow): + + def __init__(self, screen, page, area_x, area_y, area_width, area_height): + gtk.OffscreenWindow.__init__(self) + self.pixbuf = None + self.scroller = None + self.screen = screen + self.page = page + self.lock = None + self.area_x = int(area_x) + self.area_y = int(area_y) + self.area_width = int(area_width) + self.area_height = int(area_height) + self.surface = None + self.content = gtk.EventBox() + self.set_app_paintable(True) + self.content.set_app_paintable(True) + self.connect("screen-changed", self.screen_changed) + self.content.connect("expose-event", self._transparent_expose) + self.content.set_size_request(self.area_width, self.area_height) + self.add(self.content) + self.connect("damage_event", self._damage) + self.connect("expose_event", self._expose) + self.screen_changed(None, None) + self.lock = Semaphore() + + def set_content(self, content): + self.content.add(content) + self.show_all() + + # If the content window is a scroller, we send focus events to it + # moving the scroller position to the focussed component + if isinstance(content, gtk.ScrolledWindow): + self.scroller = content + + def paint(self, canvas): + if g15pythonlang.is_gobject_thread(): + raise Exception("Painting on mainloop") + self.start_for_capture() + gobject.idle_add(self._do_capture) + self.lock.acquire() + canvas.save() + canvas.translate(self.area_x, self.area_y) + canvas.set_source_surface(self.surface) + canvas.paint() + canvas.restore() + + def focus_next(self): + self.content.get_toplevel().child_focus(gtk.DIR_TAB_FORWARD) + self.scroll_to_focussed() + self.screen.redraw(self.page) + + def focus_previous(self): + self.content.get_toplevel().child_focus(gtk.DIR_TAB_BACKWARD) + self.screen.redraw(self.page) + self.scroll_to_focussed() + + def change_widget(self, amount = None, reverse = False): + focussed = self.get_focus() + if focussed != None: + if isinstance(focussed, gtk.HScale): + adj = focussed.get_adjustment() + ps = adj.get_page_size() if amount is None else amount + if ps == 0: + ps = 10 + if reverse: + adj.set_value(adj.get_value() - ps) + else: + adj.set_value(adj.get_value() + ps) + self.screen.redraw(self.page) + + def show_all(self): + gtk.OffscreenWindow.show_all(self) + + def scroll_to_focussed(self): + if self.scroller is not None: + hadj = self.scroller.get_hadjustment() + vadj = self.scroller.get_vadjustment() + x, y = self.get_focus().translate_coordinates(self.scroller.get_children()[0], 0, 0) + max_x = hadj.upper - hadj.page_size + max_y = vadj.upper - vadj.page_size + hadj.set_value(min(x, max_x)) + vadj.set_value(min(y, max_y)) + + def screen_changed(self, widget, old_screen=None): + global supports_alpha + + # To check if the display supports alpha channels, get the colormap + screen = self.get_screen() + colormap = screen.get_rgba_colormap() + if colormap == None: + colormap = screen.get_rgb_colormap() + supports_alpha = False + else: + supports_alpha = True + + # Now we have a colormap appropriate for the screen, use it + self.set_colormap(colormap) + + return False + + def _transparent_expose(self, widget, event = None): + """ + To overcome inability to set a container component as transparent. I + cannot get compositing working (perhaps it just doesn't because we are + going to an offscreen window). So, to get pseudo-transparency, + we repaint the background the screen would normally paint, offset by + the position of this component + """ + cr = widget.window.cairo_create() + self.screen.clear_canvas(cr) + cr.save() + cr.translate(-self.area_x, -self.area_y) + for s in self.screen.painters: + if s.place == g15screen.BACKGROUND_PAINTER: + s.paint(cr) + cr.restore() + return False + + def _expose(self, widget, event): + self.screen.redraw(self.page) + return False + + def _damage(self, widget, event): +# print "Damage" +# self.screen.redraw(self.page) + return False + + def start_for_capture(self): + self.lock = Lock() + self.lock.acquire() + + def _do_capture(self): + self.content.window.invalidate_rect((0,0,self.area_width,self.area_height), True) + self.content.window.process_updates(True) + pixbuf = gtk.gdk.Pixbuf( gtk.gdk.COLORSPACE_RGB, False, 8, self.area_width, self.area_height) + pixbuf.get_from_drawable(self.content.window, self.content.get_colormap(), 0, 0, 0, 0, self.area_width, self.area_height) + self.surface = g15cairo.pixbuf_to_surface(pixbuf) + self.pixbuf = pixbuf + self.lock.release() \ No newline at end of file diff --git a/src/gnome15/g15keyboard.py b/src/gnome15/g15keyboard.py new file mode 100644 index 0000000..78974a6 --- /dev/null +++ b/src/gnome15/g15keyboard.py @@ -0,0 +1,661 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2011 Brett Smith +# +# 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 . + +""" +This module deals with handling raw key presses from the driver, and turning them +into Macros or actions. The different types of macro are handled accordingly, as well +as the repetition functions. + +All key events are handled on a queue (one per instance of a key handler). + +""" + +import gnome15.g15locale as g15locale +_ = g15locale.get_translation("gnome15").ugettext + +import g15profile +import util.g15scheduler as g15scheduler +import g15driver +import g15actions +import g15uinput +import g15screen + +import logging +logger = logging.getLogger(__name__) + +class KeyState(): + """ + Holds the current state of a single macro key + """ + def __init__(self, key): + self.key = key + self.state_id = None + self.timer = None + self.consumed = False + self.defeat_release = False + self.consume_until_release = False + + def is_consumed(self): + return self.consumed or self.consume_until_release + + def cancel_timer(self): + """ + Cancel the HELD timer if one exists. + """ + if self.timer is not None: + self.timer.cancel() + self.timer = None + + def __repr__(self): + return "%s = %s [consumed = %s]" % (self.key, g15profile.to_key_state_name(self.state_id), str(self.consumed) ) + +class G15KeyHandler(): + """ + Main class for handling key events. There should be one instance of this + per active G15Screen. + """ + + def __init__(self, screen): + # Public + self.queue_name = "MacroQueue-%s" % screen.device.uid + + """ + List of callbacks invoked when an action is activated by it's key combination + """ + self.action_listeners = [] + + """ + List of callbacks invoked for raw key handling. Normally plugins shouldn't + use this, use actions instead + """ + self.key_handlers = [] + + # Private + self.__screen = screen + self.__conf_client = self.__screen.conf_client + self.__repeat_macros = [] + self.__macro_repeat_timer = None + self.__uinput_macros = [] + self.__normal_macros = [] + self.__normal_held_macros = [] + self.__notify_handles = [] + self.__key_states = {} + + def get_key_states(self): + # Get the current state of the keys + return self.__key_states + + def start(self): + """ + Start handling keys + """ + screen_key = "/apps/gnome15/%s" % self.__screen.device.uid + logger.info("Starting %s's key handler.", self.__screen.device.uid) + g15profile.profile_listeners.append(self._profile_changed) + self.__screen.screen_change_listeners.append(self) + self.__notify_handles.append(self.__conf_client.notify_add("%s/active_profile" % screen_key, self._active_profile_changed)) + logger.info("Starting of %s's key handler is complete.", self.__screen.device.uid) + self._reload_active_macros() + + def stop(self): + """ + Stop handling keys + """ + logger.info("Stopping key handler for %s", self.__screen.device.uid) + g15scheduler.stop_queue(self.queue_name) + if self in self.__screen.screen_change_listeners: + self.__screen.screen_change_listeners.remove(self) + if self._profile_changed in g15profile.profile_listeners: + g15profile.profile_listeners.remove(self._profile_changed) + for h in self.__notify_handles: + self.__conf_client.notify_remove(h) + self.__notify_handles = [] + logger.info("Stopped key handler for %s", self.__screen.device.uid) + + def key_received(self, keys, state_id): + """ + This function starts processing of the provided keys, turning them + into macros, actions and handling repetition. The key event will be + placed on the queue, leaving this function to return immediately + + Keyword arguments: + keys -- list of keys to process + state_id -- key state ID (g15driver.KEY_STATE_UP, _DOWN and _HELD) + """ + g15scheduler.execute(self.queue_name, "KeyReceived", self._do_key_received, keys, state_id) + + def memory_bank_changed(self, bank): + self._reload_active_macros() + + """ + Callbacks + """ + + def _active_profile_changed(self, client, connection_id, entry, args): + self._reload_active_macros() + return 1 + + def _profile_changed(self, profile_id, device_uid): + self._reload_active_macros() + + """ + Private + """ + + def _reload_active_macros(self): + self.__normal_held_macros = [] + self.__normal_macros = [] + self.__uinput_macros = [] + self._build_macros() + + def _do_key_received(self, keys, state_id): + """ + Actual handling of key events. + + Keyword arguments: + keys -- list of keys + state_id -- key state (g15driver.KEY_STATE_UP, _DOWN and _HELD) + """ + + """ + See if the screen itself, or the plugins, want to handle the key. This + is the legacy method of key handling, the preferred method now is + actions which is handled below. However, this is still useful for plugins + that want to take over key handling, such as screensaver which + disables all keys while it is active. + """ + try: + if self._handle_key(keys, state_id, post=False): + return + + """ + Deal with each key separately, this keeps it simpler + """ + for key in keys: + + """ + Now set up the macro key state. This is where we decide what macros + and actions to activate. + """ + if self._configure_key_state(key, state_id): + + """ + Do uinput macros first. These are treated slightly differently, because + a press of the Macro key equals a "press" of the virtual key, + a release of the Macro key equals a "release" of the virtual key etc. + """ + self._handle_uinput_macros() + + """ + Now the ordinary macros, processed on key_up + """ + self._handle_normal_macros() + + """ + Now the actions + """ + self._handle_actions() + + """ + Now do the legacy 'post' handling. + """ + if not self._handle_key(keys, state_id, post=True): + pass + + """ + When ALL keys are UP, clear out the state + """ + up = 0 + for k, v in self.__key_states.items(): + if v.state_id == g15driver.KEY_STATE_UP: + up += 1 + if up > 0 and up == len(self.__key_states): + self.__key_states = {} + finally: + """ + Always redraw the current page on key presses + """ + self.__screen.redraw() + + def _handle_actions(self): + """ + This handles the default action bindings. The actions may have + already re-mapped as a macro, in which case they will be ignored + here. + """ + action_keys = self.__screen.driver.get_action_keys() + if action_keys: + for action in action_keys: + binding = action_keys[action] + f = 0 + for k in binding.keys: + if k in self.__key_states and \ + binding.state == self.__key_states[k].state_id and \ + not self.__key_states[k].is_consumed(): + f += 1 + if f == len(binding.keys): + self._action_performed(binding) + for k in binding.keys: + self.__key_states[k].consume_until_release = True + + def _handle_normal_macros(self): + """ + First check for any KEY_STATE_HELD macros. We do these first so KEY_STATE_UP + macros don't consume the key states + """ + for m in self.__normal_held_macros: + held = [] + for k in m.keys: + if k in self.__key_states: + key_state = self.__key_states[k] + if not key_state.is_consumed() and key_state.state_id == g15driver.KEY_STATE_HELD: + held.append(key_state) + + if len(held) == len(m.keys): + self._handle_macro(m, g15driver.KEY_STATE_HELD, held) + + + """ + Search for all the non-uinput macros that would be activated by the + current key state. In this case, KEY_STATE_UP macros are looked for + """ + for m in self.__normal_macros: + up = [] + held = [] + down = [] + for k in m.keys: + if k in self.__key_states: + key_state = self.__key_states[k] + if not key_state.is_consumed() and key_state.state_id == g15driver.KEY_STATE_DOWN: + down.append(key_state) + if not key_state.is_consumed() and key_state.state_id == g15driver.KEY_STATE_UP and not key_state.defeat_release: + up.append(key_state) + if not key_state.is_consumed() and key_state.state_id == g15driver.KEY_STATE_HELD: + held.append(key_state) + + if len(up) == len(m.keys): + self._handle_macro(m, g15driver.KEY_STATE_UP, up) + if len(down) == len(m.keys): + self._handle_macro(m, g15driver.KEY_STATE_DOWN, down) + if len(held) == len(m.keys): + self._handle_macro(m, g15driver.KEY_STATE_HELD, held) + + + def _handle_uinput_macros(self): + """ + Search for all the uinput macros that would be activated by the + current key state, and emit events of the same type. + """ + uinput_repeat = False + for m in self.__uinput_macros: + down = [] + up = [] + held = [] + for k in m.keys: + if k in self.__key_states: + key_state = self.__key_states[k] + if not key_state.is_consumed(): + if key_state.state_id == g15driver.KEY_STATE_UP and not key_state.defeat_release: + up.append(key_state) + if key_state.state_id == g15driver.KEY_STATE_DOWN: + down.append(key_state) + if key_state.state_id == g15driver.KEY_STATE_HELD: + held.append(key_state) + + if len(down) == len(m.keys): + self._handle_uinput_macro(m, g15driver.KEY_STATE_DOWN, down) + if len(up) == len(m.keys): + self._handle_uinput_macro(m, g15driver.KEY_STATE_UP, up) + if len(held) == len(m.keys): + self._handle_uinput_macro(m, g15driver.KEY_STATE_HELD, held) + uinput_repeat = True + + """ + Simulate a uinput repeat by just handling an empty key list. + No keys have changed state, so we should just keep hitting this + reschedule until they do + """ + if uinput_repeat: + g15scheduler.queue(self.queue_name, "RepeatUinput", \ + 0.1, \ + self._handle_uinput_macros) + + def _configure_key_state(self, key, state_id): + + """ + Maintains the "key state" table, which holds what state each key + is currently in. + + This function will return the number of state changes, so this key + event may be ignored if it is no longer appropriate (i.e. a hold + timer event for keys that are now released) + + Keyword arguments: + key -- single key + state_id -- state_id (g15driver.KEY_STATE_UP, _DOWN or _HELD) + """ + if state_id == g15driver.KEY_STATE_HELD and not key in self.__key_states: + """ + All keys were released before the HOLD timer kicked in, so we + totally ignore this key + """ + pass + else: + if not key in self.__key_states: + self.__key_states[key] = KeyState(key) + key_state = self.__key_states[key] + + # This is a new key press, so reset this key's consumed state + key_state.consumed = False + + # Check the sanity of the key press + self._check_key_state(state_id, key_state) + key_state.state_id = state_id + + if state_id == g15driver.KEY_STATE_DOWN: + """ + Key is now down, let's set up a timer to produce a held event + """ + key_state.timer = g15scheduler.queue(self.queue_name, + "HoldKey%s" % str(key), \ + self.__screen.service.key_hold_duration, \ + self._do_key_received, [ key ], \ + g15driver.KEY_STATE_HELD) + elif state_id == g15driver.KEY_STATE_UP: + """ + Now the key is up, cancel the HELD timer if one exists. + """ + key_state.cancel_timer() + + return True + + def _get_all_macros(self, profile = None, macro_list = None, macro_keys = None, mapped_to_key = False, state = None): + """ + Get all macros, including those in parent profiles. By default, the + "root" is the active profile + + Keyword argumentsL + profile -- root profile or None for active profile + macro_list -- list to append macros to. + mapped_to_key -- boolean indicator whether to only find UINPUT type macros + """ + if profile is None: + profile = g15profile.get_active_profile(self.__screen.device) + if macro_list is None: + macro_list = [] + if macro_keys is None: + macro_keys = [] + + if state == None: + state = g15driver.KEY_STATE_UP + + bank = self.__screen.get_memory_bank() + for m in profile.macros[state][bank - 1]: + if not m.key_list_key in macro_keys: + if ( not mapped_to_key and not m.is_uinput() ) or \ + ( mapped_to_key and m.is_uinput() ): + macro_list.append(m) + macro_keys.append(m.key_list_key) + if profile.base_profile is not None: + profile = g15profile.get_profile(self.__screen.device, profile.base_profile) + if profile is not None: + self._get_all_macros(profile, macro_list, macro_keys, mapped_to_key, state) + return macro_list + + def _build_macros(self, profile = None, macro_keys = None, held_macro_keys = None, down_macro_keys = None): + if profile is None: + profile = g15profile.get_active_profile(self.__screen.device) + if macro_keys is None: + macro_keys = [] + if held_macro_keys is None: + held_macro_keys = [] + if down_macro_keys is None: + down_macro_keys = [] + + bank = self.__screen.get_memory_bank() + for m in profile.macros[g15driver.KEY_STATE_UP][bank - 1]: + if not m.key_list_key in macro_keys: + if m.is_uinput(): + self.__uinput_macros.append(m) + else: + self.__normal_macros.append(m) + macro_keys.append(m.key_list_key) + + for m in profile.macros[g15driver.KEY_STATE_DOWN][bank - 1]: + if not m.key_list_key in down_macro_keys: + if m.is_uinput(): + self.__uinput_macros.append(m) + else: + self.__normal_macros.append(m) + down_macro_keys.append(m.key_list_key) + + for m in profile.macros[g15driver.KEY_STATE_HELD][bank - 1]: + if not m.key_list_key in held_macro_keys: + if not m.is_uinput(): + self.__normal_held_macros.append(m) + held_macro_keys.append(m.key_list_key) + + if profile.base_profile is not None: + profile = g15profile.get_profile(self.__screen.device, profile.base_profile) + if profile is not None: + self._build_macros(profile, macro_keys, held_macro_keys, down_macro_keys) + + def _check_key_state(self, new_state_id, key_state): + """ + Sanity check + + Keyword arguments: + new_state_id -- new state ID + key_state -- key state object + """ + if new_state_id == g15driver.KEY_STATE_UP and \ + key_state.state_id not in [ g15driver.KEY_STATE_DOWN, g15driver.KEY_STATE_HELD ]: + logger.warning("Received key up state before receiving key down, indicates defeated key press.") + return False +# if new_state_id == g15driver.KEY_STATE_DOWN and \ +# key_state.state_id is not None: +# logger.warning("Received unexpected key down (key was in state %s).", +# g15profile.to_key_state_name(key_state.state_id)) +# return False + if new_state_id == g15driver.KEY_STATE_HELD and \ + key_state.state_id in [ g15driver.KEY_STATE_UP, None ]: + logger.warning("Received key held state before receiving key down.") + return False + + return True + + def _send_uinput_keypress(self, macro, uc, uinput_repeat = False): + g15uinput.locks[macro.type].acquire() + try: + if uinput_repeat: + g15uinput.emit(macro.type, uc, 2) + else: + g15uinput.emit(macro.type, uc, 1) + g15uinput.emit(macro.type, uc, 0) + finally: + g15uinput.locks[macro.type].release() + + def _repeat_uinput(self, macro, uc, uinput_repeat = False): + if macro in self.__repeat_macros: + self._send_uinput_keypress(macro, uc, uinput_repeat) + if macro in self.__repeat_macros: + self.__macro_repeat_timer = g15scheduler.queue(self.queue_name, "MacroRepeat", macro.repeat_delay, self._repeat_uinput, self._reload_macro_instance(macro), uc, uinput_repeat) + + def _reload_macro_instance(self, macro): + p = g15profile.get_profile(macro.profile.device, macro.profile.id) + if p: + return p.get_macro(macro.activate_on, macro.memory, macro.keys) + logger.warning("Could not reload macro %s, using old instance.", macro.name) + return macro + + def _handle_uinput_macro(self, macro, state, key_states): + uc = macro.get_uinput_code() + self._consume_keys(key_states) + if state == g15driver.KEY_STATE_UP: + if macro in self.__repeat_macros and macro.repeat_mode == g15profile.REPEAT_WHILE_HELD: + self.__repeat_macros.remove(macro) + g15uinput.emit(macro.type, uc, 0) + elif state == g15driver.KEY_STATE_DOWN: + if macro in self.__repeat_macros: + if macro.repeat_mode == g15profile.REPEAT_TOGGLE and macro.repeat_delay != -1: + """ + For REPEAT_TOGGLE mode with custom repeat rate, we now cancel + the repeat timer and defeat the key release. + """ + self.__repeat_macros.remove(macro) + else: + """ + For REPEAT_TOGGLE mode with default repeat rate, we will send a release if this + is the second press. We also defeat the 2nd release. + """ + g15uinput.emit(macro.type, uc, 0) + self.__repeat_macros.remove(macro) + self._defeat_release(key_states) + else: + if macro.repeat_mode == g15profile.REPEAT_TOGGLE: + """ + Start repeating + """ + if not macro in self.__repeat_macros: + self._defeat_release(key_states) + self.__repeat_macros.append(macro) + if macro.repeat_delay != -1: + """ + For the default delay, just let the OS handle the repeat + """ + self._repeat_uinput(macro, uc) + else: + """ + For the custom delay, send the key press now. We send + the first when it is actually released, then start + sending further repeats on a timer + """ + g15uinput.emit(macro.type, uc, 1) + self._defeat_release(key_states) + elif macro.repeat_mode == g15profile.NO_REPEAT: + """ + For NO_REPEAT macros we send the release now, and defeat the + actual key release that will come later. + """ + self._send_uinput_keypress(macro, uc) + elif macro.repeat_mode == g15profile.REPEAT_WHILE_HELD and macro.repeat_delay != -1: + self._send_uinput_keypress(macro, uc) + else: + g15uinput.emit(macro.type, uc, 1) + + elif state == g15driver.KEY_STATE_HELD: + if macro.repeat_mode == g15profile.REPEAT_WHILE_HELD: + if macro.repeat_delay != -1: + self.__repeat_macros.append(macro) + self._repeat_uinput(macro, uc, False) + + def _defeat_release(self, key_states): + for k in key_states: + k.defeat_release = True + k.cancel_timer() + + def _consume_keys(self, key_states): + """ + Mark as consumed so they don't get activated again if other key's are + pressed or released while this macro is active + + Keyword arguments: + key_states -- list of KeyState objects to mark as consumed + """ + for k in key_states: + k.consumed = True + + def _process_macro(self, macro, state, key_states): + if macro.type == g15profile.MACRO_ACTION: + binding = g15actions.ActionBinding(macro.macro, macro.keys, state) + if not self._action_performed(binding): + # Send it to the service for handling + self.__screen.service.macro_handler.handle_macro(macro) + else: + for k in key_states: + k.consume_until_released = True + else: + # Send it to the service for handling + self.__screen.service.macro_handler.handle_macro(macro) + + def _handle_macro(self, macro, state, key_states, repetition = False): + self._consume_keys(key_states) + delay = macro.repeat_delay if macro.repeat_delay != -1 else 0.1 + if macro.repeat_mode == g15profile.REPEAT_TOGGLE and state == g15driver.KEY_STATE_UP: + if macro in self.__repeat_macros and not repetition: + # Key pressed again, so stop repeating + self.__cancel_macro_repeat_timer() + self.__repeat_macros.remove(macro) + else: + if not macro in self.__repeat_macros and not repetition: + self.__repeat_macros.append(macro) + else: + self._process_macro(macro, state, key_states) + + # We test again because a toggle might have stopped the repeat + if macro in self.__repeat_macros: + self.__macro_repeat_timer = g15scheduler.queue(self.queue_name, "RepeatMacro", delay, self._handle_macro, macro, state, key_states, True) + elif macro.repeat_mode == g15profile.REPEAT_WHILE_HELD and not state == g15driver.KEY_STATE_DOWN: + if state == g15driver.KEY_STATE_UP and macro in self.__repeat_macros and not repetition: + # Key released again, so stop repeating + self.__cancel_macro_repeat_timer() + self.__repeat_macros.remove(macro) + else: + if state == g15driver.KEY_STATE_HELD and not macro in self.__repeat_macros and not repetition: + self.__repeat_macros.append(macro) + self._process_macro(macro, state, key_states) + + # We test again because a toggle might have stopped the repeat + if macro in self.__repeat_macros: + self.__macro_repeat_timer = g15scheduler.queue(self.queue_name, "RepeatMacro", delay, self._handle_macro, macro, g15driver.KEY_STATE_HELD, key_states, True) + elif state == g15driver.KEY_STATE_DOWN and macro.activate_on == g15driver.KEY_STATE_DOWN: + self._process_macro(macro, state, key_states) + elif state == g15driver.KEY_STATE_UP and macro.activate_on == g15driver.KEY_STATE_UP: + self._process_macro(macro, state, key_states) + elif state == g15driver.KEY_STATE_HELD and macro.activate_on == g15driver.KEY_STATE_HELD: + self._process_macro(macro, state, key_states) + + # Also defeat the key release so any normal KEY_STATE_UP macros don't get activated as well + self._defeat_release(key_states) + + def __cancel_macro_repeat_timer(self): + """ + Cancel the currently pending macro repeat + """ + if self.__macro_repeat_timer is not None: + self.__macro_repeat_timer.cancel() + self.__macro_repeat_timer = None + + def _handle_key(self, keys, state_id, post=False): + """ + Send the key press to various handlers. This is for plugins and other + code that needs to completely take over the macro keys, for general + key handling "Actions" should be used instead. + """ + + # Event first goes to this objects key handlers + for h in self.key_handlers: + if h.handle_key(keys, state_id, post): + return True + + return False + + def _action_performed(self, binding): + logger.info("Invoking action '%s'", binding.action) + + for l in self.action_listeners: + if l.action_performed(binding): + return True \ No newline at end of file diff --git a/src/gnome15/g15keyio.py b/src/gnome15/g15keyio.py new file mode 100644 index 0000000..bd5de79 --- /dev/null +++ b/src/gnome15/g15keyio.py @@ -0,0 +1,206 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2012 Brett Smith +# +# 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 . + +""" +Classes and functions for recording X key presses using either raw X events +or XTEST as well as injecting such keys +""" + +import gnome15.g15locale as g15locale +import gnome15.g15uinput as g15uinput +_ = g15locale.get_translation("macro-recorder", modfile = __file__).ugettext + +import time +import logging +logger = logging.getLogger(__name__) + +from Xlib import X, XK, display +from Xlib.ext import record +from Xlib.protocol import rq + +from threading import Thread + +local_dpy = display.Display() +record_dpy = display.Display() + +def get_keysyms(): + l = [] + for name in dir(XK): + logger.debug(" %s", name) + if name[:3] == "XK_": + l.append(name[3:]) + return l + +class RecordThread(Thread): + def __init__(self, _record_callback): + Thread.__init__(self) + self.setDaemon(True) + self.name = "RecordThread" + self._record_callback = _record_callback + self.ctx = record_dpy.record_create_context( + 0, + [record.AllClients], + [{ + 'core_requests': (0, 0), + 'core_replies': (0, 0), + 'ext_requests': (0, 0, 0, 0), + 'ext_replies': (0, 0, 0, 0), + 'delivered_events': (0, 0), + 'device_events': (X.KeyPress, X.MotionNotify), + 'errors': (0, 0), + 'client_started': False, + 'client_died': False, + }]) + + def disable_record_context(self): + if self.ctx != None: + local_dpy.record_disable_context(self.ctx) + local_dpy.flush() + + def run(self): + record_dpy.record_enable_context(self.ctx, self._record_callback) + record_dpy.record_free_context(self.ctx) + +class G15KeyRecorder(): + + def __init__(self, driver): + self._driver = driver + self._record_key = None + self._record_thread = None + self._last_keys = None + self._key_down = None + + self.script = [] + self.on_add = None + self.on_stop = None + self.single_key = False + self.output_delays = True + self.emit_uinput = False + + def clear(self): + del self.script[:] + + def is_recording(self): + return self._record_thread is not None + + def start_record(self): + if self._record_thread is None: + self._start_recording() + return True + else: + self._cancel_macro(None) + return True + + ''' + Private + ''' + + def _lookup_keysym(self, keysym): + logger.debug("Looking up %s", keysym) + for name in dir(XK): + logger.debug(" %s", name) + if name[:3] == "XK_" and getattr(XK, name) == keysym: + return name[3:] + return "[%d]" % keysym + + def _record_callback(self, reply): + if reply.category != record.FromServer: + return + if reply.client_swapped: + return + if not len(reply.data) or ord(reply.data[0]) < 2: + # not an event + return + + data = reply.data + while len(data): + event, data = rq.EventField(None).parse_binary_value(data, record_dpy.display, None, None) + if event.type in [X.KeyPress, X.KeyRelease]: + pr = event.type == X.KeyPress and "Press" or "Release" + logger.debug("Event detail = %s", event.detail) + keysym = local_dpy.keycode_to_keysym(event.detail, 0) + if not keysym: + logger.debug("Recorded %s", event.detail) + self._record_key_callback(event, event.detail) + else: + logger.debug("Keysym = %s", str(keysym)) + s = self._lookup_keysym(keysym) + logger.debug("Recorded %s", s) + self._record_key_callback(event, s) + + def _record_key_callback(self, event, keyname): + if self._key_down == None: + self._key_down = time.time() + else: + now = time.time() + delay = time.time() - self._key_down + if self.output_delays: + self.script.append(["Delay", str(int(delay * 1000))]) + self._key_down = now + + if self.emit_uinput: + pr = event.type == X.KeyPress and "UPress" or "URelease" + keyname = g15uinput.get_keysym_to_uinput_mapping(keyname) + " " + g15uinput.KEYBOARD + if keyname: + for c in keyname.split(","): + self._add_key(pr, event, c) + else: + pr = event.type == X.KeyPress and "Press" or "Release" + self._add_key(pr, event, keyname) + + + def _add_key(self, pr, event, keyname): + keydown = self._key_state[keyname] if keyname in self._key_state else None + if keydown is None: + if event.type == X.KeyPress: + self._key_state[keyname] = True + self._add(pr, keyname) + else: + # Got a release without getting a press - ignore + pass + else: + if event.type == X.KeyRelease: + self._add(pr, keyname) + del self._key_state[keyname] + + if self.single_key: + self.stop_record() + + def _add(self, pr, keyname): + self.script.append([pr, keyname]) + if self.on_add: + self.on_add(pr, keyname) + + def _done_recording(self, state): + if self._record_keys != None: + self.stop_record() + + def stop_record(self): + if self._record_thread != None: + self._record_thread.disable_record_context() + self._key_down = None + self._record_key = None + self._record_thread = None + if self.on_stop is not None: + self.on_stop(self) + + def _start_recording(self): + self.script = [] + self._key_state = {} + self._key_down = None + self._record_thread = RecordThread(self._record_callback) + self._record_thread.start() + diff --git a/src/gnome15/g15locale.py b/src/gnome15/g15locale.py new file mode 100644 index 0000000..7434333 --- /dev/null +++ b/src/gnome15/g15locale.py @@ -0,0 +1,241 @@ +# coding: utf-8 + +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2011 Brett Smith +# Copyright (C) 2013 Brett Smith +# Nuno Araujo +# +# 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 . + +""" +Helpers for internationalisation. See http://wiki.maemo.org/How_to_Internationalize_python_apps. +Gnome15 has multiple translation domains that are loaded dynamically and then cached +in memory. + +Translations may be required for Python code, SVG or Glade files. +""" + +import os +import locale +import gettext +import g15globals +import util.g15gconf as g15gconf +import time +import datetime +import re + +import logging +logger = logging.getLogger(__name__) + +# Change this variable to your app name! +# The translation files will be under +# @LOCALE_DIR@/@LANGUAGE@/LC_MESSAGES/@APP_NAME@.mo +APP_NAME = "SleepAnalyser" + +LOCALE_DIR = g15globals.i18n_dir + +# Now we need to choose the language. We will provide a list, and gettext +# will use the first translation available in the list +# +# In maemo it is in the LANG environment variable +# (on desktop is usually LANGUAGES) +DEFAULT_LANGUAGES = [] +if 'LANG' in os.environ: + DEFAULT_LANGUAGES += os.environ.get('LANG', '').split(':') +if 'LANGUAGE' in os.environ: + DEFAULT_LANGUAGES += os.environ.get('LANGUAGE', '').split('.') +DEFAULT_LANGUAGES += ['en_GB'] + +lc, encoding = locale.getdefaultlocale() +if lc: + languages = [lc] +else: + languages = [] + +# Concat all languages (env + default locale), +# and here we have the languages and location of the translations +languages += DEFAULT_LANGUAGES +mo_location = LOCALE_DIR + +# Cached translations +__translations = {} + +# Replace these date/time formats to get a format without seconds +REPLACE_FORMATS = [ + (u'.%S', u''), + (u':%S', u''), + (u',%S', u''), + (u' %S', u''), + (u':%OS', ''), + (u'%r', '%I:%M %p'), + (u'%t', '%H:%M'), + (u'%T', '%H:%M') + ] + +def format_time(time_val, gconf_client, display_seconds = True, show_timezone = False, compact = True): + """ + Format a given time / datetime as a time in the 12hour format. GConf + is checked for custom format, otherwise the default for the locale is + used. + + Keyword arguments: + time_val -- time / datetime object + gconf_client -- gconf client instance + display_seconds -- if false, seconds will be stripped from result + """ + fmt = g15gconf.get_string_or_default(gconf_client, + "/apps/gnome15/time_format", + locale.nl_langinfo(locale.T_FMT_AMPM)) + # For some locales T_FMT_AMPM is empty. + # Set the format to a default value if this is the case. + if fmt == "": + fmt = "%r" + + if not display_seconds: + fmt = __strip_seconds(fmt) + if isinstance(time_val, time.struct_time): + time_val = datetime.datetime(*time_val[:6]) + + if not show_timezone: + fmt = fmt.replace("%Z", "") + + if compact: + fmt = fmt.replace(" %p", "%p") + fmt = fmt.replace(" %P", "%P") + + fmt = fmt.strip() + + if isinstance(time_val, tuple): + return time.strftime(fmt, time_val) + else: + return time_val.strftime(fmt) + +def format_time_24hour(time_val, gconf_client, display_seconds = True, show_timezone = False): + """ + Format a given time / datetime as a time in the 24hour format. GConf + is checked for custom format, otherwise the default for the locale is + used. + + Keyword arguments: + time_val -- time / datetime object / tuple + gconf_client -- gconf client instance + display_seconds -- if false, seconds will be stripped from result + """ + fmt = g15gconf.get_string_or_default(gconf_client, "/apps/gnome15/time_format_24hr", locale.nl_langinfo(locale.T_FMT)) + if not display_seconds: + fmt = __strip_seconds(fmt) + if isinstance(time_val, time.struct_time): + time_val = datetime.datetime(*time_val[:6]) + + if not show_timezone: + fmt = fmt.replace("%Z", "") + fmt = fmt.strip() + + if isinstance(time_val, tuple): + return time.strftime(fmt, time_val) + else: + return time_val.strftime(fmt) + +def format_date(date_val, gconf_client): + """ + Format a datetime as a date (without time). GConf + is checked for custom format, otherwise the default for the locale is + used. + + Keyword arguments: + date_val -- date / datetime object + gconf_client -- gconf client instance + """ + fmt = g15gconf.get_string_or_default(gconf_client, "/apps/gnome15/date_format", locale.nl_langinfo(locale.D_FMT)) + if isinstance(date_val, tuple): + return datetime.date.strftime(fmt, date_val) + else: + return date_val.strftime(fmt) + +def format_date_time(date_val, gconf_client, display_seconds = True): + """ + Format a datetime as a date and a time. GConf + is checked for custom format, otherwise the default for the locale is + used. + + Keyword arguments: + date_val -- date / datetime object + gconf_client -- gconf client instance + display_seconds -- if false, seconds will be stripped from result + """ + fmt = g15gconf.get_string_or_default(gconf_client, "/apps/gnome15/date_time_format", locale.nl_langinfo(locale.D_T_FMT)) + if not display_seconds: + fmt = __strip_seconds(fmt) + if isinstance(date_val, tuple): + return datetime.datetime.strftime(fmt, date_val) + else: + return date_val.strftime(fmt) + +def get_translation(domain, modfile=None): + """ + Initialize a translation domain. Unless modfile is supplied, + the translation will be searched for in the default location. If it + is supplied, it's parent directory will be pre-pended to i18n to get + the location to use. + + Translation objects are cached. + + Keyword arguments: + domain -- translation domain + modfile -- module file location (search relative to this file + /i18n) + """ + if domain in __translations: + return __translations[domain] + gettext.install (True, localedir=None, unicode=1) + translation_location = mo_location + if modfile is not None: + translation_location = "%s/i18n" % os.path.dirname(modfile) + gettext.find(domain, translation_location) + locale.bindtextdomain(domain, translation_location) + gettext.bindtextdomain(domain, translation_location) + gettext.textdomain (domain) + gettext.bind_textdomain_codeset(domain, "UTF-8") + language = gettext.translation (domain, translation_location, languages=languages, fallback=True) + __translations[domain] = language + return language + +def parse_US_time(time_val): + """ + Parses a time in the US format (%I:%M %p) + This method assumes that the time_val value is valid. + It's behaviour is similar to a call to time.strptime + """ + parsed = re.match('(0?[1-9]|1[0-2]):([0-5][0-9]) (AM|am|PM|pm)', time_val) + hour, minute, ampm = parsed.group(1, 2, 3) + hour = int(hour) + minute = int(minute) + if ampm.lower() == 'pm': + hour = hour + 12 + return time.struct_time((1900, 1, 1, hour, minute, 0, 0, 1, -1)) + +def parse_US_time_or_none(time_val): + try: + return parse_US_time(time_val) + except Exception as e: + logger.debug("Invalid format for US time.", exc_info = e) + return None + +""" +Private +""" + +def __strip_seconds(fmt): + for f in REPLACE_FORMATS: + fmt = fmt.replace(*f) + return fmt \ No newline at end of file diff --git a/src/gnome15/g15logging.py b/src/gnome15/g15logging.py new file mode 100644 index 0000000..b54dbe9 --- /dev/null +++ b/src/gnome15/g15logging.py @@ -0,0 +1,51 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2013 Nuno Araujo +# +# 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 . + +""" +Initializes and configures the python logging system that is used in Gnome15 +""" +import logging + +DEFAULT_LEVEL = logging.NOTSET + +def configure(): + """ + Configures the logging python module with a basic configuration + and a logging format. + """ + logging.basicConfig(format='%(levelname)s\t%(asctime)s-%(threadName)s\t%(name)s - %(message)s', + datefmt='%H:%M:%S') + +def get_level(level): + """ + Returns the python logging module level matching the string passed + as parameter, or the default logging level if the string doesn't + match any level. + """ + result = logging.getLevelName(level.upper()) + if result == "Level %s" % level: + result = DEFAULT_LEVEL + return result + +def get_root_logger(): + """ + Initializes the logging system with a basic configuration, and + creates a root logger set to the default logging level. + """ + configure() + logger = logging.getLogger() + logger.setLevel(DEFAULT_LEVEL) + return logger diff --git a/src/gnome15/g15macroeditor.py b/src/gnome15/g15macroeditor.py new file mode 100644 index 0000000..9b3cfc7 --- /dev/null +++ b/src/gnome15/g15macroeditor.py @@ -0,0 +1,1104 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2011 Brett Smith +# +# 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 . + +""" +Manages the UI for editing a single macro. +""" + +import gnome15.g15locale as g15locale +_ = g15locale.get_translation("gnome15").ugettext + + +import g15globals +import g15profile +import util.g15scheduler as g15scheduler +import util.g15gconf as g15gconf +import util.g15icontools as g15icontools +import g15uinput +import g15devices +import g15driver +import g15keyio +import g15actions +import pygtk +pygtk.require('2.0') +import gtk +import gobject +import os +import pango +import gconf + +import logging +logger = logging.getLogger(__name__) + +# Key validation constants +IN_USE = "in-use" +RESERVED_FOR_ACTION = "reserved" +NO_KEYS = "no-keys" +OK = "ok" + +class G15MacroEditor(): + + def __init__(self, parent=None): + """ + Constructor. Create a new macro editor. You must call set_driver() + and set_macro() after constructions to populate the macro key buttons + and the other fields. + """ + self.__gconf_client = gconf.client_get_default() + self.__widget_tree = gtk.Builder() + self.__widget_tree.set_translation_domain("g15-macroeditor") + self.__widget_tree.add_from_file(os.path.join(g15globals.ui_dir, "macro-editor.ui")) + self.__window = self.__widget_tree.get_object("EditMacroDialog") + if self.__window is not None and parent is not None: + self.__window.set_transient_for(parent) + + self.adjusting = False + self.editing_macro = None + self.selected_profile = None + self.memory_number = 1 + self.close_button = None + + # Private + self.__text_buffer = None + self.__rows = None + self.__driver = None + self.__key_buttons = None + self.__load_objects() + self.__load_actions() + self.__create_macro_info_bar() + self.__macro_save_timer = None + + # Connect signal handlers + self.__widget_tree.connect_signals(self) + + def run(self): + self.__window.run() + self.__window.hide() + + def set_driver(self, driver): + """ + Set the driver to use for this macro. This allows the full set of + available keys (and other capabilities) to determined. + + Keyword arguments: + driver -- driver + """ + self.__driver = driver + + def set_macro(self, macro): + """ + Set the macro to edit. Note, set_driver must have been called first + so it knows which macro keys are available for use for the model + in question. + + Keyword arguments: + macro -- macro to edit + """ + if self.__driver is None: + raise Exception("No driver set. Cannot set macro") + + self.adjusting = True + try: + self.editing_macro = macro + self.selected_profile = macro.profile + self.memory_number = macro.memory + self.__widget_tree.get_object("KeyBox").set_sensitive(not self.selected_profile.read_only) + keys_frame = self.__widget_tree.get_object("KeysFrame") + self.__allow_combination.set_active(len(self.editing_macro.keys) > 1) + + # Build the G-Key selection widget + if self.__rows: + keys_frame.remove(self.__rows) + self.__rows = gtk.VBox() + self.__rows.set_spacing(4) + self.__key_buttons = [] + for row in self.__driver.get_key_layout(): + hbox = gtk.HBox() + hbox.set_spacing(4) + for key in row: + key_name = g15driver.get_key_names([ key ]) + g_button = gtk.ToggleButton(" ".join(key_name)) + g_button.key = key + key_active = key in self.editing_macro.keys + g_button.set_active(key_active) + self.__set_button_style(g_button) + g_button.connect("toggled", self._toggle_key, key, self.editing_macro) + self.__key_buttons.append(g_button) + hbox.pack_start(g_button, True, True) + self.__rows.pack_start(hbox, False, False) + keys_frame.add(self.__rows) + keys_frame.show_all() + + # Set the activation mode + for index, (activate_on_id, activate_on_name) in enumerate(self.__activate_on_combo.get_model()): + if activate_on_id == self.editing_macro.activate_on: + self.__activate_on_combo.set_active(index) + + # Set the repeat mode + for index, (repeat_mode_id, repeat_mode_name) in enumerate(self.__repeat_mode_combo.get_model()): + if repeat_mode_id == self.editing_macro.repeat_mode: + self.__repeat_mode_combo.set_active(index) + + # Set the type of macro + for index, (macro_type, macro_type_name) in enumerate(self.__map_type_model): + if macro_type == self.editing_macro.type: + self.__mapped_key_type_combo.set_active(index) + self.__set_available_options() + + # Set the other details + for index, row in enumerate(self.__map_type_model): + if row[0] == self.editing_macro.type: + self.__mapped_key_type_combo.set_active(index) + break + self.__load_keys() + if self.editing_macro.type in [ g15profile.MACRO_MOUSE, g15profile.MACRO_JOYSTICK, g15profile.MACRO_DIGITAL_JOYSTICK, g15profile.MACRO_KEYBOARD ]: + for index, row in enumerate(self.__mapped_key_model): + if self.__mapped_key_model[index][0] == self.editing_macro.macro: + self.__select_tree_row(self.__uinput_tree, index) + break + elif self.editing_macro.type == g15profile.MACRO_ACTION: + for index, row in enumerate(self.__action_model): + if self.__action_model[index][0] == self.editing_macro.macro: + self.__select_tree_row(self.__action_tree, index) + break + + self.__text_buffer = gtk.TextBuffer() + self.__text_buffer.connect("changed", self._macro_script_changed) + self.__macro_script.set_buffer(self.__text_buffer) + + self.__turbo_rate.get_adjustment().set_value(self.editing_macro.repeat_delay) + self.__memory_bank_label.set_text("M%d" % self.memory_number) + self.__macro_name_field.set_text(self.editing_macro.name) + self.__override_default_repeat.set_active(self.editing_macro.repeat_delay != -1) + + if self.editing_macro.type == g15profile.MACRO_SIMPLE: + self.__simple_macro.set_text(self.editing_macro.macro) + else: + self.__simple_macro.set_text("") + if self.editing_macro.type == g15profile.MACRO_COMMAND: + cmd = self.editing_macro.macro + background = False + if cmd.endswith("&"): + cmd = cmd[:-1] + background = True + elif cmd == "": + background = True + self.__command.set_text(cmd) + self.__run_in_background.set_active(background) + else: + self.__run_in_background.set_active(False) + self.__command.set_text("") + if self.editing_macro.type == g15profile.MACRO_SCRIPT: + self.__text_buffer.set_text(self.editing_macro.macro) + else: + self.__text_buffer.set_text("") + + self.__check_macro(self.editing_macro.keys) + self.__macro_name_field.grab_focus() + + finally: + self.adjusting = False + self.editing_macro.name = self.__macro_name_field.get_text() + self.__set_available_options() + + """ + Event handlers + """ + def _override_default_repeat_changed(self, widget): + if not self.adjusting: + sel = widget.get_active() + if sel: + self.editing_macro.repeat_delay = 0.1 + self.__turbo_rate.get_adjustment().set_value(0.1) + self.__save_macro(self.editing_macro) + self.__set_available_options() + else: + self.editing_macro.repeat_delay = -1.0 + self.__set_available_options() + self.__save_macro(self.editing_macro) + + def _macro_script_changed(self, text_buffer): + self.editing_macro.macro = text_buffer.get_text(text_buffer.get_start_iter(), text_buffer.get_end_iter()) + self.__save_macro(self.editing_macro) + + def _show_script_editor(self, widget): + editor = G15MacroScriptEditor(self.__gconf_client, self.__driver, self.editing_macro, self.__window) + if editor.run(): + self.__text_buffer.set_text(self.editing_macro.macro) + self.__save_macro(self.editing_macro) + + def _turbo_changed(self, widget): + if not self.adjusting: + self.editing_macro.repeat_delay = widget.get_value() + self.__save_macro(self.editing_macro) + + def _repeat_mode_selected(self, widget): + if not self.adjusting: + self.editing_macro.repeat_mode = widget.get_model()[widget.get_active()][0] + self.__save_macro(self.editing_macro) + self.__set_available_options() + + def _mapped_key_type_changed(self, widget): + if not self.adjusting: + key = self.__map_type_model[widget.get_active()][0] + self.editing_macro.type = key + self.editing_macro.macro = "" + self.adjusting = True + try: + self.__load_keys() + finally: + self.adjusting = False + self.__select_tree_row(self.__uinput_tree, 0) + self.set_macro(self.editing_macro) + self.__set_available_options() + + def _clear_filter(self, widget): + self.__filter.set_text("") + + def _filter_changed(self, widget): + try: + self.adjusting = True + self.__load_keys() + finally: + self.adjusting = False + self._key_selected(None) + + def _simple_macro_changed(self, widget): + self.editing_macro.macro = widget.get_text() + self.__save_macro(self.editing_macro) + + def _command_changed(self, widget): + self.__save_command() + + def _browse_for_command(self, widget): + dialog = gtk.FileChooserDialog(_("Open.."), + None, + gtk.FILE_CHOOSER_ACTION_OPEN, + (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, + gtk.STOCK_OPEN, gtk.RESPONSE_OK)) + dialog.set_default_response(gtk.RESPONSE_OK) + file_filter = gtk.FileFilter() + file_filter.set_name(_("All files")) + file_filter.add_pattern("*") + dialog.add_filter(file_filter) + response = dialog.run() + while gtk.events_pending(): + gtk.main_iteration(False) + if response == gtk.RESPONSE_OK: + self.__command.set_text(dialog.get_filename()) + dialog.destroy() + return False + + def _run_in_background_changed(self, widget): + if not self.adjusting: + self.__save_command() + + def _allow_combination_changed(self, widget): + if not self.adjusting and not self.__allow_combination.get_active(): + for button in self.__key_buttons: + if len(self.editing_macro.keys) > 1: + button.set_active(False) + self.__check_macro(self.editing_macro.keys) + + def _macro_name_changed(self, widget): + self.editing_macro.name = widget.get_text() + self.__save_macro(self.editing_macro) + + def _toggle_key(self, widget, key, macro): + """ + Event handler invoked when one of the macro key buttons is pressed. + """ + keys = list(macro.keys) + + if key in keys: + keys.remove(key) + else: + if not self.adjusting and not self.__allow_combination.get_active(): + for button in self.__key_buttons: + if button != widget: + self.adjusting = True + try : + button.set_active(False) + finally: + self.adjusting = False + for ikey in keys: + if ikey != key: + keys.remove(ikey) + keys.append(key) + + if not self.selected_profile.are_keys_in_use(self.editing_macro.activate_on, + self.memory_number, keys, + exclude=[self.editing_macro]): + if self.__macro_name_field.get_text() == "" or self.__macro_name_field.get_text().startswith("Macro "): + new_name = " ".join(g15driver.get_key_names(keys)) + self.editing_macro.name = _("Macro %s") % new_name + self.__macro_name_field.set_text(self.editing_macro.name) + macro.set_keys(keys) + + self.__set_button_style(widget) + + if not self.adjusting: + self.__check_macro(keys) + self.__save_macro(self.editing_macro) + + def _key_selected(self, widget): + if not self.adjusting: + (model, path) = self.__uinput_tree.get_selection().get_selected() + if path is not None: + key = model[path][0] + self.editing_macro.macro = key + self.__save_macro(self.editing_macro) + + def _action_selected(self, widget): + if not self.adjusting: + (model, path) = self.__action_tree.get_selection().get_selected() + if path: + key = model[path][0] + self.editing_macro.macro = key + self.__save_macro(self.editing_macro) + + def _activate_on_changed(self, widget): + if not self.adjusting: + self.editing_macro.set_activate_on(widget.get_model()[widget.get_active()][0]) + self.__save_macro(self.editing_macro) + if self.editing_macro.activate_on == g15driver.KEY_STATE_HELD: + self.__repeat_mode_combo.set_active(0) + self.__set_available_options() + self.__check_macro(list(self.editing_macro.keys)) + + """ + Private + """ + + def __save_command(self): + macrotext = self.__command.get_text() + if self.__run_in_background.get_active(): + macrotext += "&" + self.editing_macro.macro = macrotext + self.__save_macro(self.editing_macro) + + def __select_tree_row(self, tree, row): + tree_iter = tree.get_model().iter_nth_child(None, row) + if tree_iter: + tree_path = tree.get_model().get_path(tree_iter) + tree.get_selection().select_path(tree_path) + tree.scroll_to_cell(tree_path) + + def __save_macro(self, macro): + """ + Schedule saving of the macro in 2 seconds. This may be called again + before the 2 seconds are up, in which case the timer will reset. + + Keyword arguments: + macro -- macro to save + """ + if not self.adjusting: + if self.__macro_save_timer is not None: + self.__macro_save_timer.cancel() + self.__macro_save_timer = g15scheduler.schedule("SaveMacro", 2, self.__do_save_macro, macro) + + def __do_save_macro(self, macro): + """ + Actually save the macro. This should not be called directly + + Keyword arguments: + macro -- macro to save + """ + if self.__validate_macro(macro.keys) in [ OK, RESERVED_FOR_ACTION ] : + logger.info("Saving macro %s", macro.name) + macro.save() + + def __load_actions(self): + self.__action_model.clear() + for action in g15actions.actions: + self.__action_model.append([action, action]) + + def __load_objects(self): + """ + Load references to the various components contain in the Glade file + """ + self.__macro_script = self.__widget_tree.get_object("MacroScript") + self.__map_type_model = self.__widget_tree.get_object("MapTypeModel") + self.__mapped_key_model = self.__widget_tree.get_object("MappedKeyModel") + self.__mapped_key_type_combo = self.__widget_tree.get_object("MappedKeyTypeCombo") + self.__map_type_model = self.__widget_tree.get_object("MapTypeModel") + self.__simple_macro = self.__widget_tree.get_object("SimpleMacro") + self.__command = self.__widget_tree.get_object("Command") + self.__run_in_background = self.__widget_tree.get_object("RunInBackground") + self.__browse_for_command = self.__widget_tree.get_object("BrowseForCommand") + self.__allow_combination = self.__widget_tree.get_object("AllowCombination") + self.__macro_name_field = self.__widget_tree.get_object("MacroNameField") + self.__macro_warning_box = self.__widget_tree.get_object("MacroWarningBox") + self.__memory_bank_label = self.__widget_tree.get_object("MemoryBankLabel") + self.__uinput_box = self.__widget_tree.get_object("UinputBox") + self.__command_box = self.__widget_tree.get_object("CommandBox") + self.__script_box = self.__widget_tree.get_object("ScriptBox") + self.__simple_box = self.__widget_tree.get_object("SimpleBox") + self.__action_box = self.__widget_tree.get_object("ActionBox") + self.__uinput_tree = self.__widget_tree.get_object("UinputTree") + self.__action_tree = self.__widget_tree.get_object("ActionTree") + self.__action_model = self.__widget_tree.get_object("ActionModel") + self.__repeat_mode_combo = self.__widget_tree.get_object("RepeatModeCombo") + self.__repetition_frame = self.__widget_tree.get_object("RepetitionFrame") + self.__turbo_rate = self.__widget_tree.get_object("TurboRate") + self.__turbo_box = self.__widget_tree.get_object("TurboBox") + self.__filter = self.__widget_tree.get_object("Filter") + self.__override_default_repeat = self.__widget_tree.get_object("OverrideDefaultRepeat") + self.__activate_on_combo = self.__widget_tree.get_object("ActivateOnCombo") + self.__show_script_editor = self.__widget_tree.get_object("ShowScriptEditor") + + def __load_keys(self): + """ + Load the available keys for the selected macro type + """ + sel_type = self.__get_selected_type() + filter_text = self.__filter.get_text().strip().lower() + if g15profile.is_uinput_type(sel_type): + (model, path) = self.__uinput_tree.get_selection().get_selected() + sel = None + if path: + sel = model[path][0] + model.clear() + found = False + for n, v in g15uinput.get_buttons(sel_type): + if len(filter_text) == 0 or filter_text in n.lower(): + model.append([n, v]) + if n == sel: + self.__select_tree_row(self.__uinput_tree, len(model)) + found = True + (model, path) = self.__uinput_tree.get_selection().get_selected() + if not found and len(model) > 0: + self.__select_tree_row(self.__uinput_tree, 0) + + def __get_selected_type(self): + """ + Get the selected macro type + """ + return self.__map_type_model[self.__mapped_key_type_combo.get_active()][0] + + def __set_available_options(self): + """ + Set the sensitive state of various components based on the current + selection of other components. + """ + + sel_type = self.__get_selected_type(); + uinput_type = g15profile.is_uinput_type(sel_type) + opposite_state = g15driver.KEY_STATE_UP if \ + self.editing_macro.activate_on == \ + g15driver.KEY_STATE_HELD else \ + g15driver.KEY_STATE_HELD + key_conflict = self.selected_profile.get_macro(opposite_state, \ + self.editing_macro.memory, + self.editing_macro.keys) is not None + + self.__uinput_tree.set_sensitive(uinput_type) + self.__run_in_background.set_sensitive(sel_type == g15profile.MACRO_COMMAND) + self.__command.set_sensitive(sel_type == g15profile.MACRO_COMMAND) + self.__browse_for_command.set_sensitive(sel_type == g15profile.MACRO_COMMAND) + self.__simple_macro.set_sensitive(sel_type == g15profile.MACRO_SIMPLE) + self.__macro_script.set_sensitive(sel_type == g15profile.MACRO_SCRIPT) + self.__action_tree.set_sensitive(sel_type == g15profile.MACRO_ACTION) + self.__activate_on_combo.set_sensitive(not uinput_type and not key_conflict) + self.__repeat_mode_combo.set_sensitive(self.__activate_on_combo.get_active() != 2) + self.__override_default_repeat.set_sensitive(self.editing_macro.repeat_mode != g15profile.NO_REPEAT) + self.__turbo_box.set_sensitive(self.editing_macro.repeat_mode != g15profile.NO_REPEAT and self.__override_default_repeat.get_active()) + + self.__simple_box.set_visible(sel_type == g15profile.MACRO_SIMPLE) + self.__command_box.set_visible(sel_type == g15profile.MACRO_COMMAND) + self.__action_box.set_visible(sel_type == g15profile.MACRO_ACTION) + self.__script_box.set_visible(sel_type == g15profile.MACRO_SCRIPT) + self.__show_script_editor.set_visible(sel_type == g15profile.MACRO_SCRIPT) + self.__uinput_box.set_visible(uinput_type) + + def __validate_macro(self, keys): + """ + Validate the list of keys, checking if they are in use, reserved + for an action, and that some have actually been supplier + + Keyword arguments: + keys -- list of keys to validate + """ + if len(keys) > 0: + reserved = g15devices.are_keys_reserved(self.__driver.get_model_name(), keys) + + in_use = self.selected_profile.are_keys_in_use(self.editing_macro.activate_on, + self.memory_number, + keys, + exclude=[self.editing_macro]) + if in_use: + return IN_USE + elif reserved: + return RESERVED_FOR_ACTION + else: + return OK + else: + return NO_KEYS + + def __check_macro(self, keys): + """ + Check with the keys provided are valid for the current state, e.g. + check if another macro or action is using them. Note, this still + allows the change to happen, it will just show a warning and prevent + the window from being closed if + """ + val = self.__validate_macro(keys) + if val == IN_USE: + self.__macro_infobar.set_message_type(gtk.MESSAGE_ERROR) + self.__macro_warning_label.set_text(_("This key combination is already in use with " + \ + "another macro. Please choose a different key or combination of keys")) + self.__macro_infobar.set_visible(True) + self.__macro_infobar.show_all() + + if self.close_button is not None: + self.close_button.set_sensitive(False) + elif val == RESERVED_FOR_ACTION: + self.__macro_infobar.set_message_type(gtk.MESSAGE_WARNING) + self.__macro_warning_label.set_text(_("This key combination is reserved for use with an action. You " + \ + "may use it, but the results are undefined.")) + self.__macro_infobar.set_visible(True) + self.__macro_infobar.show_all() + if self.close_button is not None: + self.close_button.set_sensitive(True) + elif val == NO_KEYS: + self.__macro_infobar.set_message_type(gtk.MESSAGE_WARNING) + self.__macro_warning_label.set_text(_("You have not chosen a macro key to assign the action to.")) + self.__macro_infobar.set_visible(True) + self.__macro_infobar.show_all() + if self.close_button is not None: + self.close_button.set_sensitive(False) + else: + self.__macro_infobar.set_visible(False) + if self.close_button is not None: + self.close_button.set_sensitive(True) + + def __create_macro_info_bar(self): + """ + Creates a component for display information about the current + macro, such as conflicts. The component is added to a placeholder in + the Glade file + """ + self.__macro_infobar = gtk.InfoBar() + self.__macro_infobar.set_size_request(-1, -1) + self.__macro_warning_label = gtk.Label() + self.__macro_warning_label.set_line_wrap(True) + self.__macro_warning_label.set_width_chars(60) + content = self.__macro_infobar.get_content_area() + content.pack_start(self.__macro_warning_label, True, True) + + self.__macro_warning_box.pack_start(self.__macro_infobar, True, True) + self.__macro_infobar.set_visible(False) + + def __set_button_style(self, button): + """ + Alter the button style based on whether it is active or not + + Keyword arguments: + button -- button widget + """ + font = pango.FontDescription("Sans 10") + if button.get_use_stock(): + label = button.child.get_children()[1] + elif isinstance(button.child, gtk.Label): + label = button.child + else: + raise ValueError("button does not have a label") + if button.get_active(): + font.set_weight(pango.WEIGHT_HEAVY) + else: + font.set_weight(pango.WEIGHT_MEDIUM) + label.modify_font(font) + +OP_ICONS = { 'delay' : 'gtk-media-pause', + 'press' : 'gtk-go-down', + 'upress' : 'gtk-go-down', + 'release' : 'gtk-go-up', + 'urelease' : 'gtk-go-up', + 'execute' : 'gtk-execute', + 'label' : 'gtk-underline', + 'wait' : 'gtk-stop', + 'goto' : [ 'stock_media-prev','media-skip-backward','gtk-media-previous' ] } + +class G15MacroScriptEditor(): + + def __init__(self, gconf_client, driver, editing_macro, parent = None): + + self.__gconf_client = gconf_client + self.__driver = driver + self.__clipboard = gtk.clipboard_get(gtk.gdk.SELECTION_CLIPBOARD) + + self.__recorder = g15keyio.G15KeyRecorder(self.__driver) + self.__recorder.on_stop = self._on_stop_record + self.__recorder.on_add = self._on_record_add + + self.__widget_tree = gtk.Builder() + self.__widget_tree.set_translation_domain("g15-macroeditor") + self.__widget_tree.add_from_file(os.path.join(g15globals.ui_dir, "script-editor.ui")) + self._load_objects() + if parent is not None: + self.__window.set_transient_for(parent) + self._load_key_presses() + self._configure_widgets() + self._add_info_box() + self.set_macro(editing_macro) + self._set_available() + + # Connect signal handlers + self.__widget_tree.connect_signals(self) + + # Configure defaults + self.__output_delays.set_active(g15gconf.get_bool_or_default(self.__gconf_client, "/apps/gnome15/script_editor/record_delays", True)) + self.__emit_uinput.set_active(g15gconf.get_bool_or_default(self.__gconf_client, "/apps/gnome15/script_editor/emit_uinput", False)) + self.__recorder.output_delays = self.__output_delays.get_active() + self.__recorder.emit_uinput = self.__emit_uinput.get_active() + + def set_macro(self, macro): + self.__editing_macro = macro + self.__macros = self.__editing_macro.macro.split("\n") + self.__recorder.clear() + self._rebuild_model() + self._set_available() + + def _rebuild_model(self): + self.__script_model.clear() + for macro_text in self.__macros: + split = macro_text.split(" ") + op = split[0].lower() + if len(split) > 1: + val = " ".join(split[1:]) + if op in OP_ICONS: + icon = OP_ICONS[op] + icon_path = g15icontools.get_icon_path(icon, 24) + self.__script_model.append([gtk.gdk.pixbuf_new_from_file(icon_path), val, op, True]) + + self._validate_script() + + def _validate_script(self): + msg = self._do_validate_script() + if msg: + self._show_message(gtk.MESSAGE_ERROR, msg) + self.__save_button.set_sensitive(False) + else: + self.__infobar.hide_all() + self.__save_button.set_sensitive(True) + + def _do_validate_script(self): + labels = [] + for _,val,op,_ in self.__script_model: + if op == "label": + if val in labels: + return "Label %s is defined more than once" % val + labels.append(val) + + pressed = {} + for _,val,op,_ in self.__script_model: + if op == "press" or op == "upress": + if val in pressed: + return "More than one key press of %s before a release" % val + pressed[val] = True + elif op == "release" or op == "urelease": + if not val in pressed: + return "Release of %s before it was pressed" % val + del pressed[val] + elif op == "goto": + if not val in labels: + return "Goto %s uses a label that doesn't exist" % val + + if len(pressed) > 0: + return "The script leaves %s pressed on completion" % ",".join(pressed.keys()) + + return None + + def run(self): + response = self.__window.run() + self.__window.hide() + if response == gtk.RESPONSE_OK: + buf = "" + for p in self.__macros: + if not buf == "": + buf += "\n" + buf += p + self.__editing_macro.macro = buf + return True + + def _add_info_box(self): + self.__infobar = gtk.InfoBar() + self.__infobar.set_size_request(-1, 32) + self.__warning_label = gtk.Label() + self.__warning_label.set_size_request(400, -1) + self.__warning_label.set_line_wrap(True) + self.__warning_label.set_alignment(0.0, 0.0) + self.__warning_image = gtk.Image() + content = self.__infobar.get_content_area() + content.pack_start(self.__warning_image, False, False) + content.pack_start(self.__warning_label, True, True) + self.__info_box_area.pack_start(self.__infobar, False, False) + self.__infobar.hide_all() + + def _show_message(self, message_type, text): + print "Showing message",text + self.__infobar.set_message_type(message_type) + self.__warning_label.set_text(text) + self.__warning_label.set_use_markup(True) + + if type == gtk.MESSAGE_WARNING: + self.__warning_image.set_from_stock(gtk.STOCK_DIALOG_WARNING, gtk.ICON_SIZE_DIALOG) + +# self.main_window.check_resize() + self.__infobar.show_all() + + def _load_objects(self): + self.__window = self.__widget_tree.get_object("EditScriptDialog") + self.__script_model = self.__widget_tree.get_object("ScriptModel") + self.__script_tree = self.__widget_tree.get_object("ScriptTree") + self.__set_value_dialog = self.__widget_tree.get_object("SetValueDialog") + self.__set_value = self.__widget_tree.get_object("SetValue") + self.__edit_selected_values = self.__widget_tree.get_object("EditSelectedValues") + self.__delay_adjustment = self.__widget_tree.get_object("DelayAdjustment") + self.__command = self.__widget_tree.get_object("Command") + self.__label = self.__widget_tree.get_object("Label") + self.__goto_label_model = self.__widget_tree.get_object("GotoLabelModel") + self.__goto_label = self.__widget_tree.get_object("GotoLabel") + self.__key_press_model = self.__widget_tree.get_object("KeyPressModel") + self.__record_key = self.__widget_tree.get_object("RecordKey") + self.__emit_uinput = self.__widget_tree.get_object("EmitUInput") + self.__output_delays = self.__widget_tree.get_object("OutputDelays") + self.__record_button = self.__widget_tree.get_object("RecordButton") + self.__stop_button = self.__widget_tree.get_object("StopButton") + self.__record_status = self.__widget_tree.get_object("RecordStatus") + self.__scrip_editor_popup = self.__widget_tree.get_object("ScriptEditorPopup") + self.__info_box_area = self.__widget_tree.get_object("InfoBoxArea") + self.__save_button = self.__widget_tree.get_object("SaveButton") + self.__wait_combo = self.__widget_tree.get_object("WaitCombo") + self.__wait_model = self.__widget_tree.get_object("WaitModel") + + def _load_key_presses(self): + self.__key_press_model.clear() + if self.__emit_uinput.get_active(): + for n, v in g15uinput.get_buttons(g15uinput.KEYBOARD): + self.__key_press_model.append([n]) + else: + for n in g15keyio.get_keysyms(): + self.__key_press_model.append([n]) + + def _configure_widgets(self): + self.__script_tree.get_selection().set_mode(gtk.SELECTION_MULTIPLE) + tree_selection = self.__script_tree.get_selection() + tree_selection.connect("changed", self._on_selection_changed) + + def _on_tree_button_press(self, treeview, event): + if event.button == 3: + x = int(event.x) + y = int(event.y) + time = event.time + tree_selection = self.__script_tree.get_selection() + if tree_selection.count_selected_rows() < 2: + pthinfo = treeview.get_path_at_pos(x, y) + if pthinfo is not None: + path, col, _, _ = pthinfo + treeview.grab_focus() + treeview.set_cursor( path, col, 0) + self.__scrip_editor_popup.popup( None, None, None, event.button, time) + return True + + def _on_cut(self, widget): + self._on_copy(widget) + tree_selection = self.__script_tree.get_selection() + _, selected_paths = tree_selection.get_selected_rows() + for p in reversed(selected_paths): + del self.__macros[p[0]] + self._rebuild_model() + + def _on_copy(self, widget): + tree_selection = self.__script_tree.get_selection() + model, selected_paths = tree_selection.get_selected_rows() + buf = "" + for p in selected_paths: + if not buf == "": + buf += "\n" + buf += self._format_row(model[p]) + self.__clipboard.set_text(buf) + + def _on_paste(self, widget): + self.__clipboard.request_text(self._clipboard_text_received) + + def _clipboard_text_received(self, clipboard, text, data): + i = self._get_insert_index() + if text: + for macro_text in text.split("\n"): + split = macro_text.split(" ") + op = split[0].lower() + if len(split) > 1: + val = split[1] + if op in OP_ICONS: + self.__macros.insert(i, macro_text) + i += 1 + self._rebuild_model() + + def _on_record_add(self, pr, key): + gobject.idle_add(self._set_available) + + def _on_selection_changed(self, widget): + self.__edit_selected_values.set_sensitive(self._unique_selected_types() == 1) + + def _unique_selected_types(self): + tree_selection = self.__script_tree.get_selection() + model, selected_paths = tree_selection.get_selected_rows() + t = {} + for p in selected_paths: + op = model[p][2] + t[op] = ( t[op] if op in t else 0 ) + 1 + + return len(t) + + def _on_emit_uinput_toggled(self, widget): + self.__recorder.emit_uinput = widget.get_active() + self.__gconf_client.set_bool("/apps/gnome15/script_editor/emit_uinput", widget.get_active()) + + def _on_deselect_all(self, widget): + self.__script_tree.get_selection().unselect_all() + + def _on_edit_selected_values_activate(self, widget): + self.__set_value_dialog.set_transient_for(self.__window) + response = self.__set_value_dialog.run() + self.__set_value_dialog.hide() + if response == gtk.RESPONSE_OK: + tree_selection = self.__script_tree.get_selection() + model, selected_paths = tree_selection.get_selected_rows() + for p in selected_paths: + self.__macros[p[0]] = self._format_row(model[p], self.__set_value.get_text()) + self._rebuild_model() + + def _format_row(self, row, value = None): + return "%s %s" % (self._format_op(row[2]),value if value is not None else row[1]) + + def _format_op(self, op): + return op[:1].upper() + op[1:] + + def _on_browse_command(self, widget): + dialog = gtk.FileChooserDialog("Choose Command..", + None, + gtk.FILE_CHOOSER_ACTION_OPEN, + (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, + gtk.STOCK_SAVE, gtk.RESPONSE_OK)) + dialog.set_default_response(gtk.RESPONSE_OK) + dialog.set_transient_for(self.__window) + dialog.set_filename(self.__command.get_text()) + response = dialog.run() + dialog.hide() + if response == gtk.RESPONSE_OK: + self.__command.set_text(dialog.get_filename()) + + def _on_new_goto(self, widget): + self.__goto_label_model.clear() + for _,val,op,_ in self.__script_model: + if op == "label": + self.__goto_label_model.append([val]) + if not self.__goto_label.get_active() >= 0 and len(self.__goto_label_model) > 0: + self.__goto_label.set_active(0) + + dialog = self.__widget_tree.get_object("AddGotoDialog") + dialog.set_transient_for(self.__window) + response = dialog.run() + dialog.hide() + if response == gtk.RESPONSE_OK: + self._insert_macro("%s %s" % ( self._format_op("goto"), self.__goto_label_model[self.__goto_label.get_active()][0])) + + def _on_new_label(self, widget): + dialog = self.__widget_tree.get_object("AddLabelDialog") + dialog.set_transient_for(self.__window) + response = dialog.run() + dialog.hide() + if response == gtk.RESPONSE_OK: + self._insert_macro("%s %s" % ( self._format_op("label"), self.__label.get_text())) + + def _on_new_execute(self, widget): + dialog = self.__widget_tree.get_object("AddExecuteDialog") + dialog.set_transient_for(self.__window) + response = dialog.run() + dialog.hide() + if response == gtk.RESPONSE_OK: + self._insert_macro("%s %s" % ( self._format_op("execute"), self.__command.get_text())) + + + def _on_new_wait(self, widget): + dialog = self.__widget_tree.get_object("AddWaitDialog") + dialog.set_transient_for(self.__window) + if not self.__wait_combo.get_active() >= 0 and len(self.__wait_model) > 0: + self.__wait_combo.set_active(0) + response = dialog.run() + dialog.hide() + if response == gtk.RESPONSE_OK: + self._insert_macro("%s %s" % ( self._format_op("wait"), self.__wait_model[self.__wait_combo.get_active()][0])) + + def _on_add_delay(self, widget): + dialog = self.__widget_tree.get_object("AddDelayDialog") + dialog.set_transient_for(self.__window) + response = dialog.run() + dialog.hide() + self._stop_recorder() + if response == gtk.RESPONSE_OK: + self._insert_macro("%s %s" % ( self._format_op("delay"), int(self.__delay_adjustment.get_value())) ) + + def _on_rows_reordered(self, model, path, iter, new_order): + print "reorder" + # The model will have been updated, so update our text base list from that + for index,row in enumerate(self._script.model): + x = self._format_row(row) + print x + self.__macros[index] = x + self._rebuild_model() + + def _get_insert_index(self): + tree_selection = self.__script_tree.get_selection() + _, selected_paths = tree_selection.get_selected_rows() + return len(self.__script_model) if len(selected_paths) == 0 else selected_paths[0][0] + 1 + + def _on_start_record_button(self, widget): + self.__recorder.start_record() + self._set_available() + + def _set_available(self): + self.__record_button.set_sensitive(not self.__recorder.is_recording()) + self.__stop_button.set_sensitive(self.__recorder.is_recording()) + ops = len(self.__recorder.script) + self.__record_status.set_text(_("Now recording (%d) operations" % ops) if self.__recorder.is_recording() else (_("Will insert %d operations" % ops) if ops > 0 else "")) + + def _on_stop_record_button(self, widget): + self.__recorder.stop_record() + + def _on_stop_record(self, recorder): + gobject.idle_add(self._set_available) + + def _stop_recorder(self): + if self.__recorder.is_recording(): + self.__recorder.stop_record() + + def _on_output_delays_changed(self, widget): + self.__recorder.output_delays = widget.get_active() + self.__gconf_client.set_bool("/apps/gnome15/script_editor/record_delays", self.__recorder.output_delays) + + def _on_record(self, widget): + self.__recorder.clear() + self._set_available() + dialog = self.__widget_tree.get_object("RecordDialog") + dialog.set_transient_for(self.__window) + response = dialog.run() + dialog.hide() + if self.__recorder.is_recording(): + self.__recorder.stop_record() + if response == gtk.RESPONSE_OK: + i = self._get_insert_index() + for op, value in self.__recorder.script: + if len(self.__recorder.script) > 0: + macro_text = "%s %s" % ( self._format_op(op), value) + self.__macros.insert(i, macro_text) + i += 1 + self._rebuild_model() + + def _insert_macro(self, macro_text): + i = self._get_insert_index() + self.__macros.insert(i, macro_text) + self._rebuild_model() + + def _on_remove_macro_operations(self, widget): + dialog = self.__widget_tree.get_object("RemoveMacroOperationsDialog") + dialog.set_transient_for(self.__window) + response = dialog.run() + dialog.hide() + if response == gtk.RESPONSE_OK: + tree_selection = self.__script_tree.get_selection() + _, selected_paths = tree_selection.get_selected_rows() + for p in reversed(selected_paths): + del self.__macros[p[0]] + self._rebuild_model() + + def _on_value_edited(self, widget, path, value): + self.__macros[int(path)] = self._format_row(self.__script_model[int(path)], value) + self._rebuild_model() + + def _on_select_all_key_operations(self, widget): + self._select_by_op([ "press", "release", "upress", "urelease" ]) + + def _on_select_all_key_presses(self, widget): + self._select_by_op(["press", "upress" ]) + + def _on_select_all_key_releases(self, widget): + self._select_by_op(["release", "urelease"]) + + def _on_select_all_commands(self, widget): + self._select_by_op("execute") + + def _on_select_all(self, widget): + self.__script_tree.get_selection().select_all() + + def _on_select_all_delays(self, widget): + self._select_by_op("delay") + + def _on_macro_operation_cursor_changed(self, widget): + pass + +# tree_selection = self.__script_tree.get_selection() +# _, selected_path = tree_selection.get_selected_rows() +# if len(selected_path) == 1: +# selected_index = selected_path[0][0] +# _,val,op,_ = self.__script_model[selected_index] +# print op,val +# +# if op == "press": +# for i in range(selected_index + 1, len(self.__macros)): +# _,row_val,row_op,_ = self.__script_model[i] +# if row_op == "delay": +# self._select_row(i) +# elif row_op == "release" and val == row_val: +# self._select_row(i) +# +# if i + 1 < len(self.__script_model) and \ +# self.__script_model[i + 1][2] == "delay": +# self._select_row(i + 1) +# +# break +# elif op == "release": +# if selected_index + 1 < len(self.__script_model) and \ +# self.__script_model[selected_index + 1][2] == "delay": +# self._select_row(selected_index + 1) +# +# for i in range(selected_index - 1, 0, -1): +# _,row_val,row_op,_ = self.__script_model[i] +# if row_op == "delay": +# self._select_row(i) +# elif row_op == "press" and val == row_val: +# self._select_row(i) +# break + + + def _select_by_op(self, show_ops): + tree_selection = self.__script_tree.get_selection() + tree_selection.unselect_all() + for idx, row in enumerate(self.__script_model): + _,_,op,_ = row + if isinstance(show_ops, list) and op in show_ops or op == show_ops: + tree_selection.select_path(self.__script_model.get_path(self.__script_model.get_iter_from_string("%d" % idx))) + + def _select_row(self, row): + self.__script_tree.get_selection().select_path(self.__script_model.get_path(self.__script_model.get_iter_from_string("%d" % row))) + +if __name__ == "__main__": + me = G15MacroEditor() + if (me.window): + me.window.connect("destroy", gtk.main_quit) + me.window.run() + me.window.hide() diff --git a/src/gnome15/g15network.py b/src/gnome15/g15network.py new file mode 100644 index 0000000..89d631d --- /dev/null +++ b/src/gnome15/g15network.py @@ -0,0 +1,79 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2012 Brett Smith +# +# 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 . + +''' +Classes and utilities for monitoring the current state of the network, allowing +plugins that declare "needs_network" to be enabled or disabled depending on this +state. + +This is done using the NetworkManager DBUS interface, although the number of +states available is reduced to connected/disconnected +''' + +import dbus + +# Logging +import logging +logger = logging.getLogger(__name__) + +_system_bus = dbus.SystemBus() + +NM_BUS_NAME = 'org.freedesktop.NetworkManager' +NM_OBJECT_PATH = '/org/freedesktop/NetworkManager' +NM_INTERFACE_NAME = 'org.freedesktop.NetworkManager' +NM_STATE_INDEX = { 0: 'Unknown', + 10: 'Asleep', + 20: 'Disconnected', + 30: 'Disconnecting', + 40: 'Connecting', + 50: 'Connected (Local)', + 60: 'Connected (Site)', + 70: 'Connected (Global)' } + +class NetworkManager(): + def __init__(self, screen): + self._screen = self + self.listeners = [] + self._state = -1 + try: + _manager = _system_bus.get_object(NM_BUS_NAME, NM_OBJECT_PATH) + self._interface = dbus.Interface(_manager, NM_INTERFACE_NAME) + self._set_state(self._interface.state()) + self._handle = self._interface.connect_to_signal('StateChanged', self._set_state) + except dbus.DBusException as e: + logger.warning("NetworkManager DBUS interface could not be contacted. All plugins will assume the network is available, and may behave unexpectedly.") + logger.debug("NetworkManager connection attempt below :", exc_info = e) + + # Assume connected + self._state = 70 + + def _set_state(self, state): + if state in NM_STATE_INDEX: + logger.info("New network state is %s", NM_STATE_INDEX[state]) + s = state + else: + logger.info("New network state is unknown") + s = 0 + if s != self._state and s in [ 0, 20, 60, 70 ]: + self._state = s + for l in self.listeners: + l(self.is_network_available()) + + def is_network_available(self): + return self._state in [ 60, 70 ] + + def is_internet_available(self): + return self._state == 70 diff --git a/src/gnome15/g15notify.py b/src/gnome15/g15notify.py new file mode 100644 index 0000000..dbd6e06 --- /dev/null +++ b/src/gnome15/g15notify.py @@ -0,0 +1,62 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2011 Brett Smith +# +# 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 . + +''' +Notifications +''' +import dbus +import g15globals + +# Logging +import logging +logger = logging.getLogger(__name__) + +_session_bus = dbus.SessionBus() + +class NotifyMessage(): + def __init__(self): + self.id = 0 + + def close(self): + logger.info("Closing notification %s", str(self.id)) + _get_obj().CloseNotification(self.id) + + def handle_reply(self, e): + self.id = int(e) + logger.debug("Got message ID %d", self.id) + + def handle_error(self, e): + logger.error("Error getting notification message ID. %s", str(e)) + +def _get_obj(): + return _session_bus.get_object("org.freedesktop.Notifications", '/org/freedesktop/Notifications') + +def notify(summary, body, icon = "", actions = [], hints = {}, timeout = 10.0, replaces = 0): + actions_array = dbus.Array(actions, signature='s') + hints_dict = dbus.Dictionary(hints, signature='sv') + msg = NotifyMessage() + _get_obj().Notify(g15globals.name, + replaces, + icon, + summary, + body, + actions_array, + hints_dict, + int(timeout * 1000), + dbus_interface = 'org.freedesktop.Notifications', + reply_handler = msg.handle_reply, + error_handler = msg.handle_error) + return msg \ No newline at end of file diff --git a/src/gnome15/g15plugin.py b/src/gnome15/g15plugin.py new file mode 100644 index 0000000..b7bcffc --- /dev/null +++ b/src/gnome15/g15plugin.py @@ -0,0 +1,426 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2011 Brett Smith +# +# 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 . + +import dbus +import util.g15scheduler as g15scheduler +import util.g15cairo as g15cairo +import util.g15icontools as g15icontools +import g15theme +import g15screen +import sys +import gobject + +class G15Plugin(): + + """ + Generic base plugin class + """ + def __init__(self, gconf_client, gconf_key, screen): + """ + Constructor + + Keyword arguments: + gconf_client - gconf client + gconf_key - gconf key for plugin + screen - screen + """ + self.screen = screen + self.gconf_client = gconf_client + self.gconf_key = gconf_key + self.active = False + self.__notify_handlers = [] + + def create_theme(self): + """ + Create a theme, using the currently selected theme for this plugin + if one is available. + """ + theme = self.gconf_client.get_string("%s/theme" % self.gconf_key) + new_theme = None + if theme: + theme_def = g15theme.get_theme(theme, sys.modules[self.__module__]) + if theme_def: + new_theme = g15theme.G15Theme(theme_def) + if not new_theme: + new_theme = g15theme.G15Theme(self) + new_theme.plugin = self + return new_theme + + def watch(self, key, callback): + """ + Watch for gconf changes for this plugin on a particular sub-key, calling + the callback when the value changes. All watches will be removed when + the plugin deactivates, so these should be added during the activate + phase. + + Keyword arguments: + key - sub-key (or None to monitor everything) + callback - function to call on change + """ + if isinstance(key, list): + for k in key: + self.watch(k, callback) + return + if key is not None and key.startswith("/"): + k = key + else: + k = "%s/%s" % (self.gconf_key, key) if key is not None else self.gconf_key + self.__notify_handlers.append(self.gconf_client.notify_add(k, callback)) + + def activate(self): + self.active = True + self.watch("theme", self._reactivate) + + def deactivate(self): + for h in self.__notify_handlers: + self.gconf_client.notify_remove(h); + self.active = False + + def destroy(self): + pass + + def _reactivate(self, client, connection_id, entry, args): + self.deactivate() + self.activate() + +class G15PagePlugin(G15Plugin): + + """ + Generic base plugin for plugins that want to contribute a page (most plugins + will extend this in some way) + """ + def __init__(self, gconf_client, gconf_key, screen, icon, page_id, title): + """ + Constructor + + Keyword arguments: + gconf_client - gconf client + gconf_key - gconf key for plugin + screen - screen + icon - icon to use for thumbnail + title - title for page (displayed in menu etc) + refresh_interval - how often to refresh the page + """ + G15Plugin.__init__(self, gconf_client, gconf_key, screen) + self.page_id = page_id + self.hidden = False + self._icon_path = g15icontools.get_icon_path(icon) + self._title = title + self.page = None + self.thumb_icon = g15cairo.load_surface_from_file(self._icon_path) + self.add_page_on_activate = True + + def activate(self): + G15Plugin.activate(self) + self.page = self.create_page() + self.populate_page() + if self.add_page_on_activate: + self.screen.add_page(self.page) + self.screen.redraw(self.page) + + def deactivate(self): + if self.page is not None: + self.screen.del_page(self.page) + self.page = None + G15Plugin.deactivate(self) + + def create_page(self): + return g15theme.G15Page(self.page_id, self.screen, + title = self._title, theme = self.create_theme(), + thumbnail_painter = self._paint_thumbnail, + theme_properties_callback = self.get_theme_properties, + theme_properties_attributes = self.get_theme_attributes, + painter = self._paint, + originating_plugin = self) + + def populate_page(self): + """ + Populate page. Subclasses may override to create or configure + additional components. + """ + pass + + def get_theme_properties(self): + """ + Get the properties to pass to the SVG theme file for rendering. Sub-classes + may override to provide more properties if needed. + + The subclass may return the same properties object with more properties added, + or a complete new one if the default properties are to be excluded. + + Keyword arguments: + properties -- properties + """ + properties = {} + properties["icon"] = self._icon_path + properties["title"] = self._title + properties["alt_title"] = "" + return properties + + def reload_theme(self): + """ + Reload the current theme + """ + self.page.set_theme(self.create_theme()) + + def _paint_thumbnail(self, canvas, allocated_size, horizontal): + if self.page != None and self.thumb_icon != None and self.screen.driver.get_bpp() == 16: + return g15cairo.paint_thumbnail_image(allocated_size, self.thumb_icon, canvas) + + def _paint_panel(self, canvas, allocated_size, horizontal): + pass + + def _paint(self, canvas): + pass + +class G15RefreshingPlugin(G15PagePlugin): + + """ + Base plugin class that may be used for plugins that refresh at set intervals. This + abstract class will take care of disabling the refresh while the page is not + visible + """ + + def __init__(self, gconf_client, gconf_key, screen, icon, page_id, title, refresh_interval = 1.0): + """ + Constructor + + Keyword arguments: + gconf_client - gconf client + gconf_key - gconf key for plugin + screen - screen + icon - icon to use for thumbnail + title - title for page (displayed in menu etc) + refresh_interval - how often to refresh the page + """ + G15PagePlugin.__init__(self, gconf_client, gconf_key, screen, icon, page_id, title) + self.refresh_interval = refresh_interval + self.session_bus = dbus.SessionBus() + self.timer = None + self.schedule_on_gobject = False + self.only_refresh_when_visible = True + + def create_page(self): + return g15theme.G15Page(self.page_id, + self.screen, + title = self._title, + theme = self.create_theme(), + thumbnail_painter = self._paint_thumbnail, + painter = self._paint, + panel_painter = self._paint_panel, + theme_properties_callback = self.get_theme_properties, + originating_plugin = self) + + def activate(self): + G15PagePlugin.activate(self) + self._schedule_refresh() + + def deactivate(self): + self._cancel_refresh() + G15PagePlugin.deactivate(self) + + def populate_page(self): + G15PagePlugin.populate_page(self) + self.refresh() + + def refresh(self): + """ + Sub-classes should implement and perform the recurring actions. There is no need to + to redraw the page, it is done automatically. + """ + pass + + def get_next_tick(self): + """ + Get how long to wait before the next refresh. By default this uses the 'refresh + interval', but sub-classes may override to provide custom tick logic. + """ + return self.refresh_interval + + def do_refresh(self): + """ + Programatically refresh. The timer will be reset + """ + self._cancel_refresh() + self._do_refresh() + self._schedule_refresh() + + + ''' Private + ''' + + def _reschedule_refresh(self): + self._cancel_refresh() + self._schedule_refresh() + + def _cancel_refresh(self): + if self.timer != None: + if isinstance(self.timer, int): + gobject.source_remove(self.timer) + else: + self.timer.cancel() + self.timer = None + + def _schedule_refresh(self): + if self.schedule_on_gobject: + self.timer = gobject.timeout_add(int(self.get_next_tick() * 1000), self._refresh) + else: + self.timer = g15scheduler.schedule("%s-Redraw" % self.page_id, + self.get_next_tick(), + self._refresh) + + def _refresh(self): + if self.page and (not self.only_refresh_when_visible or self.screen.is_visible(self.page)): + self._do_refresh() + self._reschedule_refresh() + + def _do_refresh(self): + self.refresh() + self.screen.redraw(self.page) + +class G15MenuPlugin(G15Plugin): + ''' + Base plugin class that may be used when the plugin just displays a single + menu style component. + ''' + + def __init__(self, gconf_client, gconf_key, screen, menu_title_icon, page_id, title, show_on_activate = True): + """ + Constructor + + Keyword arguments: + gconf_client - gconf client + gconf_key - gconf key for plugin + screen - screen + menu_title_icon - icon to use for thumbnail and the menu title + title - title for page (displayed in menu etc) + refresh_interval - how often to refresh the page + """ + G15Plugin.__init__(self, gconf_client, gconf_key, screen) + + self.page_id = page_id + self.page = None + self.hidden = False + self.menu = None + self.session_bus = dbus.SessionBus() + self._title = title + self._show_on_activate = show_on_activate + self.set_icon(menu_title_icon) + + def set_icon(self, icon): + self._icon_path = g15icontools.get_icon_path(icon) + self.thumb_icon = g15cairo.load_surface_from_file(self._icon_path) + + def activate(self): + G15Plugin.activate(self) + self.reload_theme() + if self._show_on_activate: + self.show_menu() + + def deactivate(self): + G15Plugin.deactivate(self) + if self.page != None: + self.hide_menu() + + def get_theme_properties(self): + """ + Get the properties to pass to the SVG theme file for rendering. Sub-classes + may override to provide more properties if needed. + + The subclass may return the same properties object with more properties added, + or a complete new one if the default properties are to be excluded. + + Keyword arguments: + properties -- properties + """ + properties = {} + properties["icon"] = self._icon_path + properties["title"] = self._title + properties["alt_title"] = "" + properties["no_items"] = self.menu.get_child_count() == 0 + return properties + + def reload_theme(self): + """ + Reload the SVG theme and configure it + """ + self.theme = g15theme.G15Theme(self, "menu-screen") + + def show_menu(self): + + """ + Create the component tree for the menu page and draw it + """ + if not self.active: + return + + self.page = self.create_page() + self.menu = self.create_menu() + self.page.set_focused_component(self.menu) + self.menu.focusable = True + self.page.on_deleted = self.page_deleted + self.menu.set_focused(True) + self.page.add_child(self.menu) + self.page.add_child(g15theme.MenuScrollbar("viewScrollbar", self.menu)) + self.load_menu_items() + self.add_to_screen() + + def add_to_screen(self): + """ + Add the page to the screen + """ + self.screen.add_page(self.page) + self.screen.redraw(self.page) + + def create_page(self): + """ + Create the page. Subclasses may override. + """ + return g15theme.G15Page(self.page_id, self.screen, priority=g15screen.PRI_NORMAL, title = self._title, theme = self.theme, \ + theme_properties_callback = self.get_theme_properties, + thumbnail_painter = self.paint_thumbnail, + originating_plugin = self) + + def page_deleted(self): + """ + Invoked when the page is removed from the screen + """ + self.page = None + + def create_menu(self): + """ + Create the menu component. Subclasses may override to create or configure + different components. + """ + return g15theme.Menu("menu") + + def hide_menu(self): + """ + Delete the page + """ + self.screen.del_page(self.page) + self.page = None + + def load_menu_items(self): + """ + Subclasses should override to set the initial menu items + """ + pass + + def paint_thumbnail(self, canvas, allocated_size, horizontal): + if self.page != None and self.thumb_icon != None and self.screen.driver.get_bpp() == 16: + return g15cairo.paint_thumbnail_image(allocated_size, self.thumb_icon, canvas) + \ No newline at end of file diff --git a/src/gnome15/g15pluginmanager.py b/src/gnome15/g15pluginmanager.py new file mode 100644 index 0000000..ee464e6 --- /dev/null +++ b/src/gnome15/g15pluginmanager.py @@ -0,0 +1,574 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2011 Brett Smith +# +# 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 . + +""" +This module handles the loading, starting, stopping and general management of +plugins. + +There are two types of plugins supported :- + +Device Plugins - These are plugins that require an actual keyboard device + to work. For example, they might add a new screen, + or listen for key events. There may be one instance of + each plugin per connected device. + +Global Plugins - These are plugins that are not tied to any specific device. + Only one instance may be running at a time. + +Plugins are looked for in a number of locations. + +* g15globals.plugin_dir - This is where official plugins installed with Gnome15 reside +* $XDG_DATA_HOME/gnome15/plugins - This is where users can put their own local plugins +* $XDG_CONFIG_HOME/gnome15/plugins - This is a deprecated place where users can put + their own local plugins. It will be removed on a future + version of Gnome15 +* $G15_PLUGINS_DIR - If it exists allows custom locations to be added +* g15pluginmanager.extra_plugin_dirs - Allows other plugins to dynamically register new plugin + locations + +The lifecycle of all plugins consists of 5 stages. + +1. Loading - When the python module is loaded. This happens to all plugins, +regardless of whether they are enabled or not. Any plugins that fail this +stage will not be visible. + +2. Initialise - This is when the plugin instance is created. All enabled +plugins will go through this stage *once*. If a plugin is de-activated, and +then re-activated, it will not be re-initialised unless the device it is +attached to is completely stopped (not necessarily because of shutdown). + +3. Activation - Occurs during start-up of all enabled plugins. If a plugin is +de-activated, and then re-activated. The activate() function is called again. + +4. De-activation - Occurs when the plugin is de-activated for some reason. +This may be because the user disabled it, or if the device is attached to is +stopped, or when Gnome15 itself is shutting down. + +5. Destruction - Occurs when the device the plugin is attached to is stopped, +or if Gnome15 itself is closing down. + +""" + +import os.path +import sys +import g15globals +import g15driver +import g15actions +import gconf +import threading + +# Logging +import logging +logger = logging.getLogger(__name__) + +imported_plugins = [] + +""" +This list may be added to dynamically to add new plugin locations +""" +extra_plugin_dirs = [] + +# Plugin manager states +UNINITIALISED = 0 +STARTING = 1 +STARTED = 2 +ACTIVATING = 3 +ACTIVATED = 4 +DEACTIVATING = 5 +DEACTIVATED = 6 +DESTROYING = 7 + +def list_plugin_dirs(path): + """ + List all plugin directories in a given path. + + Keyword arguments: + path -- path to look for plugins + """ + plugindirs = [] + if os.path.exists(path): + for p_dir in os.listdir(path): + plugin_path = os.path.join(path, p_dir) + if os.path.isdir(plugin_path): + plugindirs.append(os.path.realpath(plugin_path)) + else: + logger.debug("Plugin path %s does not exist.", path) + return plugindirs + +def get_extra_plugin_dirs(): + """ + Get a list of all directories plugin directories may be found in. This + will included any dynamically registered using the + g15pluginmanager.extra_plugin_dirs list, and all paths that are found + in the G15_PLUGINS environment variable. + """ + plugindirs = [] + plugindirs += extra_plugin_dirs + if "G15_PLUGINS" in os.environ: + for p_dir in os.environ["G15_PLUGINS"].split(":"): + plugindirs += list_plugin_dirs(p_dir) + return plugindirs + +def get_module_for_id(module_id): + """ + Get a plugin module given it's ID. + + Keyword arguments: + module_id -- plugin module ID + """ + for mod in imported_plugins: + if mod.id == module_id: + return mod + +def get_supported_models(plugin_module): + """ + Get a list of models that a plugin supports. This takes into account + the supported_models and unsupported_models attributes to provide a list + of the actual model ID's that can be used. See g15driver.MODLES and other + contants. + + Keyword arguments: + plugin_module -- plugin module instance + """ + supported_models = [] + getattr(plugin_module, 'supported_models', g15driver.MODELS) + unsupported_models = getattr(plugin_module, 'unsupported_models', []) + for p in unsupported_models: + if p in supported_models: + supported_models.remove(p) + else: + logger.debug("Tried to remove '%s' not in supported_models. Ignoring...", p) + return supported_models + +def is_needs_network(plugin_module): + """ + Get if the provided plugin_module instance requires the network to be available. + If the plugin doesn't declare this, it is assumed to be False + + Keyword arguments: + plugin_module -- plugin module instance + """ + return getattr(plugin_module, 'needs_network', False) + +def is_default_enabled(plugin_module): + """ + Get if the provided plugin_module instance should be enabled by default. + This is used to determine a basic list of plugins to get the user going + when Gnome15 is first installed. + + Keyword arguments: + plugin_module -- plugin module instance + """ + return getattr(plugin_module, 'default_enabled', False) + +def is_global_plugin(plugin_module): + """ + Get if the provided plugin_module instance should is a "Global Plugin". + + Keyword arguments: + plugin_module -- plugin module instance + """ + return getattr(plugin_module, 'global_plugin', False) + +def is_passive_plugin(plugin_module): + """ + Get if the provided plugin_module instance should is a "Passive Plugin". + Passive plugins just provide additional classes and functions, probably + for another plugin. They are always enabled. + + Keyword arguments: + plugin_module -- plugin module instance + """ + return getattr(plugin_module, 'passive', False) + +def get_actions(plugin_module, device): + """ + Get a dictionary of all the "Actions" this plugin uses. The key is + the action ID, and the value of a textual description of what the action + is used for in this plugin. + + Keyword arguments: + plugin_module -- plugin module instance + device -- device the plugins are for + """ + actions = {} + # First look for actions for the specific device + if device is not None: + actions = getattr(plugin_module, 'actions_%s' % device.model_id, {}) + + if actions == {}: + return getattr(plugin_module, 'actions', {}) + else: + return actions + + + +""" +Loads the python modules for all plugins for all known locations. This is done +in two phases. + +Firstly, the paths of all plugins are added to the python search path. + +Secondly, all of these directories are scanned for python files with the same +name as the directory they are in. Each one of these is the main plugin module. + +TODO - These should really be using __init__.py +""" +all_plugin_directories = get_extra_plugin_dirs() + \ + list_plugin_dirs(os.path.expanduser("~/.gnome15/plugins")) + \ + list_plugin_dirs(os.path.join(g15globals.user_config_dir, "plugins")) + \ + list_plugin_dirs(os.path.join(g15globals.user_data_dir, "plugins")) + \ + list_plugin_dirs(g15globals.plugin_dir) + +# Phase 1 +for plugindir in all_plugin_directories: + if not plugindir in sys.path: + sys.path.insert(0, plugindir) + +# Phase 2 +for plugindir in all_plugin_directories: + plugin_name = os.path.basename(plugindir) + pluginfiles = [fname[:-3] for fname in os.listdir(plugindir) if fname == plugin_name + ".py"] + if not plugindir in sys.path: + sys.path.insert(0, plugindir) + try : + for mod in ([__import__(fname) for fname in pluginfiles]): + imported_plugins.append(mod) + # TODO - we need to be registering actions for a particular device + actions = get_actions(mod, None) + for a in actions: + if not a in g15actions.actions: + g15actions.actions.append(a) + except Exception as e: + logger.error("Failed to load plugin module %s.", plugindir, exc_info = e) + + +class G15Plugins(): + """ + Managed a set of plugins for either the global set, or the per device + set (in this case the screen argument must be provided). + + In total there will be n+1 instances of this, where n is the number of + connected and enabled devices. + """ + def __init__(self, screen, service=None, network_manager = None): + """ + Create a new plugin manager either for the provided device (screen), + or globally (when screen is None) + + Keyword arguments: + screen -- screen this plugin managed is attached to, or None for global plugins + service -- the service this plugin manager is managed by + """ + self.network_manager = network_manager + self.lock = threading.RLock() + self.screen = screen + self.service = service if service is not None else screen.service + self.conf_client = self.service.conf_client + self.started = [] + self.activated = [] + self.conf_client.add_dir(self._get_plugin_key(), gconf.CLIENT_PRELOAD_NONE) + self.module_map = {} + self.plugin_map = {} + self.state = UNINITIALISED + + def is_activated(self): + """ + Get if the plugin manager is currently fully ACTIVATED. + """ + return self.state == ACTIVATED + + def is_started(self): + """ + Get if the plugin manager is currently fully STARTED or in any ACTIVE + state. + """ + return self.is_in_active_state() or self.state == STARTED + + def is_in_active_state(self): + """ + Get if the plugin manager is currently in a state where it is either + fully ACTIVATED, or partially activated (ACTIVATING, DEACTIVATING). + """ + return self.state in [ ACTIVATED, DEACTIVATING, ACTIVATING ] + + def is_in_started_state(self): + """ + Get if the plugin manager is currently in a state where it is either + fully STARTED (or any activated state) or partially STARTED (STARTING, STOPPING) + """ + return self.is_in_active_state() or self.state in [ STARTED, STARTING, DESTROYING ] + + def has_plugin(self, module_id): + """ + Get if the plugin manager contains a plugin install with the + given plugin module ID. + + Keyword arguments: + module_id -- plugin module ID to search for + """ + return module_id in self.module_map + + def get_plugin(self, module_id): + """ + Get the plugin instance given the plugin module's ID + + Keyword arguments: + id -- plugin module ID to search for + """ + if module_id in self.module_map: + return self.module_map[module_id] + + def start(self): + """ + Start all plugins that are currently enabled. + """ + self.lock.acquire() + try : + self.state = STARTING + self.started = [] + added = [] + for mod in imported_plugins: + plugin_dir_key = self._get_plugin_key(mod.id) + if mod.id in added: + logger.warning("Same plugin with ID of %s is already loaded." \ + "Only the first copy will be used.", mod.id) + else: + self.conf_client.add_dir(plugin_dir_key, gconf.CLIENT_PRELOAD_NONE) + key = "%s/enabled" % plugin_dir_key + self.conf_client.notify_add(key, self._plugin_changed) + if (self.screen is None and is_global_plugin(mod)) or \ + (self.screen is not None and not is_global_plugin(mod)): + + # If first use, set the default enabled state + if self.conf_client.get(key) == None: + self.conf_client.set_bool(key, is_default_enabled(mod)) + + # Only actually activate if the plugin is not passive and the network + # is in the right state + + if self.conf_client.get_bool(key) and \ + not is_passive_plugin(mod): + try : + instance = self._create_instance(mod, plugin_dir_key) + if self.screen is None or self.screen.driver.get_model_name() in get_supported_models(mod): + self.started.append(instance) + except Exception as e: + self.conf_client.set_bool(key, False) + logger.error("Failed to load plugin %s.", mod.id, exc_info = e) + self.state = STARTED + except Exception as a: + self.state = UNINITIALISED + logger.debug("Error when starting plugins", exc_info = a) + raise a + finally: + self.lock.release() + logger.info("Started plugin manager") + + def handle_key(self, key, state, post=False): + """ + Pass the provided key event to all plugins. For each key event, this + will be called twice, once with post=False, and once with post=True + + Keyword arguments: + key -- key name + state -- key state + post -- post processing stage + """ + for plugin in self.started: + can_handle_keys = hasattr(plugin, 'handle_key') + if can_handle_keys and plugin.handle_key(key, state, post): + logger.info("Plugin %s handled key %s (%d), %s", + str(plugin), + str(key), + state, + str(post)) + return True + return False + + def activate(self, callback=None, plugin=None): + """ + Activate all plugins that currently started. + + Keyword arguments: + callback -- callback function to invoke when each invididual plugin + is activated. This is used for the progress bar during initial startup. + plugin -- If None, all started are activated. If a list, + those plugins are activated. If a single plugin, + that plugin is activated. If plugin is either list + or None, the state of the plugin manager will also + be changed + """ + + if plugin is None or isinstance(plugin, list): + logger.info("Activating plugins") + self.lock.acquire() + try : + self.state = ACTIVATING + self.activated = [] + idx = 0 + for plugin in plugin if isinstance(plugin, list) else self.started: + mod = self.plugin_map[plugin] + + # Only actually activate if the plugin is not passive and the network + # is in the right state + + needs_net = is_needs_network(mod) + if not needs_net or ( needs_net and \ + self.network_manager.is_network_available() ): + self._activate_instance(plugin, callback, idx) + + idx += 1 + self.state = ACTIVATED + except Exception as e: + self.state = STARTED + logger.debug("Error while activating plugin", exc_info = e) + raise e + finally: + self.lock.release() + logger.debug("Activated plugins") + else: + self._activate_instance(plugin, callback, 0) + + + def deactivate(self, plugin=None): + """ + De-activate plugins that are currently activated. + + Keyword arguments: + plugin -- If None, all activated are deactivated. If a list, + those plugins are deactivated. If a single plugin, + that plugin is deactivated. If plugin is either list + or None, the state of the plugin manager will also + be changed + """ + if plugin is None or isinstance(plugin, list): + logger.info("De-activating plugins") + self.lock.acquire() + try : + self.state = DEACTIVATING + for plugin in plugin if isinstance(plugin, list) else list(self.activated): + self._deactivate_instance(plugin) + finally: + self.state = DEACTIVATED + self.lock.release() + logger.info("De-activated plugins") + else: + self._deactivate_instance(plugin) + + def destroy(self): + """ + Destroy all plugins that are currently started. + """ + try : + self.state = DESTROYING + for plugin in self.started: + self.state = DESTROYING + self.started.remove(plugin) + plugin.destroy() + finally: + self.state = UNINITIALISED + + ''' + Private + ''' + def _deactivate_instance(self, plugin): + mod = self.plugin_map[plugin] + logger.debug("De-activating %s", mod.id) + if not plugin in self.activated: + raise Exception("%s is not activated" % mod.id) + try : + plugin.deactivate() + except Exception as e: + logger.warning("Failed to deactive plugin properly.", exc_info = e) + finally: + mod_id = self.plugin_map[plugin].id + if mod_id in self.service.active_plugins: + del self.service.active_plugins[mod_id] + self.activated.remove(plugin) + + def _get_plugin_key(self, subkey=None): + folder = self.screen.device.uid if self.screen is not None else "global" + if subkey: + return "/apps/gnome15/%s/plugins/%s" % (folder, subkey) + else: + return "/apps/gnome15/%s/plugins" % folder + + def _plugin_changed(self, client, connection_id, entry, args): + self.lock.acquire() + if self.screen is not None: + self.screen._check_active_plugins() + try : + path = entry.key.split("/") + plugin_id = path[5] + now_enabled = entry.value.get_bool() + plugin = get_module_for_id(plugin_id) + + # Check network state, and prevent enable if not in right state + needs_net = is_needs_network(plugin) + if now_enabled and needs_net and not self.network_manager.is_network_available(): + now_enabled = False + + instance = None + if plugin_id in self.module_map: + instance = self.module_map[plugin_id] + + if not is_passive_plugin(plugin): + if now_enabled and instance == None: + instance = self._create_instance(plugin, self._get_plugin_key(plugin_id)) + self.started.append(instance) + if self.is_in_active_state() == True: + self._activate_instance(instance) + elif not now_enabled and instance != None: + if instance in self.activated: + self._deactivate_instance(instance) + if instance in self.started: + self.started.remove(instance) + del self.module_map[plugin_id] + instance.destroy() + finally: + self.lock.release() + + def _activate_instance(self, instance, callback=None, idx=0): + mod = self.plugin_map[instance] + logger.info("Activating %s", mod.id) + try : + if self._is_single_instance(mod): + logger.info("%s may only be run once, checking if there is another instance", mod.id) + if mod.id in self.service.active_plugins: + raise Exception("Plugin may %s only run on one device at a time." % mod.id) + if callback != None: + callback(idx, len(self.started), mod.name) + instance.activate() + self.service.active_plugins[mod.id] = True + self.activated.append(instance) + except Exception as e: + logger.error("Failed to activate plugin %s.", mod.id, exc_info = e) + self.conf_client.set_bool(self._get_plugin_key("%s/enabled" % mod.id), False) + + def _is_single_instance(self, module): + return getattr(module, 'single_instance', False) + + def _create_instance(self, module, key): + logger.info("Loading %s", module.id) + if self.screen is not None: + instance = module.create(key, self.conf_client, screen=self.screen) + else: + instance = module.create(key, self.conf_client, service=self.service) + self.module_map[module.id] = instance + self.plugin_map[instance] = module + logger.info("Loaded %s", module.id) + return instance + diff --git a/src/gnome15/g15profile.py b/src/gnome15/g15profile.py new file mode 100644 index 0000000..bedac16 --- /dev/null +++ b/src/gnome15/g15profile.py @@ -0,0 +1,1173 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2010 Brett Smith +# +# 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 . + +""" +This module contains the classes required for accessing macro details, as +well as the functions to load and save new profiles. + +A number of utility functions are also supplied to do things such as +getting the default or active profile. +""" + +import gconf +import time +import util.g15convert as g15convert +import util.g15gconf as g15gconf +import util.g15os as g15os +import util.g15icontools as g15icontools +import g15globals +import g15actions +import g15devices +import g15uinput +import g15driver +import ConfigParser +import codecs +import os.path +import stat +import pyinotify +import logging +import re +import zipfile +from cStringIO import StringIO + +logger = logging.getLogger(__name__) +active_profile = None +conf_client = gconf.client_get_default() + +''' +Watch for changes in macro configuration directory. +Observers can add a callback function to profile_listeners +to be informed when macro profiles change +''' +profile_listeners = [] + +wm = pyinotify.WatchManager() +mask = pyinotify.IN_DELETE | pyinotify.IN_MODIFY | pyinotify.IN_CREATE | pyinotify.IN_ATTRIB # watched events + +# Create macro profiles directory +conf_dir = os.path.join(g15globals.user_config_dir, "macro_profiles") +g15os.mkdir_p(conf_dir) + +class EventHandler(pyinotify.ProcessEvent): + """ + Event handle the listens for the inotify events and informs all callbacks + that are registered in the profile_listeners variable + """ + + def _get_profile_ids(self, event): + path = os.path.basename(event.pathname) + device_uid = os.path.basename(os.path.dirname(event.pathname)) + if path.endswith(".macros") and not path.startswith("."): + id_no = path.split(".")[0] + return ( id_no, device_uid ) + + def _notify(self, event): + ids = self._get_profile_ids(event) + if ids: + for profile_listener in profile_listeners: + profile_listener(ids[0], ids[1]) + + def process_IN_MODIFY(self, event): + self._notify(event) + + def process_IN_CREATE(self, event): + self._notify(event) + + def process_IN_ATTRIB(self, event): + self._notify(event) + + def process_IN_DELETE(self, event): + self._notify(event) + +notifier = pyinotify.ThreadedNotifier(wm, EventHandler()) +notifier.name = "ProfilePyInotify" +notifier.setDaemon(True) +notifier.start() +wdd = wm.add_watch(conf_dir, mask, rec=True) + + +''' +Macro types. +''' + +MACRO_COMMAND="command" +MACRO_SIMPLE="simple" +MACRO_SCRIPT="script" +MACRO_MOUSE=g15uinput.MOUSE +MACRO_JOYSTICK=g15uinput.JOYSTICK +MACRO_DIGITAL_JOYSTICK=g15uinput.DIGITAL_JOYSTICK +MACRO_KEYBOARD=g15uinput.KEYBOARD +MACRO_ACTION="action" + +''' +Repeat modes +''' +REPEAT_TOGGLE="toggle" +NO_REPEAT="none" +REPEAT_WHILE_HELD="held" + +''' +Plugin modes +''' +NO_PLUGINS = "none" +ALL_PLUGINS = "all" +SELECTED_PLUGINS = "selected" + +""" +Defaults +""" +DEFAULT_REPEAT_DELAY = -1.0 + + +__profile_dirs = [] + +def add_profile_dir(profile_dir): + ''' + Add a new location to search for macro profiles. This allows plugins to + register their own directories and contribute new profiles. + + profile_dir -- profile directory to register + ''' + __profile_dirs.append(profile_dir) + +def remove_profile_dir(profile_dir): + ''' + Remove a location being search for macro profiles. This allows plugins to + de-register their own directories and stop contributing new profiles. + + profile_dir -- profile directory to de-register + ''' + __profile_dirs.remove(profile_dir) + +def get_profile_by_name(device, name): + """ + Get a profile given it's name. If there is more than one profile with + the same name, the first will be return. If no profile is found, None + will be returned + + Keyword arguments: + device -- device associated with profile + name -- profile name to find + """ + for profile in get_profiles(device): + if profile.name == name: + return profile + +def get_profiles(device): + ''' + Get list of all configured macro profiles for the specified device. + + Keyword arguments: + device -- device associated with profiles + ''' + profiles = [] + for profile_dir in get_all_profile_dirs(device): + if os.path.exists(profile_dir): + for profile in os.listdir(profile_dir): + if not profile.startswith(".") and profile.endswith(".macros"): + profile_id = ".".join(profile.split(".")[:-1]) + profile_object = G15Profile(device, profile_id, \ + file_path = "%s/%s" % \ + ( profile_dir, profile )) + if device.model_id in profile_object.models: + profiles.append(profile_object) + + if len(profiles) == 0: + return [ create_default(device) ] + + return profiles + +def get_all_profile_dirs(device): + """ + Get a list of all the directories profiles are searched for in. + + Keyword arguments: + device -- device + """ + dirs = list(__profile_dirs) + dirs.append(get_profile_dir(device)) + return dirs + +def create_default(device): + """ + Create the default profile for the specified device. + + Keyword arguments: + device -- device associated with default profile + """ + if not get_default_profile(device): + logger.info("No default macro profile. Creating one") + default_profile = G15Profile(device, profile_id = "Default") + default_profile.name = "Default" + default_profile.device = device + default_profile.activate_on_focus = True + default_profile.activate_on_launch = False + create_profile(default_profile) + wdd = wm.add_watch(conf_dir, mask, rec=True) + return get_default_profile(device) + +def create_profile(profile): + """ + Assign a profile object an ID, and save it to disk + + Keyword arguments: + profile -- profile to save + """ + if profile.id == None or profile.id == -1: + profile.set_id(generate_profile_id()) + logger.info("Creating profile %s, %s", profile.id, profile.name) + profile.save() + + +def generate_profile_id(): + return long(time.time()) + +def get_profile(device, profile_id): + """ + Get a profile given the device it is associated with and it's ID. The + profile will be fully loaded on return. The object returned will be a + new instance. + + Keyword arguments: + device -- device associated with profile + profile_id -- ID of profile to load + """ + for profile_dir in get_all_profile_dirs(device): + path = "%s/%s.macros" % ( profile_dir, profile_id ) + if os.path.exists(path): + return G15Profile(device, profile_id, file_path = path); + +def get_active_profile(device): + """ + Get the currently active profile for the specified device. This will + be retrieved from the configuration backend. + + Keyword arguments: + device -- device associated with profile + """ + val= conf_client.get("/apps/gnome15/%s/active_profile" % device.uid) + profile = None + if val != None and val.type == gconf.VALUE_INT: + # This is just here for compatibility with <= 0.7.x + profile = get_profile(device, str(val.get_int())) + elif val != None and val.type == gconf.VALUE_STRING: + profile = get_profile(device, val.get_string()) + + if profile is None: + profile = get_default_profile(device) + + if profile is None: + profile = create_default(device) + profile.make_active() + + return profile + +def is_locked(device): + """ + Get if the active profile is "locked" or if it may be changed for the specified device. + + Keyword arguments: + device -- device associated with profile + """ + return g15gconf.get_bool_or_default(conf_client, "/apps/gnome15/%s/locked" % device.uid, False) + +def set_locked(device, locked): + """ + Set if the active profile is 'locked', or if it may be changed + for the specified device. + + Keyword arguments: + device -- device associated with profile + locked -- lock statue + """ + conf_client.set_bool("/apps/gnome15/%s/locked" % device.uid, locked) + +def get_default_profile(device): + """ + Get the default profile for the specified device. + + Keyword arguments: + device -- device associated with default profile + """ + old_default = get_profile(device, "0") + if old_default is not None: + return old_default + return get_profile(device, "Default") + +def get_keys_from_key(key_list_key): + """ + Utility function to convert the string format of the list of keys used + in profile storage into a list of key codes as defined in g15driver + + Keyword arguments: + key_list_key -- string of key sequence required to activate macro + """ + return key_list_key.split("_") + +def get_keys_key(keys): + """ + Utility function function to convert the list of key codes as defined + in g15driver into the key list string used in profile storage. + + Keyword arguments: + keys -- list of key codes to convert to string + """ + return "_".join(keys) + +def get_profile_dir(device): + """ + Get the directory profiles for a particular device are stored. + + Keyword arguments: + device -- device + """ + return "%s/%s" % (conf_dir, device.uid) + + +def is_uinput_type(macro_type): + """ + Get if the macro type is a uinput mapping type + + Keyword arguments: + macro_type -- macro type + """ + return macro_type in [ MACRO_MOUSE, \ + MACRO_KEYBOARD, \ + MACRO_JOYSTICK, \ + MACRO_DIGITAL_JOYSTICK ] + +def find_profile_for_command(args, device): + """ + Searchs for a profile that is associated with a particular command. When + the command is launched through g15-launch, the desktop service will + call the function to find the profile to launch the command under. See + G15Profile.launch() for details on launching applications through + Gnome15. + + Keyword arguments: + device -- device + args -- list of arguments the command was launched with. The + first argument is the either the executable name (when the + executable is on the PATH, or the full path) + full path to the executable + """ + + + """ + First reformat the arguments so they are all wrapped in single quotes. + They shell that called g15-launch would have already expanded any + variables or filepaths that exist, so let's use single quotes + """ + command_line = "" + for a in args: + if len(command_line) > 0: + command_line += " " + command_line += "'" + a + "'" + + logger.info("Processed command '%s'", command_line) + + for p in get_profiles(device): + if p.can_launch(command_line): + return p + +def to_key_state_name(key_state_id): + """ + Return an english representation of a key state code + + Keyword arguments: + key_state_id -- key state ID (g15driver.KEY_STATE_UP, .. DOWN and HELD) + """ + return "Up" if key_state_id == g15driver.KEY_STATE_UP else \ + ( "Down" if key_state_id == g15driver.KEY_STATE_DOWN else "Held" ) + +def clone_macro(macro): + """ + Clone a macro + + Keyword arguments: + macro -- macro to clone + """ + m = G15Macro(macro.profle, macro.memory, macro.key_list_key, macro.activate_on) + m.name = macro.name + m.macro = macro.macro + m.repeat_mode = macro.repeat_mode + m.type = macro.type + m.repeat_delay = macro.repeat_delay + return m + + +class G15Macro(object): + """ + Represents a single macro in a profile. A macro defines how it's used + using the 'type', which may be one of MACRO_COMMAND, MACRO_SIMPLE, + MACRO_SCRIPT, MACRO_MOUSE, MACRO_JOYSTICK, MACRO_DIGITAL_JOYSTICK, + MACRO_KEYBOARD or MACRO_ACTION + """ + def __init__(self, profile, memory, key_list_key, activate_on): + """ + Constructor + + Keyword arguments: + profile -- parent profile object + memory -- memory bank this macro exists in + key_list_key -- string representation of keys required to activate macro + activate_on -- whether to activate on RELEASE or when HELD + """ + if profile is None: + raise Exception("No profile provided") + + self.profile = profile + self.memory = memory + self.key_list_key = key_list_key + self.activate_on = activate_on + + self.keys = key_list_key.split("_") + self.name = "" + self.macro = "" + self.repeat_mode = REPEAT_WHILE_HELD + self.type = MACRO_SCRIPT + self.repeat_delay = DEFAULT_REPEAT_DELAY + section_name = "m%d" % self.memory + if not self.profile.parser.has_section(section_name): + self.profile.parser.add_section(section_name) + + def is_uinput(self): + """ + Get if the macro type is a uinput mapping type + + Keyword arguments: + macro_type -- macro type + """ + return is_uinput_type(self.type) + + def compare(self, o): + """ + Compare this macro with another for sorting purposes. Macros will + be ordered with the G keys being first in numeric order, followed by + the memory bank keys in number order (MR is last), followed by the + 'L1' - 'L5' keys and finally all other keys ordered alphabetically. + + Keyword arguments: + o -- macro to compare this macro to + """ + return self._get_total(self.keys) - self._get_total(o.keys) + + def get_uinput_code(self): + """ + Get the uinput code of the key this macro is mapped to. If this + macro is not of a type that maps to a uinput key, an exception + will be thrown + """ + if not self.type in [ MACRO_MOUSE, MACRO_KEYBOARD, MACRO_JOYSTICK, MACRO_DIGITAL_JOYSTICK ]: + raise Exception("Macro of type %s, is not a type that maps to a uinput code." % self.type) + return g15uinput.capabilities[self.macro][1] if self.macro in g15uinput.capabilities else 0 + + def set_keys(self, keys): + """ + Set the list of keys this macro requires to activate. + + Keyword arguments: + keys -- list of keys required to activate macro + """ + section_name = "m%d" % self.memory + self.profile._delete_key(section_name, self.key_list_key) + self.keys = keys + self.key_list_key = get_keys_key(keys) + + def save(self): + """ + Save this macro. This triggers the whole profile that contains the + macro to be saved as well. + """ + self._store() + self.profile.save() + + def delete(self): + """ + Delete this macro + """ + self.profile.delete_macro(self.memory, self.key_list_key) + + def set_activate_on(self, new_activate_on): + """ + Changes the Activate On mode (i.e. when released or when held). This + function should be used rather than just modifying the property, as + the parent profile needs to be adjusted as well + + Keyword arguments: + new_activate_on -- new activate on ID + """ + current_list = self.profile.macros[self.activate_on][self.memory - 1] + current_list.remove(self) + self.profile._delete_key(self._get_section_name(), self.key_list_key) + self.activate_on = new_activate_on + self.profile.macros[self.activate_on][self.memory - 1].append(self) + + + """ + Private + """ + + def _remove_option(self, section_name, option_key): + if self.profile.parser.has_option(section_name, option_key): + self.profile.parser.remove_option(section_name, option_key) + + def _get_section_name(self): + return self.profile._get_section_name(self.activate_on, self.memory) + + def _store(self): + section_name = self._get_section_name() + pk = "keys_%s" % self.key_list_key + self.profile.parser.set(section_name, "%s_name" % pk, self._encode_val(self.name)) + self.profile.parser.set(section_name, "%s_type" % pk, self.type) + + if self.repeat_mode == REPEAT_WHILE_HELD: + self.profile._remove_if_exists("%s_repeatmode" % pk, section_name) + else: + self.profile.parser.set(section_name, "%s_repeatmode" % pk, self.repeat_mode) + if self.repeat_delay == -1: + self.profile._remove_if_exists("%s_repeatdelay" % pk, section_name) + else: + self.profile.parser.set(section_name, "%s_repeatdelay" % pk, self.repeat_delay) + + if self.profile.version == 1.0: + + if self.type in [ MACRO_KEYBOARD, MACRO_JOYSTICK, MACRO_DIGITAL_JOYSTICK, MACRO_MOUSE ]: + self.profile.parser.set(section_name, "%s_type" % pk, "mapped-to-key") + self.profile.parser.set(section_name, "%s_maptype" % pk, self.type) + self.profile.parser.set(section_name, "%s_mappedkey" % pk, self.macro) + self.profile._remove_if_exists("%s_command" % pk, section_name) + self.profile._remove_if_exists("%s_simplemacro" % pk, section_name) + self.profile._remove_if_exists("%s_macro" % pk, section_name) + self.profile._remove_if_exists("%s_action" % pk, section_name) + else: + self.profile._remove_if_exists("%s_mappedkey" % pk, section_name) + self.profile._remove_if_exists("%s_maptype" % pk, section_name) + if self.type == MACRO_COMMAND: + self.profile.parser.set(section_name, "%s_command" % pk, self._encode_val(self.macro)) + else: + self.profile._remove_if_exists("%s_command" % pk, section_name) + if self.type == MACRO_SIMPLE: + self.profile.parser.set(section_name, "%s_simplemacro" % pk, self._encode_val(self.macro)) + else: + self.profile._remove_if_exists("%s_simplemacro" % pk, section_name) + if self.type == MACRO_SCRIPT: + self.profile.parser.set(section_name, "%s_macro" % pk, self._encode_val(self.macro)) + else: + self.profile._remove_if_exists("%s_macro" % pk, section_name) + + """ + Actions aren't actually supported in < 0.8, but store it in it's + own field anyway. Earlier versions will just not support that + macro + """ + if self.type == MACRO_ACTION: + self.profile.parser.set(section_name, "%s_action" % pk, self._encode_val(self.macro)) + else: + self.profile._remove_if_exists("%s_action" % pk, section_name) + else: + """ + Store in the new more compact version 2.0 format + """ + self.profile.parser.set(section_name, "%s_macro" % pk, self._encode_val(self.macro)) + self.profile._remove_if_exists("%s_maptype" % pk, section_name) + self.profile._remove_if_exists("%s_mappedkey" % pk, section_name) + self.profile._remove_if_exists("%s_command" % pk, section_name) + self.profile._remove_if_exists("%s_simplemacro" % pk, section_name) + self.profile._remove_if_exists("%s_action" % pk, section_name) + + def _encode_val(self, val): + val = val.encode('utf8') + return val + + def _decode_val(self, val): + return val + + def _load(self): + self.type = self._get("type", MACRO_SCRIPT) + self.macro = self._decode_val(self._get("macro", "")) + self.name = self._decode_val(self._get("name", "")) + self.repeat_mode = self._decode_val(self._get("repeatmode", REPEAT_WHILE_HELD)) + self.repeat_delay = float(self._get("repeatdelay", DEFAULT_REPEAT_DELAY)) + if self.type == "mapped-to-key": + self.macro = self._get("mappedkey", "") + self.type = self._get("maptype", "") + elif self.profile.version == 1.0: + if self.type == MACRO_COMMAND: + self.macro = self._decode_val(self._get("command", "")) + elif self.type == MACRO_SIMPLE: + self.macro = self._decode_val(self._get("simplemacro", "")) + elif self.type == MACRO_ACTION: + """ + Actions aren't actually supported in < 0.8, but this is how + it's stored when the profile is in 1.0 mode. + """ + self.macro = self._decode_val(self._get("action", "")) + + def _get(self, key, default_value): + section_name = self._get_section_name() + option_key = "keys_" + self.key_list_key + "_" + key + return self.profile.parser.get(section_name, option_key) if self.profile.parser.has_option(section_name, option_key) else default_value + + def __ne__(self, macro): + return not self.__eq__(macro) + + def __eq__(self, macro): + try: + return macro is not None and self.profile.id == macro.profile.id and self.key_list_key == macro.key_list_key and self.activate_on == macro.activate_on + except AttributeError as e: + logger.debug("Error when reading a macro attribute", exc_info = e) + return False + + def _get_total(self, keys): + t = 0 + for i in range(0, len(keys)): + if keys[i] != "": + t += self._get_key_val(keys[i]) + return t + + def _get_key_val(self, key): + if(key == ""): + return 0 + elif re.match("g[0-9]+.*", key): + return int(key[1:]) + elif re.match("m[1-3]", key): + return 50 + int(key[1:]) + elif key == "mr": + return 55 + elif re.match("l[0-9]+.*", key): + return 100 + int(key[1:]) + else: + ki = self.profile.device.get_key_index(key) + if ki is None: + ki = 200 + return 200 + ki + + def __repr__(self): + return "[Macro %d/%s (%s) [%s]" % ( self.memory, self.name, self.key_list_key, to_key_state_name(self.activate_on) ) + +class G15Profile(object): + """ + Encapsulates a single macro profile with 3 memory banks. This object + contains all the general information about the profile, as well as the + list of macros themselves. + """ + + def __init__(self, device, profile_id=None, file_path = None): + """ + Constructor + + Keyword arguments: + device -- device the profile is associated with + id -- profile ID + """ + + + self.device = device + self.read_only = False + self.parser = ConfigParser.ConfigParser({ + }) + self.name = None + self.icon = None + self.background = None + self.filename = None + self.id = -1 + if profile_id is not None: + self.set_id(profile_id) + if file_path is not None: + self.filename = file_path + self.author = "" + self.macros = { g15driver.KEY_STATE_UP: [], + g15driver.KEY_STATE_DOWN: [], + g15driver.KEY_STATE_HELD: [] + } + self.mkey_color = {} + self.activate_on_focus = False + self.activate_on_launch = False + self.launch_pattern = None + self.monitor = [ "stdout" ] + self.models = [ device.model_id ] + self.window_name = "" + self.base_profile = None + self.version = 2.1 + self.plugins_mode = ALL_PLUGINS + self.selected_plugins = [] + + self.load(self.filename) + + def can_launch(self, command_line): + """ + Test if this profile can launch a command with the provided arguments, + monitoring it's output (or other log files) for output, and produce + events and extract information that may be used by a "Game Theme" or "Game Plugin" + + Keyword arguments: + command_line -- command line to match against. this should have + each argument wrapped in quotes for consistency. + """ + return re.search(self.launch_pattern, command_line) + + def export(self, filename): + """ + Save this profile in a format that may be transmitted to another + computer (as a zip file). All references to external images (for icon and background) + are made relative and added to the archive. + + Keyword arguments: + filename -- file to save copy to + """ + profile_copy = get_profile(self.device, self.id) + + archive_file = zipfile.ZipFile(filename, "w", compression = zipfile.ZIP_DEFLATED) + try: + # Icon + if profile_copy.icon and os.path.exists(profile_copy.icon): + base_path = "%s.resources/%s" % ( profile_copy.id, os.path.basename(profile_copy.icon) ) + archive_file.write(profile_copy.icon, base_path ) + profile_copy.icon = base_path + + # Background + if profile_copy.background and os.path.exists(profile_copy.background): + base_path = "%s.resources/%s" % ( profile_copy.id, os.path.basename(profile_copy.background) ) + archive_file.write(profile_copy.background, base_path) + profile_copy.background = base_path + + # Profile + profile_data = StringIO() + try: + profile_copy.save(profile_data) + archive_file.writestr("%s.macros" % profile_copy.id, profile_data.getvalue()) + finally: + profile_data.close() + finally: + archive_file.close() + + def are_keys_in_use(self, activate_on, memory, keys, exclude = None): + """ + Get if the specified keys are currently in use for a macro in the + supplied memory bank number. Optionally, a list of macros that + should be excluded from the search can be supplied (usually used + to exclude the current macro when checking if other macros currently + use a set of keys) + + Keyword arguments: + activate_on -- the key state to activate the macro on + memory -- memory bank number + keys -- keys to search for + exclude -- list of macro objects to exclude + """ + bank = self.macros[activate_on][memory - 1] + for macro in bank: + if ( exclude == None or ( exclude != None and not self._is_excluded(exclude, macro) ) ) and sorted(keys) == sorted(macro.keys): + return True + return False + + def get_default(self): + """ + Get if this profile is the default one + """ + return self == get_default_profile(self.device) + + def save(self, filename = None): + """ + Save this profile to disk + """ + if self.read_only: + raise Exception("Cannot write to read-only profile") + logger.info("Saving macro profile %s, %s", self.id, self.name) + if filename is None: + filename = self.filename + if self.window_name == None: + self.window_name = "" + if self.icon == None: + self.icon = "" + + # Set the profile options + self.parser.set("DEFAULT", "name", self.name) + self.parser.set("DEFAULT", "version", str(self.version)) + self.parser.set("DEFAULT", "icon", self.icon) + self.parser.set("DEFAULT", "window_name", self.window_name) + if self.version == 1.0: + self.parser.set("DEFAULT", "base_profile", str(self.base_profile) if self.base_profile is not None else "-1") + else: + self.parser.set("DEFAULT", "base_profile", str(self.base_profile) if self.base_profile is not None else "") + self.parser.set("DEFAULT", "icon", self.icon) + self.parser.set("DEFAULT", "background", self.background) + self.parser.set("DEFAULT", "author", self.author) + self.parser.set("DEFAULT", "activate_on_focus", str(self.activate_on_focus)) + self.parser.set("DEFAULT", "plugins_mode", str(self.plugins_mode)) + self.parser.set("DEFAULT", "selected_plugins", ",".join(self.selected_plugins)) + self.parser.set("DEFAULT", "send_delays", str(self.send_delays)) + self.parser.set("DEFAULT", "fixed_delays", str(self.fixed_delays)) + self.parser.set("DEFAULT", "press_delay", str(self.press_delay)) + self.parser.set("DEFAULT", "release_delay", str(self.release_delay)) + self.parser.set("DEFAULT", "models", ",".join(self.models)) + + # Set the launch options + if self.launch_pattern is not None: + self.parser.set("LAUNCH", "pattern", self.launch_pattern) + self.parser.set("LAUNCH", "monitor", ",".join(self.monitor)) + self.parser.set("LAUNCH", "activate_on_launch", str(self.activate_on_launch)) + else: + self._remove_if_exists("pattern", "LAUNCH") + self._remove_if_exists("monitor", "LAUNCH") + self._remove_if_exists("activate_on_launch", "LAUNCH") + + # Remove and re-add the bank sections + for activate_on in [ g15driver.KEY_STATE_UP, g15driver.KEY_STATE_HELD ]: + for i in range(1, 4): + section_name = "m%d" % i + if activate_on != g15driver.KEY_STATE_UP: + section_name = "%s-%s" % ( section_name, activate_on ) + if not self.parser.has_section(section_name): + self.parser.add_section(section_name) + col = self.mkey_color[i] if i in self.mkey_color else None + if col: + self.parser.set(section_name, "backlight_color", g15convert.rgb_to_string(col)) + elif self.parser.has_option(section_name, "backlight_color"): + self.parser.remove_option(section_name, "backlight_color") + + # Add the macros + for activate_on in [ g15driver.KEY_STATE_UP, g15driver.KEY_STATE_HELD ]: + for i in range(1, 4): + for macro in self.get_sorted_macros(activate_on, i): + if len(macro.keys) > 0: + macro._store() + + self._write(filename) + + def set_id(self, profile_id): + self.id = str(profile_id) + self.read_only = False + self.filename = "%s/%s/%s.macros" % ( conf_dir, self.device.uid, self.id ) + + def get_binding_for_action(self, activate_on, action_name): + """ + Get an ActionBinding if this profile contains a map to the supplied + action name. + + Keyword arguments: + activate_on -- the key state to activate the macro on + action_name -- name of action + """ + for bank in self.macros[activate_on]: + for m in bank: + if m.type == MACRO_ACTION and m.macro == action_name and activate_on == m.activate_on: + # TODO held actions? + return g15actions.ActionBinding(action_name, m.keys, g15driver.KEY_STATE_UP) + + def set_mkey_color(self, memory, rgb): + """ + Set a tuple containing the red, green and blue values of the colour + to use when the specifed bank is active + + Keyword arguments: + memory -- memory bank number + rgb -- colour to assign to bank + """ + self.mkey_color[memory] = rgb + + def get_mkey_color(self, memory): + """ + Get a tuple contain the red, green and blue values of the colour + to use when the specifed bank is active + + Keyword arguments: + memory -- memory bank number + """ + return self.mkey_color[memory] if memory in self.mkey_color else None + + def delete(self): + """ + Delete this macro profile + """ + os.remove(self.filename) + + def delete_macro(self, activate_on, memory, keys): + """ + Delete the macro that is activated by the specified keys in the + supplied memory bank number + + Keyword arguments: + activate_on -- key state to activate the macro on + memory -- memory bank number (starts at 1) + keys -- keys that activate the macro + """ + section_name = self._get_section_name(activate_on, memory) + key_list_key = get_keys_key(keys) + logger.info("Deleting macro M%d, for %s", memory, key_list_key) + self._delete_key(section_name, key_list_key) + self._write(self.filename) + bank_macros = self.macros[activate_on][memory - 1] + for macro in bank_macros: + if macro.key_list_key == key_list_key and macro in bank_macros: + bank_macros.remove(macro) + + def get_profile_icon_path(self, height): + """ + Get the icon for the profile. This will either be a specific icon + path, or if none is available, the default profile icon. If the + icon is a themed icon name, then that icon will be searched for and + the full path returned + + Keyword arguments: + height -- preferred height + """ + icon = self.icon + if icon is not None and icon.startswith("/"): + return icon + + path = self.get_resource_path(icon) + if path is None: + if icon == None or icon == "": + icon = [ "preferences-desktop-keyboard-shortcuts", "preferences-desktop-keyboard" ] + + return g15icontools.get_icon_path(icon, height) + + return path + + def get_resource_path(self, resource_name): + """ + Get the full path of a resource (i.e. a path relative to the location + of the profile's file. None will be returned if no such resource exists + + Keyword arguments: + resource_name -- resource name + """ + if resource_name is not None and resource_name != "": + if resource_name.startswith("/"): + return resource_name + if self.filename is not None: + path = os.path.join(os.path.dirname(self.filename), resource_name) + if os.path.exists(path): + return path + + def create_macro(self, memory, keys, name, macro_type, macro, activate_on): + """ + Create a new macro + + Keyword arguments: + memory -- memory bank number (starts at 1) + keys -- list of keys that activate the macro + name -- name of macro + type -- macro type + macro -- content of macro + """ + key_list_key = get_keys_key(keys) + logger.info("Creating macro M%d, for %s", memory, key_list_key) + new_macro = G15Macro(self, memory, key_list_key, activate_on) + new_macro.name = name + new_macro.type = macro_type + new_macro.macro = macro + self.macros[activate_on][memory - 1].append(new_macro) + new_macro.save() + return new_macro + + def get_macro(self, activate_on, memory, keys): + """ + Get the macro given the memory bank number and the list of keys + the macro requires to activate + + Keyword arguments: + activate_on -- the key state to activate the macro on + memory -- memory bank number (starts at 1) + keys -- list of keys that activate the macro + """ + bank = self.macros[activate_on][memory - 1] + for macro in bank: + key_count = 0 + for k in macro.keys: + if k in keys: + key_count += 1 + if key_count == len(macro.keys) and key_count == len(keys): + return macro + + def is_active(self): + """ + Get if this profile is the currently active one + """ + active = get_active_profile(self.device) + return active is not None and self.id == active.id + + def make_active(self): + """ + Make this the currently active profile. An Exception will be raised + if the profile is currently locked for this device + """ + if is_locked(self.device): + raise Exception("Cannot change active profile when locked.") + + + conf_client.set_string("/apps/gnome15/%s/active_profile" % self.device.uid, str(self.id)) + + def load(self, filename = None, fd = None): + """ + Load the profile from disk + """ + + # Initial values + self.macros = { g15driver.KEY_STATE_UP: [], + g15driver.KEY_STATE_DOWN: [], + g15driver.KEY_STATE_HELD: [] + } + self.mkey_color = {} + + # Load macro file + if self.id != -1 or filename is not None or fd is not None: + if ( isinstance(filename, str) or isinstance(filename, unicode) ) and os.path.exists(filename): + self.read_only = not os.stat(filename)[0] & stat.S_IWRITE + self.parser.readfp(codecs.open(filename, "r", "utf8")) + elif fd is not None: + self.read_only = True + self.parser.readfp(fd) + else: + self.read_only = False + + # Macro file format version. Try to keep macro files backwardly and + # forwardly compatible + if self.parser.has_option("DEFAULT", "version"): + self.version = float(self.parser.get("DEFAULT", "version").strip()) + else: + self.version = 1.0 + + # Info section + self.name = self.parser.get("DEFAULT", "name").strip() if self.parser.has_option("DEFAULT", "name") else "" + self.icon = self.parser.get("DEFAULT", "icon").strip() if self.parser.has_option("DEFAULT", "icon") else "" + self.background = self.parser.get("DEFAULT", "background").strip() if self.parser.has_option("DEFAULT", "background") else "" + self.author = self.parser.get("DEFAULT", "author").strip() if self.parser.has_option("DEFAULT", "author") else "" + self.window_name = self.parser.get("DEFAULT", "window_name").strip() if self.parser.has_option("DEFAULT", "window_name") else "" + self.models = self.parser.get("DEFAULT", "models").strip().split(",") if self.parser.has_option("DEFAULT", "models") else [ self.device.model_id ] + self.plugins_mode = self.parser.get("DEFAULT", "plugins_mode").strip() if self.parser.has_option("DEFAULT", "plugins_mode") else ALL_PLUGINS + self.selected_plugins = self.parser.get("DEFAULT", "selected_plugins").strip().split(",") \ + if self.parser.has_option("DEFAULT", "selected_plugins") else [ ] + + self.activate_on_focus = self.parser.getboolean("DEFAULT", "activate_on_focus") if self.parser.has_option("DEFAULT", "activate_on_focus") else False + self.send_delays = self.parser.getboolean("DEFAULT", "send_delays") if self.parser.has_option("DEFAULT", "send_delays") else False + self.fixed_delays = self.parser.getboolean("DEFAULT", "fixed_delays") if self.parser.has_option("DEFAULT", "fixed_delays") else False + + self.base_profile = self.parser.get("DEFAULT", "base_profile").strip() if self.parser.has_option("DEFAULT", "base_profile") else "" + if self.base_profile == "-1": + # For version 1.0 profile format compatibility + self.base_profile = None + + self.press_delay = self._get_int("press_delay", 50) + self.release_delay = self._get_int("release_delay", 50) + + # Launch + self.launch_pattern = self.parser.get("LAUNCH", "pattern").strip() \ + if self.parser.has_option("LAUNCH", "pattern") else None + self.monitor = self.parser.get("LAUNCH", "monitor").strip().split(",") \ + if self.parser.has_option("LAUNCH", "monitor") else [ "stdout" ] + self.activate_on_launch = self.parser.getboolean("LAUNCH", "activate_on_launch") \ + if self.parser.has_option("LAUNCH", "activate_on_launch") else False + + # Bank sections + + for activate_on in [ g15driver.KEY_STATE_UP, g15driver.KEY_STATE_DOWN, g15driver.KEY_STATE_HELD ]: + for i in range(1, 4): + section_name = "m%d" % i + if activate_on != g15driver.KEY_STATE_UP: + section_name = "%s-%s" % ( section_name, activate_on ) + if not self.parser.has_section(section_name): + self.parser.add_section(section_name) + self.mkey_color[i] = g15convert.to_rgb(self.parser.get(section_name, "backlight_color")) if self.parser.has_option(section_name, "backlight_color") else None + memory_macros = [] + self.macros[activate_on].append(memory_macros) + for option in self.parser.options(section_name): + if option.startswith("keys_") and option.endswith("_name"): + key_list_key = option[5:-5] + macro_obj = G15Macro(self, i, key_list_key, activate_on) + macro_obj._load() + memory_macros.append(macro_obj) + + def get_sorted_macros(self, activate_on, memory_number): + """ + Get the list of macros sorted + + Keyword arguments: + activate_on -- the state the macro is activated on + memory_number -- memory bank number to retrieve macros from (starts at 1) + """ + sm = [] + if activate_on is None: + for activate_on in [ g15driver.KEY_STATE_UP, g15driver.KEY_STATE_DOWN, g15driver.KEY_STATE_HELD ]: + if activate_on in self.macros and memory_number <= len(self.macros[activate_on]): + sm += self.macros[activate_on][memory_number - 1] + else: + if activate_on in self.macros and memory_number <= len(self.macros[activate_on]): + sm += self.macros[activate_on][memory_number - 1] + sm.sort(self._comparator) + return sm + + ''' + Private + ''' + def _comparator(self, o1, o2): + return o1.compare(o2) + + def _remove_if_exists(self, name, section = "DEFAULT"): + if self.parser.has_option(section, name): + self.parser.remove_option(section, name) + + def _get_section_name(self, state, memory): + section_name = "m%d" % memory + if state != g15driver.KEY_STATE_UP: + section_name = "%s-%s" % ( section_name, state ) + return section_name + + def _get_int(self, name, default_value, section = "DEFAULT"): + try: + return self.parser.getint(section, name) if self.parser.has_option(section, name) else default_value + except ValueError as v: + logger.debug("Error when parsing a integer value", exc_info = v) + return default_value + + def __ne__(self, profile): + return not self.__eq__(profile) + + def __eq__(self, profile): + return profile is not None and self.id == profile.id + + def _write(self, save_file = None): + + if save_file is None or self.id == -1: + raise Exception("Cannot save a profile without a filename or an id.") + + if isinstance(save_file, str): + dir_name = os.path.dirname(save_file) + if not os.path.exists(dir_name): + os.mkdir(dir_name) + tmp_file = "%s.tmp" % save_file + with open(tmp_file, 'wb') as configfile: + self.parser.write(configfile) + os.rename(tmp_file, save_file) + fhandle = file(save_file, 'a') + try: + os.utime(save_file, None) + finally: + fhandle.close() + else: + self.parser.write(save_file) + + def _delete_key(self, section_name, key_list_key): + for option in self.parser.options(section_name): + if option.startswith("keys_" + key_list_key + "_"): + self.parser.remove_option(section_name, option) + + def _is_excluded(self, excluded, macro): + for e in excluded: + if e == macro: + return True \ No newline at end of file diff --git a/src/gnome15/g15screen.py b/src/gnome15/g15screen.py new file mode 100644 index 0000000..6bb8197 --- /dev/null +++ b/src/gnome15/g15screen.py @@ -0,0 +1,1593 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2010 Brett Smith +# +# 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 . + +import gnome15.g15locale as g15locale +import gnome15.g15devices as g15devices +#from gnome15 import g15pluginmanager +_ = g15locale.get_translation("gnome15").ugettext + +""" +Queues +""" +REDRAW_QUEUE = "redrawQueue" + +""" +Page priorities +""" +PRI_POPUP = 999 +PRI_EXCLUSIVE = 100 +PRI_HIGH = 99 +PRI_NORMAL = 50 +PRI_LOW = 20 +PRI_INVISIBLE = 0 + +""" +Paint stages +""" +BACKGROUND_PAINTER = 0 +FOREGROUND_PAINTER = 1 + +""" +Simple colors +""" +COLOURS = [(0, 0, 0), (255, 0, 0), (0, 255, 0), (0, 0, 255), (255, 255, 0), (255, 0, 255), (0, 255, 255), (255, 255, 255)] + +import g15driver +import util.g15scheduler as g15scheduler +import util.g15pythonlang as g15pythonlang +import util.g15gconf as g15gconf +import util.g15cairo as g15cairo +import util.g15icontools as g15icontools +import g15profile +import g15globals +import g15drivermanager +import g15keyboard +import g15theme +import g15actions +import time +import threading +import cairo +import gconf +import os.path +import sys +import logging +from threading import RLock +from g15exceptions import NotConnectedException +from g15exceptions import RetryException +logger = logging.getLogger(__name__) + +""" +This module contains the root component for a single device (i.e. the 'screen'), and all +of the supporting classes. The screen object is responsible for maintaining the connection +to the driver, starting and stopping all the device's plugins, tracking the current memory +bank, performing the actual painting for the associated device and more. + +To this screen, 'pages' will be added by plugins and other subsystems. Only a single page +is ever visible at one time, and the screen is responsible for switching between them. +You can think of the screen as the window manager. +""" + +def check_on_redraw(): + """ + Helper to check the current thread is the redraw thread + """ +# if not jobqueue.is_on_queue(REDRAW_QUEUE): +# raise Exception("Illegal thread access (on queue %s)." % jobqueue.get_current_queue()) + pass + +def run_on_redraw(cb, *args): + """ + Helper to run a callback function on the redraw queue. + """ + g15scheduler.queue(REDRAW_QUEUE, "Redraw", 0, cb, *args) + +class ScreenChangeAdapter(): + """ + Adapter class for screen change listeners to save such listeners having to + implement all callbacks, just override the ones you want + """ + + def memory_bank_changed(self, new_bank_number): + """ + Call when the current memory bank changes + + Keyword arguments: + new_bank_number -- new memory bank number + """ + pass + + def attention_cleared(self): + """ + Called when the screen is no longer in attention state (i.e. the + error has been cleared) + """ + pass + + def attention_requested(self, message): + """ + Called when the screen has some problem and needs attention (e.g. + driver problem) + + Keyword arguments: + message -- message detailing problem + """ + pass + + def driver_disconnected(self, driver): + """ + Called when the underlying driver is disconnected. + + Keyword arguments: + driver -- driver that disconnected + """ + pass + + def driver_connected(self, driver): + """ + Called when the underlying driver is connected. + + Keyword arguments: + driver -- driver that connected + """ + pass + + def driver_connection_failed(self, driver): + """ + Called when the underlying driver connection fails. + + Keyword arguments: + driver -- driver that connected + exception -- exception + """ + pass + + def deleting_page(self, page): + """ + Called when a page is about to be removed from screen + + Keyword arguments: + page -- page that is to be removed + """ + pass + + def deleted_page(self, page): + """ + Called when a page has been removed from screen + + Keyword arguments: + page -- page that has been removed + """ + pass + + def new_page(self, page): + """ + Called when a page is added to the screen + + Keyword arguments: + page -- page that has been added + """ + pass + + def title_changed(self, page, title): + """ + Called when the title of page changes + + Keyword arguments: + page -- page that has changed + title -- title new title + """ + pass + + def page_changed(self, page): + """ + Called when the page changes in some other way (e.g. priority) + + Keyword arguments: + page -- page that has changed + """ + pass + + + +class KeyState(): + """ + Holds the current state of a single macro key + """ + def __init__(self, key): + self.key = key + self.state_id = None + self.timer = None + + def cancel_timer(self): + """ + Cancel the HELD timer if one exists. + """ + if self.timer is not None: + self.timer.cancel() + self.timer = None + + def __repr__(self): + return "%s = %s" % (self.key, g15profile.to_key_state_name(self.state_id)) + +class Painter(): + """ + Painters may be added to screens to draw stuff either beneath (BACKGROUND_PAINTER) + or above (FOREGROUND_PAINTER) the main component (i.e. the currently visible page). + Each painter also has z-order which determines when it is painted in relation to + other painters of the same place. + """ + + + def __init__(self, place=BACKGROUND_PAINTER, z_order=0): + """ + Constructor + + Keyword arguments: + place -- either BACKGROUND_PAINTER or FOREGROUND_PAINTER + z_order -- the order the painter is called within the place + """ + self.z_order = z_order + self.place = place + + def paint(self, canvas): + """ + Subclasses must override to do the actual painting + + Keyword arguments: + canvas -- canvas + """ + raise Exception("Not implemented") + + +class G15Screen(): + + def __init__(self, plugin_manager_module, service, device): + self.service = service + self.plugin_manager_module = plugin_manager_module + self.device = device + self.driver = None + self.screen_change_listeners = [] + self.local_data = threading.local() + self.local_data.surface = None + self.plugins = [] + self.conf_client = service.conf_client + self.notify_handles = [] + self.connection_lock = RLock() + self.draw_lock = RLock() + self.defeat_profile_change = 0 + self.first_page = None + self.attention_message = g15globals.name + self.attention = False + self.splash = None + self.reschedule_lock = RLock() + self.last_error = None + self.loading_complete = False + self.control_handles = [] + self.color_no = 1 + self.cycle_timer = None + self._started_plugins = False + self.stopping = False + self.reconnect_timer = None + self.plugins = self.plugin_manager_module.G15Plugins(self, network_manager = service.network_manager) + self.pages = [] + self.memory_bank_color_control = None + self.acquired_controls = {} + self.painters = [] + self.fader = None + self.mkey = 1 + self.temp_acquired_controls = {} + self.key_handler = g15keyboard.G15KeyHandler(self) + self.glass_pane = g15theme.Component("glasspane") + + if not self._load_driver(): + raise Exception("Driver failed to load") + + def set_active_application_name(self, application_name): + """ + Set the currently active application (may be a window name or a high + level application name). Returns a boolean indicating whether or not + a profile that matches was found + + Keyword arguments: + application_name -- application name + splash -- splash callback + startup -- True when this change is the result of startup + """ + if self.device is None: + return False + + found = False + if self.defeat_profile_change < 1 and not g15profile.is_locked(self.device): + choose_profile = None + # Active window has changed, see if we have a profile that matches it + if application_name is not None: + for profile in g15profile.get_profiles(self.device): + if not profile.get_default() and profile.activate_on_focus and len(profile.window_name) > 0 and application_name.lower().find(profile.window_name.lower()) != -1: + choose_profile = profile + break + + # No applicable profile found. Look for a default profile, and see if it is set to activate by default + active_profile = g15profile.get_active_profile(self.device) + if choose_profile == None: + default_profile = g15profile.get_default_profile(self.device) + + if (active_profile == None or active_profile.id != default_profile.id) and default_profile.activate_on_focus: + default_profile.make_active() + found = True + elif active_profile == None or choose_profile.id != active_profile.id: + choose_profile.make_active() + found = True + + return found + + def start(self): + logger.info("Starting %s.", self.device.uid) + + # Remove previous fader is it exists + if self.fader: + self.painters.remove(self.fader) + self.fader = None + + # Start the driver + self.attempt_connection() + + # Start handling keys + self.key_handler.start() + self.key_handler.action_listeners.append(self) + self.key_handler.action_listeners.append(self.service.macro_handler) + self.key_handler.key_handlers.append(self) + self.key_handler.key_handlers.append(self.service.macro_handler) + + # This is just here for backwards compatibility and may be removed at some point + self.action_listeners = self.key_handler.action_listeners + + # Monitor gconf + screen_key = "/apps/gnome15/%s" % self.device.uid + logger.info("Watching GConf settings in %s", screen_key) + self.conf_client.add_dir(screen_key, gconf.CLIENT_PRELOAD_NONE) + self.notify_handles.append(self.conf_client.notify_add("%s/cycle_screens" % screen_key, self.resched_cycle)) + self.notify_handles.append(self.conf_client.notify_add("%s/active_profile" % screen_key, self.active_profile_changed)) + self.notify_handles.append(self.conf_client.notify_add("%s/driver" % screen_key, self.driver_changed)) + for control in self.driver.get_controls(): + self.notify_handles.append(self.conf_client.notify_add("%s/%s" % (screen_key, control.id), self._control_changed)) + logger.info("Starting for %s is complete.", self.device.uid) + + g15profile.profile_listeners.append(self._profile_changed) + + # Start watching for network changes + self.service.network_manager.listeners.append(self._network_state_change) + + def stop(self, quickly=False): + logger.info("Stopping screen for %s", self.device.uid) + self.stopping = True + + # Stop attempting reconnection + if self.reconnect_timer is not None: + self.reconnect_timer.cancel() + + + # Stop watching for network changes + if self._network_state_change in self.service.network_manager.listeners: + self.service.network_manager.listeners.remove(self._network_state_change) + + # Clean up key handler + if self in self.key_handler.action_listeners: + self.key_handler.action_listeners.remove(self) + if self.service.macro_handler in self.key_handler.action_listeners: + self.key_handler.action_listeners.remove(self.service.macro_handler) + if self in self.key_handler.key_handlers: + self.key_handler.key_handlers.remove(self) + if self.service.macro_handler in self.key_handler.key_handlers: + self.key_handler.key_handlers.remove(self.service.macro_handler) + self.key_handler.stop() + + # Stop listening for profile changes + if self._profile_changed in g15profile.profile_listeners: + g15profile.profile_listeners.remove(self._profile_changed) + + # Stop listening for configuration changes + for h in self.notify_handles: + self.conf_client.notify_remove(h) + self.notify_handles = [] + + # Shutdown effects + if self.is_active() and not quickly and (self.service.fade_screen_on_close \ + or self.service.fade_keyboard_backlight_on_close): + # Start fading keyboard + acquisition = None + slow_shutdown_duration = 3.0 + if self.service.fade_keyboard_backlight_on_close: + bl_control = self.driver.get_control_for_hint(g15driver.HINT_DIMMABLE) + if bl_control: + current_val = bl_control.value + """ + First acquire, and turn all lights off, this is the state it will + return to before disconnecting (we never release it) + """ + self.driver.acquire_control(bl_control, val=0 if isinstance(current_val, int) else (0, 0, 0)) + + acquisition = self.driver.acquire_control(bl_control, val=current_val) + acquisition.fade(duration=slow_shutdown_duration, release=True, step=1 if isinstance(current_val, int) else 10) + + # Fade screen + if self.driver.get_bpp() > 0 and self.service.fade_screen_on_close: + self.fade(True, duration=slow_shutdown_duration, step=10) + + # Wait for keyboard fade to finish as well if it hasn't already + if acquisition: + acquisition.wait() + + # Stop the plugins + if self.plugins and self.plugins.is_activated(): + self.plugins.deactivate() + self.plugins.destroy() + + # Disconnect the driver + if self.driver and self.driver.is_connected(): + self.driver.all_off_on_disconnect = self.service.all_off_on_disconnect + self.driver.disconnect() + + def add_screen_change_listener(self, screen_change_listener): + if not screen_change_listener in self.screen_change_listeners: + self.screen_change_listeners.append(screen_change_listener) + + def remove_screen_change_listener(self, screen_change_listener): + if screen_change_listener in self.screen_change_listeners: + self.screen_change_listeners.remove(screen_change_listener) + + def set_available_size(self, size): + self.available_size = size + self.redraw() + + def get_memory_bank(self): + return self.mkey + + def set_memory_bank(self, bank): + logger.info("Setting memory bank to %d", bank) + self.mkey = bank + val = g15driver.get_mask_for_memory_bank(bank) + control = self.driver.get_control_for_hint(g15driver.HINT_MKEYS) + if control: + self.acquired_controls[control.id].set_value(val) + self.set_color_for_mkey() + for listener in self.screen_change_listeners: + g15pythonlang.call_if_exists(listener, "memory_bank_changed", bank) + + def index(self, page): + """ + Returns the page index + + Keyword arguments: + page -- page object + """ + i = 0 + for p in self.pages: + if p == page: + return i + i = i + 1 + return i + + def get_page(self, page_id): + """ + Return a page object given it's ID + + Keyword arguments: + page_id -- page ID + """ + for page in self.pages: + if page.id == page_id: + return page + + def clear_popup(self): + """ + Clear any popup screens that are currently running + """ + for page in self.pages: + if page.priority == PRI_POPUP: + # Drop the priority of other popups + page.set_priority(PRI_LOW) + break + + def add_page(self, page): + """ + Add a new page. Returns the G15Page object + + Keyword arguments: + page -- page to add + """ + if self.driver.get_bpp() == 0: + raise Exception("The current device has no suitable output device") + + logger.info("Creating new page with %s of priority %d", page.id, page.priority) + self.page_model_lock.acquire() + try : + logger.info("Adding page %s", page.id) + self.clear_popup() + if page.priority == PRI_EXCLUSIVE: + for p in self.pages: + if p.priority == PRI_EXCLUSIVE: + logger.warning("Another page is already exclusive. Lowering %s to HIGH", id) + page.priority = PRI_HIGH + break + self.pages.append(page) + for l in self.screen_change_listeners: + g15pythonlang.call_if_exists(l, "new_page", page) + return page + finally: + self.page_model_lock.release() + + def new_page(self, painter=None, priority=PRI_NORMAL, on_shown=None, on_hidden=None, on_deleted=None, + id="Unknown", thumbnail_painter=None, panel_painter=None, title=None, \ + theme_properties_callback=None, theme_attributes_callback=None, + originating_plugin = None): + logger.warning("DEPRECATED call to G15Screen.new_page, use G15Screen.add_page instead") + + """ + Create a new page. Returns the G15Page object + + Keyword arguments: + painter -- painter function. Will be called with a 'canvas' argument that is a cairo.Context + priority -- priority of screen, defaults to PRI_NORMAL + on_shown -- function to call when screen is show. Defaults to None + on_hidden -- function to call when screen is hidden. Defaults to None + on_deleted -- function to call when screen is deleted. Defaults to None + id -- id of screen + thumbnail_painter -- function to call to paint thumbnails for this page. Defaults to None + panel_painter -- function to call to paint panel graphics for this page. Defaults to None + theme_properties_callback -- function to call to get theme properties + theme_attributes_callback -- function to call to get theme attributes + """ + if self.driver.get_bpp() == 0: + raise Exception(_("The current device has no suitable output device")) + + logger.info("Creating new page with %s of priority %d", id, priority) + self.page_model_lock.acquire() + try : + self.clear_popup() + if priority == PRI_EXCLUSIVE: + for page in self.pages: + if page.priority == PRI_EXCLUSIVE: + logger.warning("Another page is already exclusive. Lowering %s to HIGH", id) + priority = PRI_HIGH + break + + page = g15theme.G15Page(id, self, painter, priority, on_shown, on_hidden, on_deleted, \ + thumbnail_painter, panel_painter, theme_properties_callback, \ + theme_attributes_callback, + originating_plugin = originating_plugin) + self.pages.append(page) + for l in self.screen_change_listeners: + g15pythonlang.call_if_exists(l, "new_page", page) + if title: + page.set_title(title) + return page + finally: + self.page_model_lock.release() + + def delete_after(self, delete_after, page): + """ + Delete a page after a given time interval. Returns timer object used for deleting. May be canceled + + Keyword arguments: + delete_after -- interval in seconds (float) + page -- page object to hide + """ + self.page_model_lock.acquire() + try : + if page.id in self.deleting: + # If the page was already deleting, cancel previous timer + self.deleting[page.id].cancel() + del self.deleting[page.id] + + timer = g15scheduler.schedule("DeleteScreen", delete_after, self.del_page, page) + self.deleting[page.id] = timer + return timer + finally: + self.page_model_lock.release() + + def is_on_timer(self, page): + ''' + Get if the given page is currently on a revert or delete timer + + Keyword arugments: + page -- page object + ''' + return page.id in self.reverting or page.id in self.deleting + + def set_priority(self, page, priority, revert_after=0.0, delete_after=0.0, do_redraw=True): + """ + Change the priority of a page, optionally reverting or deleting after a specified time. Returns timer object used for reverting or deleting. May be canceled + + Keyword arguments: + page -- page object to change + priority -- new priority + revert_after -- revert the page priority to it's original value after specified number of seconds + delete_after -- delete the page after specified number of seconds + do_redraw -- redraw after changing priority. Defaults to True + """ + self.page_model_lock.acquire() + try : + if page != None: + old_priority = page.priority + page._do_set_priority(priority) + if do_redraw: + self.redraw() + if revert_after != 0.0: + # If the page was already reverting, restore the priority and cancel the timer + if page.id in self.reverting: + old_priority = self.reverting[page.id][0] + self.reverting[page.id][1].cancel() + del self.reverting[page.id] + + # Start a new timer to revert + timer = g15scheduler.schedule("Revert", revert_after, self.set_priority, page, old_priority) + self.reverting[page.id] = (old_priority, timer) + return timer + if delete_after != 0.0: + return self.delete_after(delete_after, page) + finally: + self.page_model_lock.release() + + def raise_page(self, page): + """ + Raise the page. If it is LOW priority, it will be turned into a POPUP. If it is any other priority, + it will be raised to the top of list of all pages that are of the same priority (effectively making + it visible) + + Keyword arguments: + page - page to raise + """ + if page.priority == PRI_LOW: + page.set_priority(PRI_POPUP) + else: + page.set_time(time.time()) + self.redraw() + + def del_page(self, page): + """ + Remove the page from the screen. The page will be hidden and the next highest priority page + displayed. + + Keyword arguments: + page -- page to remove + """ + self.page_model_lock.acquire() + try : + if page != None and page in self.pages: + logger.info("Deleting page %s", page.id) + + # Remove any timers that might be running on this page + if page.id in self.deleting: + self.deleting[page.id].cancel() + del self.deleting[page.id] + if page.id in self.reverting: + self.reverting[page.id][1].cancel() + del self.reverting[page.id] + + for l in self.screen_change_listeners: + g15pythonlang.call_if_exists(l, "deleting_page", page) + + if page == self.visible_page: + self.visible_page = None + page._do_on_hidden() + + page.remove_all_children() + + self.pages.remove(page) + page._do_on_deleted() + self.redraw() + for l in self.screen_change_listeners: + g15pythonlang.call_if_exists(l, "deleted_page", page) + finally: + self.page_model_lock.release() + + def get_last_error(self): + return self.last_error + + def should_reconnect(self, exception): + if isinstance(exception, RetryException): + return True + if g15devices.have_udev: + return False + return isinstance(exception, NotConnectedException) + + def complete_loading(self): + try : +# logger.info("Activating plugins") +# self.plugins.activate(self.splash.update_splash if self.splash else None) + + logger.info("Setting active profile and activating plugins") + self.set_active_application_name(self.service.get_active_application_name()) + self._check_active_plugins(splash=self.splash.update_splash if self.splash else None, \ + startup=True) + + if self.first_page != None: + page = self.get_page(self.first_page) + if page: + self.raise_page(page) + + logger.info("Grabbing keyboard") + self.driver.grab_keyboard(self.key_handler.key_received) + + logger.info("Grabbed keyboard") + self.clear_attention() + + if self.splash: + self.splash.complete() + self.loading_complete = True + logger.info("Loading complete") + except Exception as e: + logger.debug("Exception completing loading", exc_info = e) + if self._process_exception(e): + raise + + def screen_cycle(self): + page = self.get_visible_page() + if page != None and page.priority < PRI_HIGH: + self.cycle(1) + else: + self.resched_cycle() + + def resched_cycle(self, arg1=None, arg2=None, arg3=None, arg4=None): + + self.reschedule_lock.acquire() + try: + logger.debug("Rescheduling cycle") + self._cancel_timer() + cycle_screens = g15gconf.get_bool_or_default(self.conf_client, "/apps/gnome15/%s/cycle_screens" % self.device.uid, True) + active = self.driver != None and self.driver.is_connected() and cycle_screens + if active and self.cycle_timer == None: + val = self.conf_client.get("/apps/gnome15/%s/cycle_seconds" % self.device.uid) + time = 10 + if val != None: + time = val.get_int() + self.cycle_timer = g15scheduler.schedule("CycleTimer", time, self.screen_cycle) + finally: + self.reschedule_lock.release() + + def cycle_backlight(self, val): + c = self.driver.get_control_for_hint(g15driver.HINT_DIMMABLE) + if c: + if isinstance(c, int): + self.cycle_level(val, c) + else: + self.cycle_color(val, c) + + def cycle_color(self, val, control): + logger.debug("Cycling of %s color by %d", control.id, val) + self.color_no += val + if self.color_no < 0: + self.color_no = len(COLOURS) - 1 + if self.color_no >= len(COLOURS): + self.color_no = 0 + color = COLOURS[self.color_no] + self.conf_client.set_string("/apps/gnome15/%s/%s" % (self.device.uid, control.id), "%d,%d,%d" % (color[0], color[1], color[2])) + + + def cycle_level(self, val, control): + logger.debug("Cycling of %s level by %d", control.id, val) + level = self.conf_client.get_int("/apps/gnome15/%s/%s" % (self.device.uid, control.id)) + level += val + if level > control.upper - 1: + level = control.lower + if level < control.lower - 1: + level = control.upper + self.conf_client.set_int("/apps/gnome15/%s/%s" % (self.device.uid, control.id), level) + + def control_configuration_changed(self, client, connection_id, entry, args): + key = os.path.basename(entry.key) + logger.debug("Controls changed %s", str(key)) + if self.driver != None: + for control in self.driver.get_controls(): + if key == control.id and control.hint & g15driver.HINT_VIRTUAL == 0: + if isinstance(control.value, int): + value = entry.value.get_int() + else: + rgb = entry.value.get_string().split(",") + value = (int(rgb[0]), int(rgb[1]), int(rgb[2])) + + """ + This sets the "root" acquisition so the colour/level is + whatever it is when nothing else has acquired + """ + self.acquired_controls[control.id].set_value(value) + + """ + Also create a temporary acquisition to override any + other acquisitions, such as profile/bank levels + """ + if control.id in self.temp_acquired_controls: + acq = self.temp_acquired_controls[control.id] + if acq.is_active(): + self.driver.release_control(acq) + acq = self.driver.acquire_control(control, release_after=3.0, val = value) + self.temp_acquired_controls[control.id] = acq + acq.set_value(value) + + break + self.redraw() + + def request_defeat_profile_change(self): + self.defeat_profile_change += 1 + + def release_defeat_profile_change(self): + if self.defeat_profile_change < 1: + raise Exception("Cannot release defeat profile change if not requested") + self.defeat_profile_change -= 1 + + def driver_changed(self, client, connection_id, entry, args): + if self.reconnect_timer: + self.reconnect_timer.cancel() + if self.driver == None or self.driver.id != entry.value.get_string(): + g15scheduler.schedule("DriverChange", 1.0, self._reload_driver) + + def active_profile_changed(self, client, connection_id, entry, args): + # Check if the active profile has change) + new_profile = g15profile.get_active_profile(self.device) + if new_profile == None: + logger.info("No profile active") + self.deactivate_profile() + else: + logger.info("Active profile changed to %s", new_profile.name) + self.activate_profile() + self.set_color_for_mkey() + g15scheduler.schedule("ProfileChange", 1.0, self._check_active_plugins) + + return 1 + + def activate_profile(self): + logger.debug("Activating profile") + + if self.driver and self.driver.is_connected(): + self.set_memory_bank(1) + + def _network_state_change(self, new_state): + g15scheduler.schedule("ProfileChange", 1.0, self._check_active_plugins) + + def _profile_changed(self, profile_id, device_uid): + self.set_color_for_mkey() + g15scheduler.schedule("ProfileChange", 1.0, self._check_active_plugins) + + def deactivate_profile(self): + logger.debug("De-activating profile") + if self.driver and self.driver.is_connected(): + self.set_memory_bank(0) + + def clear_attention(self): + logger.debug("Clearing attention") + self.attention = False + for listener in self.screen_change_listeners: + g15pythonlang.call_if_exists(listener, "attention_cleared") + + def request_attention(self, message=None): + logger.debug("Requesting attention '%s'", message) + self.attention = True + if message != None: + self.attention_message = message + + for listener in self.screen_change_listeners: + g15pythonlang.call_if_exists(listener, "attention_requested", message) + + def handle_key(self, keys, state_id, post): + """ + Do not call. This is invoked by the key handler + + Keyword arguments: + keys -- list of keys + state_id -- key state ID (g15driver.KEY_STATED_UP, _DOWN and _HELD) + """ + + self.resched_cycle() + + # Next it goes to the visible page + visible = self.get_visible_page() + if visible != None: + for h in visible.key_handlers: + if h.handle_key(keys, state_id, post): + return True + + # Now to all the plugins + if self.plugins.handle_key(keys, state_id, post=post): + return True + + def action_performed(self, binding): + if binding.action == g15driver.MEMORY_1: + self.set_memory_bank(1) + return True + elif binding.action == g15driver.MEMORY_2: + self.set_memory_bank(2) + return True + elif binding.action == g15driver.MEMORY_3: + self.set_memory_bank(3) + return True + elif binding.action == g15actions.NEXT_SCREEN: + self.cycle(1, True) + return True + elif binding.action == g15actions.PREVIOUS_SCREEN: + self.cycle(-1, True) + return True + elif binding.action == g15actions.NEXT_BACKLIGHT: + self.cycle_backlight(1) + return True + elif binding.action == g15actions.PREVIOUS_BACKLIGHT: + self.cycle_backlight(-1) + return True + + ''' + Private + ''' + + + def _init_screen(self): + logger.info("Starting screen") + self.pages = [] + self.content_surface = None + self.width = self.driver.get_size()[0] + self.height = self.driver.get_size()[1] + + self.surface = cairo.ImageSurface (cairo.FORMAT_ARGB32, self.width, self.height) + self.size = (self.width, self.height) + self.available_size = (0, 0, self.size[0], self.size[1]) + + self.page_model_lock = threading.RLock() + self.draw_lock = threading.Lock() + self.visible_page = None + self.old_canvas = None + self.transition_function = None + self.painter_function = None + self.mkey = 1 + self.reverting = { } + self.deleting = { } + self._do_redraw() + + def _control_changed(self, client, connection_id, entry, args): + control_id = entry.get_key().split("/")[-1] + control = self.driver.get_control(control_id) + control.set_from_configuration(self.driver.device, self.conf_client) + if self.visible_page: + self.visible_page.mark_dirty() + + def _cancel_timer(self): + self.reschedule_lock.acquire() + try: + if self.cycle_timer: + self.cycle_timer.cancel() + self.cycle_timer = None + finally: + self.reschedule_lock.release() + + def _process_exception(self, exception): + self.last_error = exception + self.request_attention(str(exception)) + self.resched_cycle() + if self.driver is not None: + for listener in self.screen_change_listeners: + g15pythonlang.call_if_exists(listener, "driver_connection_failed", self.driver, exception) + self.driver = None + if self.should_reconnect(exception): + logger.debug("Could not gracefully process exception.", exc_info = exception) + self.attempt_connection(5.0) + else: + return True + + def _reload_driver(self): + logger.info("Reloading driver") + if self.driver and self.driver.is_connected() : + self.driver.disconnect() + # Let any clients receive their disconnecting. Driver changes should be rare so this is not a big deal + time.sleep(2.0) + self._load_driver() + if self.driver: + self.attempt_connection(0.0) + + def _load_driver(self): + # Get the driver. If it is not configured, configuration will be required at this point + try : + self.driver = g15drivermanager.get_driver(self.conf_client, self.device, on_close=self.on_driver_close) + self.driver.on_driver_options_change = self._reload_driver + return True + except Exception as e: + logger.debug("Error loading driver", exc_info = e) + self._process_exception(e) + self.driver = None + return False + + def _should_deactivate(self, profile, mod): + import g15pluginmanager + """ + Determine if a plugin should be deactivated based on the current profile + and the state of the network + + Keyword arguments: + profile -- profile to check + mod -- plugin module + """ + return profile.plugins_mode == g15profile.NO_PLUGINS or \ + ( profile.plugins_mode == g15profile.SELECTED_PLUGINS and not mod.id in profile.selected_plugins) or \ + ( g15pluginmanager.is_needs_network(mod) and not self.service.network_manager.is_network_available()) + + def _should_activate(self, profile, mod): + import g15pluginmanager + """ + Determine if a plugin should be activated based on the current profile + and the state of the network + + Keyword arguments: + profile -- profile to check + mod -- plugin module + """ + needs_net = g15pluginmanager.is_needs_network(mod) + return ( profile.plugins_mode == g15profile.ALL_PLUGINS or \ + ( profile.plugins_mode == g15profile.SELECTED_PLUGINS and mod.id in profile.selected_plugins) ) and \ + ( not needs_net or ( needs_net and self.service.network_manager.is_network_available() ) ) + + def _check_active_plugins(self, splash=None, startup=False): + if self.driver is None or not self.driver.is_connected(): + logger.info("Ignoring change in plugin state, not connected") + return + + to_activate = [] + choose_profile = g15profile.get_active_profile(self.device) + + """ + Decide what plugins should de-activated or activated + """ + + if not startup: + """ + We don't need to deactivate during startup, nothing will be activated + """ + l = [] + for plugin in self.plugins.activated: + mod = self.plugins.plugin_map[plugin] + if self._should_deactivate(choose_profile, mod): + l.append(plugin) + for plugin in l: + self.plugins.deactivate(plugin=plugin) + + for plugin in self.plugins.started: + if not plugin in self.plugins.activated: + mod = self.plugins.plugin_map[plugin] + if self._should_activate(choose_profile, mod): + to_activate.append(plugin) + + """ + If this is happening during startup, then activate the actual + plugin manager (i.e. a list of plugins or None). Otherwise, + only activate the individual plugins + """ + if startup: + self.plugins.activate(splash, plugin=to_activate) + else: + for plugin in to_activate: + self.plugins.activate(plugin=plugin) + + def error_on_keyboard_display(self, text, title="Error", icon="dialog-error"): + page = g15theme.ErrorScreen(self, title, text, icon) + return page + + def error(self, error_text=None): + self.attention(error_text) + + def on_driver_close(self, driver, retry=True): + logger.info("Driver closed") + + for handle in self.control_handles: + self.conf_client.notify_remove(handle); + self.control_handles = [] + self.acquired_controls = {} + self.memory_bank_color_control = None + if self.plugins.is_activated(): + self.plugins.deactivate() + + # Delete any remaining pages + if self.pages: + for page in list(self.pages): + self.del_page(page) + + for listener in self.screen_change_listeners: + g15pythonlang.call_if_exists(listener, "driver_disconnected", driver) + + if not self.service.shutting_down and not self.stopping: + if retry: + logger.info("Testing if connection should be retried") + self._process_exception(NotConnectedException("Keyboard driver disconnected.")) + + self.stopping = False + logger.info("Completed closing driver") + + def is_active(self): + """ + Get if the driver is active. + """ + return self.driver != None and self.driver.is_connected() + + def is_visible(self, page): + return self._get_next_page_to_display() == page + + def get_visible_page(self): + return self.visible_page + + def has_page(self, page): + return self.get_page(page.id) != None + + def set_painter(self, painter): + o_painter = self.painter_function + self.painter_function = painter + return o_painter + + def set_transition(self, transition): + o_transition = self.transition_function + self.transition_function = transition + return o_transition + + def cycle_to(self, page, transitions=True): + g15scheduler.clear_jobs(REDRAW_QUEUE) + g15scheduler.execute(REDRAW_QUEUE, "cycleTo", self._do_cycle_to, page, transitions) + + def cycle(self, number, transitions=True): + g15scheduler.clear_jobs(REDRAW_QUEUE) + g15scheduler.execute(REDRAW_QUEUE, "doCycle", self._do_cycle, number, transitions) + + def redraw(self, page=None, direction="up", transitions=True, redraw_content=True, queue=True): + if page: + logger.debug("Redrawing %s", page.id) + else: + logger.debug("Redrawing current page") + if queue: + g15scheduler.execute(REDRAW_QUEUE, "redraw", self._do_redraw, page, direction, transitions, redraw_content) + else: + self._do_redraw(page, direction, transitions, redraw_content) + + + def set_color_for_mkey(self): + control = self.driver.get_control_for_hint(g15driver.HINT_DIMMABLE) + rgb = None + if control != None and not isinstance(control.value, int): + profile = g15profile.get_active_profile(self.device) + if profile != None: + rgb = profile.get_mkey_color(self.mkey) + + if rgb is not None: + if self.memory_bank_color_control is None: + self.memory_bank_color_control = self.driver.acquire_control(control) + self.memory_bank_color_control.set_value(rgb) + elif self.memory_bank_color_control is not None: + self.driver.release_control(self.memory_bank_color_control) + self.memory_bank_color_control = None + + def get_current_surface(self): + return self.local_data.surface + + def get_desktop_scale(self): + sx = float(self.available_size[2]) / float(self.width) + sy = float(self.available_size[3]) / float(self.height) + return min(sx, sy) + + def fade(self, stay_faded=False, duration=4.0, step=1): + self.fader = Fader(self, stay_faded=stay_faded, duration=duration, step=step).run() + + def attempt_connection(self, delay=0.0): + logger.debug("Attempting connection in %f", delay) + self.connection_lock.acquire() + try : + if self.reconnect_timer is not None: + self.reconnect_timer.cancel() + + if not self.service.session_active: + logger.debug("Desktop session not active, will not connect to driver") + return + + # With a G510, it's possible the keyboard is now in audio mode, so + # the device we use has changed + new_device = g15devices.get_device(self.device.uid) + if new_device is not None and self.device.controls_usb_id != new_device.controls_usb_id: + logger.info("Device changed, probably a G510 switching to or from audio mode") + self.device = new_device + self.driver = None + + if self.driver == None: + if not self._load_driver(): + raise + + if self.driver.is_connected(): + logger.warning("WARN: Attempt to reconnect when already connected.") + return + + if not self._started_plugins: + self.plugins.start() + self._started_plugins = True + + self.loading_complete = False + self.first_page = self.conf_client.get_string("/apps/gnome15/%s/last_page" % self.device.uid) + + if delay != 0.0: + self.reconnect_timer = g15scheduler.schedule("ReconnectTimer", delay, self.attempt_connection) + return + + try : + + if not self.driver.allow_multiple: + # Look for other screens using the same driver + for s in self.service.screens: + if s.driver is not None and self.driver != s.driver and \ + ( s.driver.is_connected() or s.driver.connecting ) and \ + s.driver.get_name() == self.driver.get_name(): + raise RetryException("Driver %s only allows one device at a time" % s.driver.get_name()) + + self.acquired_controls = {} + self.driver.zeroize_all_controls() + self.driver.connect() + self.driver.release_all_acquisitions() + for control in self.driver.get_controls(): + control.set_from_configuration(self.driver.device, self.conf_client) + self.acquired_controls[control.id] = self.driver.acquire_control(control, val=control.value) + logger.info("Acquired control of %s with value of %s", + control.id, + str(control.value)) + self.control_handles.append(self.conf_client.notify_add("/apps/gnome15/%s/%s" % (self.device.uid, control.id), self.control_configuration_changed)); + self.driver.update_controls() + self._init_screen() + if self.splash == None: + if self.driver.get_bpp() > 0: + self.splash = G15Splash(self, self.conf_client) + else: + self.splash.update_splash(0, 100, "Starting up ..") + self.set_memory_bank(1) + self.activate_profile() + self.last_error = None + for listener in self.screen_change_listeners: + g15pythonlang.call_if_exists(listener, "driver_connected", self.driver) + + self.complete_loading() + + except Exception as e: + logger.debug("Error attenpting connection", exc_info = e) + if self._process_exception(e): + raise + finally: + self.connection_lock.release() + + logger.debug("Connection for %s is complete.", self.device.uid) + + def clear_canvas(self, canvas): + """ + Clears a canvas, filling it with the current background color, and setting the canvas + paint color to the current foreground color + """ + rgb = self.driver.get_color_as_ratios(g15driver.HINT_BACKGROUND, (255, 255, 255)) + canvas.set_source_rgb(rgb[0], rgb[1], rgb[2]) + canvas.rectangle(0, 0, self.width, self.height) + canvas.fill() + rgb = self.driver.get_color_as_ratios(g15driver.HINT_FOREGROUND, (0, 0, 0)) + canvas.set_source_rgb(rgb[0], rgb[1], rgb[2]) + self.configure_canvas(canvas) + + def page_title_changed(self, page, title): + """ + Tell all screen listeners a page title has changed + + Keyword arguments: + page -- page object + title -- new title + """ + for l in self.screen_change_listeners: + g15pythonlang.call_if_exists(l, "title_changed", page, title) + + ''' + Private functions + ''' + + def _draw_page(self, visible_page, direction="down", transitions=True, redraw_content=True): + self.draw_lock.acquire() + try: + if self.driver == None or not self.driver.is_connected(): + return + + # Do not paint if the device has no LCD (i.e. G110) + if self.driver.get_bpp() == 0: + return + + surface = self.surface + + painters = sorted(self.painters, key=lambda painter: painter.z_order) + + # If the visible page is changing, creating a new surface. Both surfaces are + # then passed to any transition functions registered + if visible_page != self.visible_page: + logger.debug("Page has changed, recreating surface") + if visible_page.priority == PRI_NORMAL and not self.stopping: + self.service.conf_client.set_string("/apps/gnome15/%s/last_page" % self.device.uid, visible_page.id) + surface = cairo.ImageSurface (cairo.FORMAT_ARGB32, self.width, self.height) + + self.local_data.surface = surface + canvas = cairo.Context (surface) + self.clear_canvas(canvas) + + # Background painters + for painter in painters: + if painter.place == BACKGROUND_PAINTER: + painter.paint(canvas) + + old_page = None + if visible_page != self.visible_page: + old_page = self.visible_page + redraw_content = True + if self.visible_page != None: + self.visible_page = visible_page + old_page._do_on_hidden() + else: + self.visible_page = visible_page + if self.visible_page != None: + self.visible_page._do_on_shown() + + self.resched_cycle() + for l in self.screen_change_listeners: + g15pythonlang.call_if_exists(l, "page_changed", self.visible_page) + + # Call the screen's painter + if self.visible_page != None: + logger.debug("Drawing page %s " \ + "(direction = %s, transitions = %s, redraw_content = %s", + self.visible_page.id, + direction, + str(transitions), + str(redraw_content)) + + + # Paint the content to a new surface so it can be cached + if self.content_surface == None or redraw_content: + self.content_surface = cairo.ImageSurface (cairo.FORMAT_ARGB32, self.width, self.height) + content_canvas = cairo.Context(self.content_surface) + self.configure_canvas(content_canvas) + self.visible_page.paint(content_canvas) + + tx = self.available_size[0] + ty = self.available_size[1] + + # Scale to the available space, and center + sx = float(self.available_size[2]) / float(self.width) + sy = float(self.available_size[3]) / float(self.height) + scale = min(sx, sy) + sx = scale + sy = scale + + if tx == 0 and self.available_size[3] != self.size[1]: + sx = 1 + + if ty == 0 and self.available_size[2] != self.size[0]: + sy = 1 + + canvas.save() + canvas.translate(tx, ty) + canvas.scale(sx, sy) + canvas.set_source_surface(self.content_surface) + canvas.paint() + canvas.restore() + + """ + Glass pane (components a bit like foreground painters in that + they paint over the top of pages + """ + self.glass_pane.paint(canvas) + + # Foreground painters + for painter in painters: + if painter.place == FOREGROUND_PAINTER: + painter.paint(canvas) + + # Run any transitions + if transitions and self.transition_function != None and self.old_canvas != None: + self.transition_function(self.old_surface, surface, old_page, self.visible_page, direction) + + # Now apply any global transformations and paint + if self.painter_function != None: + self.painter_function(surface) + else: + self.driver.paint(surface) + + self.old_canvas = canvas + self.old_surface = surface + finally: + self.draw_lock.release() + + def configure_canvas(self, canvas): + canvas.set_antialias(self.driver.get_antialias()) + fo = cairo.FontOptions() + fo.set_antialias(self.driver.get_antialias()) + if self.driver.get_antialias() == cairo.ANTIALIAS_NONE: + fo.set_hint_style(cairo.HINT_STYLE_NONE) + fo.set_hint_metrics(cairo.HINT_METRICS_OFF) + canvas.set_font_options(fo) + return fo + + def _do_cycle_to(self, page, transitions=True): + self.page_model_lock.acquire() + try : + if page.priority == PRI_LOW: + # Visible until the next popup, or it hides itself + self.set_priority(page, PRI_POPUP) + elif page.priority < PRI_LOW: + self.clear_popup() + # Up to the page to make itself stay visible + self._draw_page(page, "down", transitions) + else: + self.clear_popup() + self._flush_reverts_and_deletes() + # Cycle within pages of the same priority + page_list = self._get_pages_of_priority(page.priority) + direction = "up" + direction_val = 1 + diff = page_list.index(page) + if diff >= (len(page_list) / 2): + direction_val *= -1 + direction = "down" + self._cycle_pages(diff, page_list) + self._do_redraw(page, direction=direction, transitions=transitions) + finally: + self.page_model_lock.release() + + def _do_cycle(self, number, transitions=True): + self.page_model_lock.acquire() + try : + self._flush_reverts_and_deletes() + self._cycle(number, transitions) + direction = "up" + if number < 0: + direction = "down" + self._do_redraw(self._get_next_page_to_display(), direction=direction, transitions=transitions) + finally: + self.page_model_lock.release() + + def _get_pages_of_priority(self, priority): + p_pages = [] + for page in self._sort(): + if page.priority == PRI_NORMAL: + p_pages.append(page) + return p_pages + + def _cycle_pages(self, number, pages): + if len(pages) > 0: + if number < 0: + for _ in range(number, 0): + first_time = pages[0].time + for i in range(0, len(pages) - 1): + pages[i].set_time(pages[i + 1].time) + pages[len(pages) - 1].set_time(first_time) + else: + for _ in range(0, number): + last_time = pages[len(pages) - 1].time + for i in range(len(pages) - 1, 0, -1): + pages[i].set_time(pages[i - 1].time) + pages[0].set_time(last_time) + + def _cycle(self, number, transitions=True): + if len(self.pages) > 0: + self._cycle_pages(number, self._get_pages_of_priority(PRI_NORMAL)) + + def _do_redraw(self, page=None, direction="up", transitions=True, redraw_content=True): + self.page_model_lock.acquire() + try : + current_page = self._get_next_page_to_display() + if page == None or page == current_page: + self._draw_page(current_page, direction, transitions, redraw_content) + elif page != None and page.panel_painter != None: + self._draw_page(current_page, direction, transitions, False) + finally: + self.page_model_lock.release() + + def _flush_reverts_and_deletes(self): + self.page_model_lock.acquire() + try : + for page_id in self.reverting: + (old_priority, timer) = self.reverting[page_id] + timer.cancel() + self.set_priority(self.get_page(page_id), old_priority) + self.reverting = {} + for page_id in list(self.deleting.keys()): + timer = self.deleting[page_id] + timer.cancel() + self.del_page(self.get_page(page_id)) + self.deleting = {} + finally: + self.page_model_lock.release() + + def _sort(self): + return sorted(self.pages, key=lambda page: page.value, reverse=True) + + def _get_next_page_to_display(self): + self.page_model_lock.acquire() + try : + srt = sorted(self.pages, key=lambda key: key.value, reverse=True) + if len(srt) > 0 and srt[0].priority != PRI_INVISIBLE: + return srt[0] + finally: + self.page_model_lock.release() + +""" +Fades the screen by inserting a foreground painter that paints a transparent +black rectangle over the top of everything. The opacity is this gradually +increased, creating a fading effect +""" +class Fader(Painter): + + def __init__(self, screen, stay_faded=False, duration=3.0, step=1): + Painter.__init__(self, FOREGROUND_PAINTER, 9999) + self.screen = screen + self.duration = duration + self.opacity = 0 + self.step = step + self.stay_faded = stay_faded + self.interval = (duration / 255) * step + + def run(self): + self.screen.painters.append(self) + try: + while self.opacity <= 255: + self.screen.redraw(redraw_content = False) + time.sleep(self.interval) + finally: + if not self.stay_faded: + self.screen.painters.remove(self) + + def paint(self, canvas): + # Fade to black on the G19, or white on everything else + if self.screen.driver.get_bpp() == 1: + col = 1.0 + else: + col = 0.0 + canvas.set_source_rgba(col, col, col, float(self.opacity) / 255.0) + canvas.rectangle(0, 0, self.screen.width, self.screen.height) + canvas.fill() + self.opacity += self.step + +class G15Splash(): + + def __init__(self, screen, gconf_client): + self.screen = screen + self.progress = 0.0 + self.text = _("Starting up ..") + icon_path = g15icontools.get_icon_path("gnome15") + if icon_path == None: + icon_path = os.path.join(g15globals.icons_dir, "hicolor", "apps", "scalable", "gnome15.svg") + self.logo = g15cairo.load_surface_from_file(icon_path) + self.page = g15theme.G15Page("Splash", self.screen, priority=PRI_EXCLUSIVE, thumbnail_painter=self._paint_thumbnail, \ + theme_properties_callback=self._get_properties, theme=g15theme.G15Theme(g15globals.image_dir, "background")) + self.screen.add_page(self.page) + + def complete(self): + self.progress = 100 + self.screen.redraw(self.page) + g15scheduler.queue(REDRAW_QUEUE, "ClearSplash", 2.0, self._hide) + + def update_splash(self, value, max_value, text=None): + self.progress = (float(value) / float(max_value)) * 100.0 + self.screen.redraw(self.page) + if text != None: + self.text = text + + def _get_properties(self): + return { "version": g15globals.version, + "progress": self.progress, + "text": self.text + } + + def _paint_thumbnail(self, canvas, allocated_size, horizontal): + return g15cairo.paint_thumbnail_image(allocated_size, self.logo, canvas) + + def _hide(self): + self.screen.del_page(self.page) + self.screen.redraw() diff --git a/src/gnome15/g15service.py b/src/gnome15/g15service.py new file mode 100644 index 0000000..8630af0 --- /dev/null +++ b/src/gnome15/g15service.py @@ -0,0 +1,1138 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2010 Brett Smith +# Copyright (C) 2013 Brett Smith +# Nuno Araujo +# NoXPhasma +# +# 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 . + +import sys +import pygtk +from gnome15 import g15accounts +pygtk.require('2.0') +import os +import gobject +import g15globals +import g15screen +import g15profile +import g15dbus +import g15devices +import g15desktop +import g15uinput +import g15network +import g15accounts +import g15driver +import gconf +import util.g15scheduler as g15scheduler +import util.g15gconf as g15gconf +import util.g15os as g15os +import Xlib.X +import Xlib.ext +import Xlib.XK +import Xlib.display +import Xlib.protocol +import time +import dbus +import signal +import g15pluginmanager +import g15actions +from threading import Thread +import gtk.gdk + +# Logging +import logging +logger = logging.getLogger(__name__) + +# Used for getting logout / shutdown signals +master_client = None +if g15desktop.get_desktop() in [ "gnome", "gnome-shell" ]: + try: + import gnome.ui + master_client = gnome.ui.master_client() + except Exception as e: + logger.debug("Could not get gnome master client", exc_info = e) + pass + +# Upgrade +import g15upgrade +g15upgrade.upgrade() + + +NAME = "Gnome15" +VERSION = g15globals.version +SERVICE_QUEUE = "serviceQueue" +MACRO_HANDLER_QUEUE = "macroHandler" + +special_X_keysyms = { + ' ' : "space", + '\t' : "Tab", + '\n' : "Return", # for some reason this needs to be cr, not lf + '\r' : "Return", + '\e' : "Escape", + '\b' : "BackSpace", + '!' : "exclam", + '#' : "numbersign", + '%' : "percent", + '$' : "dollar", + '&' : "ampersand", + '"' : "quotedbl", + '\'' : "apostrophe", + '(' : "parenleft", + ')' : "parenright", + '*' : "asterisk", + '=' : "equal", + '+' : "plus", + ',' : "comma", + '-' : "minus", + '.' : "period", + '/' : "slash", + ':' : "colon", + ';' : "semicolon", + '<' : "less", + '>' : "greater", + '?' : "question", + '@' : "at", + '[' : "bracketleft", + ']' : "bracketright", + '\\' : "backslash", + '^' : "asciicircum", + '_' : "underscore", + '`' : "grave", + '{' : "braceleft", + '|' : "bar", + '}' : "braceright", + '~' : "asciitilde" + } + +class CheckThread(Thread): + def __init__(self, device, check_function, quickly): + Thread.__init__(self) + self.name = "CheckDeviceState%s" % device.uid + self.device = device + self.quickly = quickly + self.check_function = check_function + self.start() + + def run(self): + self.check_function(self.device, self.quickly) + +class StartThread(Thread): + def __init__(self, screen): + Thread.__init__(self) + self.name = "StartScreen%s" % screen.device.uid + self.screen = screen + self.error = None + + def run(self): + try: + self.screen.start() + except Exception as e: + logger.error("Failed to start screen.", exc_info = e) + self.error = e + +class MacroHandler(object): + + def __init__(self): + self.buffered_executions = [] + self.cancelled = False + self.use_x_test = None + self.x_test_available = None + self.window = None + + def cancel(self): + """ + Cancel the currently running macro script if any. This script may + not immediately be cancelled if there are un-interuptable tasks running. + """ + self.cancelled = True + + def handle_key(self, keys, state_id, post): + """ + Handle raw keys. We use this to complete any macros waiting for another + key events + """ + g15scheduler.queue(MACRO_HANDLER_QUEUE, "HandleMacro", 0, self._do_handle_key, keys, state_id, post) + + def handle_macro(self, macro): + """ + We want to return control immediately after asking for a macro + to be handled, but we only ever want one macro running at a time. + This means the macro action is put on it's own queue. This also + allows long running macros to be cancelled + + Keyword arguments: + macro -- macro to handle + """ + g15scheduler.queue(MACRO_HANDLER_QUEUE, "HandleMacro", 0, self._do_handle, macro) + + def get_x_display(self): + self.init_xtest() + return self.local_dpy + + def init_xtest(self): + """ + Initialise XTEST if it is available. + """ + if self.x_test_available == None: + logger.info("Initialising macro output system") + + # Load Python Virtkey if it is available + + # Use python-virtkey for preference + + self.virtual_keyboard = None + try: + import virtkey + self.virtual_keyboard = virtkey.virtkey() + self.x_test_available = False + except Exception as e: + logger.warning("No python-virtkey, macros may be weird. Trying XTest", exc_info = e) + + # Determine whether to use XTest for sending key events to X + self.x_test_available = True + try : + import Xlib.ext.xtest + except ImportError as e: + logger.warning("No XTest, falling back to raw X11 events", exc_info = e) + self.x_test_available = False + + self.local_dpy = Xlib.display.Display() + + if self.x_test_available and not self.local_dpy.query_extension("XTEST") : + logger.warning("Found XTEST module, but the X extension could not be found") + self.x_test_available = False + + def send_string(self, ch, press): + """ + Sends a string (character) to the X server as if it was typed. + Depending on the configuration virtkey, XTEST or raw events may be used + + Keyword arguments: + ch -- character to send + press -- boolean indicating if this is a PRESS or RELEASE + """ + logger.debug("Sending string %s", ch) + + if self.virtual_keyboard is not None: + keysym = self._get_keysym(ch) + if press: + logger.debug("Sending keychar %s press = %s, keysym = %d (%x)", + ch, + press, + keysym, + keysym) + self.virtual_keyboard.press_keysym(keysym) + else: + self.virtual_keyboard.release_keysym(self._get_keysym(ch)) + else: + keycode, shift_mask = self._char_to_keycodes(ch) + logger.debug("Sending keychar %s keycode %d, press = %s, shift = %d", + ch, + int(keycode), + str(press), + shift_mask) + if (self.x_test_available and self.use_x_test) : + if press: + if shift_mask != 0 : + Xlib.ext.xtest.fake_input(self.local_dpy, Xlib.X.KeyPress, 62) + Xlib.ext.xtest.fake_input(self.local_dpy, Xlib.X.KeyPress, keycode) + else: + Xlib.ext.xtest.fake_input(self.local_dpy, Xlib.X.KeyRelease, keycode) + if shift_mask != 0 : + Xlib.ext.xtest.fake_input(self.local_dpy, Xlib.X.KeyRelease, 62) + else : + if press: + event = Xlib.protocol.event.KeyPress( + time=int(time.time()), + root=self.local_dpy.screen().root, + window=self.window, + same_screen=0, child=Xlib.X.NONE, + root_x=0, root_y=0, event_x=0, event_y=0, + state=shift_mask, + detail=keycode + ) + self.window.send_event(event, propagate=True) + else: + event = Xlib.protocol.event.KeyRelease( + time=int(time.time()), + root=self.local_dpy.screen().root, + window=self.window, + same_screen=0, child=Xlib.X.NONE, + root_x=0, root_y=0, event_x=0, event_y=0, + state=shift_mask, + detail=keycode + ) + self.window.send_event(event, propagate=True) + self.local_dpy.sync() + + def send_simple_macro(self, macro): + logger.debug("Simple macro '%s'", macro.macro) + esc = False + i = 0 + + press_delay = 0.0 if not macro.profile.fixed_delays else ( float(macro.profile.press_delay) / 1000.0 ) + release_delay = 0.0 if not macro.profile.fixed_delays else ( float(macro.profile.release_delay) / 1000.0 ) + + for c in macro.macro: + if self.cancelled: + logger.warning("Macro cancelled.") + break + if c == '\\' and not esc: + esc = True + else: + if esc and c == 'p': + time.sleep(release_delay + press_delay) + else: + if i > 0: + logger.debug("Release delay of %f", release_delay) + time.sleep(release_delay) + + if esc and c == 't': + c = '\t' + elif esc and c == 'r': + c = '\r' + elif esc and c == 'n': + c = '\r' + elif esc and c == 'b': + c = '\b' + elif esc and c == 'e': + c = '\e' + elif esc and c == '\\': + c = '\\' + + if c in special_X_keysyms: + c = special_X_keysyms[c] + + self.send_string(c, True) + time.sleep(press_delay) + logger.debug("Press delay of %f", press_delay) + self.send_string(c, False) + + i += 1 + + esc = False + + def press_delay(self, macro): + delay = 0.0 if not macro.profile.fixed_delays else ( float(macro.profile.press_delay) / 1000.0 ) + logger.debug("Press delay of %f", delay) + time.sleep(delay) + + def release_delay(self, macro): + delay = 0 if not macro.profile.fixed_delays else ( float(macro.profile.release_delay) / 1000.0 ) + logger.debug("Release delay of %f", delay) + time.sleep(delay) + + + def action_performed(self, binding): + if binding.action == g15actions.CANCEL_MACRO: + self.cancel() + return True + + def _do_handle_key(self, keys, state_id, post): + for b in list(self.buffered_executions): + if b.handle_key(keys, state_id, post): + """ + The keys that activated this macro are now all in the required state, + so continue execution + """ + self.buffered_executions.remove(b) + wait_for_state = b.execute() + if wait_for_state: + self.buffered_executions.append(b) + + def _get_keysym(self, ch) : + keysym = Xlib.XK.string_to_keysym(ch) + if keysym == 0 : + # Unfortunately, although this works to get the correct keysym + # i.e. keysym for '#' is returned as "numbersign" + # the subsequent display.keysym_to_keycode("numbersign") is 0. + if ch in special_X_keysyms: + keysym_name = special_X_keysyms[ch] + keysym = Xlib.XK.string_to_keysym(keysym_name) + return keysym + + def _char_to_keycodes(self, ch): + """ + Convert a character from a string into an X11 keycode when possible. + + Keyword arguments: + ch -- character to convert + """ + self.init_xtest() + shift_mask = 0 + + if str(ch).startswith("["): + keysym_code = int(ch[1:-1]) + # AltGr + if keysym_code == 65027: + keycode = 108 + else: + logger.warning("Unknown keysym %d", keysym_code) + keycode = 0 + else: + + keysym = self._get_keysym(ch) + + x_keycodes = self.local_dpy.keysym_to_keycodes(keysym) + keycode = 0 if keysym == 0 else self.local_dpy.keysym_to_keycode(keysym) + + # I have no idea how accurate this is, but it seems more so that + # the is_shifted() function + if keysym < 256: + for x in x_keycodes: + if x[1] == 1: + shift_mask = Xlib.X.ShiftMask + + if keycode == 0 : + logger.warning("Sorry, can't map (character %d)", ord(ch)) + + return keycode, shift_mask + + def _do_handle(self, macro): + + # Get the latest focused window if not using XTest + self.cancelled = False + self.init_xtest() + if self.virtual_keyboard is None and ( not self.use_x_test or not self.x_test_available ): + self.window = self.local_dpy.get_input_focus()._data["focus"]; + + if macro.type == g15profile.MACRO_COMMAND: + logger.warning("Running external command '%s'", macro.macro) + os.system(macro.macro) + elif macro.type == g15profile.MACRO_SIMPLE: + self.send_simple_macro(macro) + else: + executor = MacroScriptExecution(macro, self) + wait_for_state = executor.execute() + if wait_for_state: + self.buffered_executions.append(executor) + +class MacroScriptExecution(object): + + def __init__(self, macro, handler): + self.macro = macro + self.handler = handler + self.l = -1 + self.macros = self.macro.macro.split("\n") + self.wait_for_state = -2 + self.wait_for_keys = [] + self.down = 0 + self.all_keys_up = False + self.cancelled = False + + # First parse to get where the labels are + self.labels = {} + for l in range(0, len(self.macros)): + macro_text = self.macros[l] + split = macro_text.split(" ") + op = split[0].lower() + if op == "label" and len(op) > 1: + self.labels[split[1].lower()] = l + + def handle_key(self, keys, state_id, post): + + """ + If we get the state we are waiting for, OR if we get an UP before + getting a HELD, we remove this key from this key from the list we are waiting for + """ + if state_id == self.wait_for_state or state_id == g15driver.KEY_STATE_UP and self.wait_for_state == g15driver.KEY_STATE_HELD: + for k in keys: + self.wait_for_keys.remove(k) + + if len(self.wait_for_keys) == 0: + # All keys are now in the required state + if state_id == g15driver.KEY_STATE_UP and self.wait_for_state == g15driver.KEY_STATE_HELD: + # We should cancel execution now + self.cancelled = True + if state_id == g15driver.KEY_STATE_UP: + # Make a note of the fact all triggering keys are now up + self.all_keys_up = True + return True + + def execute(self): + while True: + if self.down == 0 and ( self.handler.cancelled or self.cancelled ): + logger.warning("Macro cancelled") + break + self.l += 1 + if self.l == len(self.macros): + break + macro_text = self.macros[self.l] + split = macro_text.split(" ") + op = split[0].lower() + if len(split) > 1: + val = split[1] + if op == "goto": + val = val.lower() + if val in self.labels: + self.l = self.labels[val] + else: + logger.warning("Unknown goto label %s in macro script. Ignoring", val) + elif op == "delay": + if not self.handler.cancelled and self.macro.profile.send_delays and not self.macro.profile.fixed_delays: + time.sleep(float(val) / 1000.0 if not self.macro.profile.fixed_delays else self.macro.profile.delay_amount) + elif op == "press": + if self.down > 0: + self.handler.release_delay(self.macro) + self.handler.send_string(val, True) + self.down += 1 + self.handler.press_delay(self.macro) + elif op == "release": + self.handler.send_string(val, False) + self.down -= 1 + elif op == "upress": + if len(split) < 3: + logger.error("Invalid operation in macro script. '%s'", macro_text) + else: + if self.down > 0: + self.handler.release_delay(self.macro) + self.down += 1 + self._send_uinput(split[2], val, 1) + self.handler.press_delay(self.macro) + elif op == "urelease": + if len(split) < 3: + logger.error("Invalid operation in macro script. '%s'", macro_text) + else: + self.down -= 1 + self._send_uinput(split[2], val, 0) + elif op == "wait": + if self.all_keys_up: + logger.warning("All keys for the macro %s are already up, " \ + "the rest of the script will be ignored", self.macro.name) + return False + else: + val = val.lower() + if val == "release": + if self.macro.activate_on == g15driver.KEY_STATE_UP: + logger.error("WaitRelease cannot be used with macros that activate on release") + else: + self.wait_for_state = g15driver.KEY_STATE_UP + self.wait_for_keys = list(self.macro.keys) + return True + elif val == "hold": + if self.macro.activate_on == g15driver.KEY_STATE_DOWN: + self.wait_for_state = g15driver.KEY_STATE_HELD + self.wait_for_keys = list(self.macro.keys) + return True + else: + logger.error("WaitHold cannot be used with macros that activate on hold or release") + else: + logger.error("Wait may only have an argument of release or hold") + elif op == "label": + # Ignore label / comment + pass + else: + logger.error("Invalid operation in macro script. '%s'", macro_text) + + else: + if len(split) > 0: + logger.error("Insufficient arguments in macro script. '%s'", macro_text) + + + def _send_uinput(self, target, val, state): + if val in g15uinput.capabilities: + g15uinput.emit(target, g15uinput.capabilities[val], state, True) + else: + logger.error("Unknown uinput key %s.", val) + +class G15Service(g15desktop.G15AbstractService): + + def __init__(self, service_host, no_trap=False): + self.exit_on_no_devices = False + self.active_plugins = {} + self.session_active = True + self.service_host = service_host + self.active_window = None + self.shutting_down = False + self.starting_up = True + self.conf_client = gconf.client_get_default() + self.screens = [] + self.started = False + self.service_listeners = [] + self.notify_handles = [] + self.device_notify_handles = {} + self.font_faces = {} + self.stopping = False + self.window_title_listener = None + self.active_application_name = None + self.active_window_title = None + self.ignore_next_sigint = False + self.debug_svg = False + self.devices = g15devices.find_all_devices() + self.macro_handler = MacroHandler() + self.global_plugins = None + + # Expose Gnome15 functions via DBus + logger.debug("Starting the DBUS service") + self.dbus_service = g15dbus.G15DBUSService(self) + + # Watch for signals + if not no_trap: + signal.signal(signal.SIGINT, self.sigint_handler) + signal.signal(signal.SIGTERM, self.sigterm_handler) + signal.signal(signal.SIGUSR1, self.sigusr1_handler) + + g15desktop.G15AbstractService.__init__(self) + self.name = "DesktopService" + + def start_service(self): + try: + self._do_start_service() + except Exception as e: + self.shutdown(True) + logger.error("Failed to start service.", exc_info = e) + + def sigusr1_handler(self, signum, frame): + logger.info("Got SIGUSR1 signal from %s, restarting", str(frame)) + self.restart() + + def sigint_handler(self, signum, frame): + logger.info("Got SIGINT signal from %s, shutting down", str(frame)) + self.shutdown(True) + + def sigterm_handler(self, signum, frame): + logger.info("Got SIGTERM signal from %s, shutting down", str(frame)) + self.shutdown(True) + + def stop(self, quickly = False): + if self.started: + g15accounts.STATUS.stopping = True + self.stopping = True + + g15devices.device_added_listeners.remove(self._device_added) + g15devices.device_removed_listeners.remove(self._device_removed) + g15uinput.close_devices() + self.global_plugins.deactivate() + self.session_active = False + try : + for h in self.notify_handles: + self.conf_client.notify_remove(h) + for h in self.device_notify_handles: + self.conf_client.notify_remove(self.device_notify_handles[h]) + try : + logger.info("Stopping profile change notification") + g15profile.notifier.stop() + except Exception as e: + logger.debug("Error stopping profile change notification", exc_info = e) + pass + try : + logger.info("Stopping account change notification") + g15accounts.notifier.stop() + except Exception as e: + logger.debug("Error stopping account change notification", exc_info = e) + pass + logger.info("Informing listeners we are stopping") + for listener in self.service_listeners: + listener.service_stopping() + logger.info("Stopping screens") + self._check_state_of_all_devices_async(quickly) + logger.info("Screens stopped") + self.started = False + finally : + self.stopping = False + else: + logger.warning("Ignoring stop request, already stopped.") + + def restart(self): + g15os.run_script("g15-desktop-service", ["restart"], background = True) + + def shutdown(self, quickly = False): + logger.info("Shutting down") + self.shutting_down = True + if self.global_plugins is not None: + self.global_plugins.destroy() + self.stop(quickly) + g15scheduler.stop_queue(MACRO_HANDLER_QUEUE) + g15scheduler.stop_queue(SERVICE_QUEUE) + logger.info("Stopping all schedulers") + g15scheduler.stop_all_schedulers() + for listener in self.service_listeners: + listener.service_stopped() + logger.info("Quiting loop") + self.loop.quit() + logger.info("Stopping DBus service") + self.dbus_service.stop() + + def get_active_application_name(self): + return self.active_application_name + + """ + Private + """ + + def _active_window_changed(self, old, object_name): + if object_name != "": + app = self.session_bus.get_object("org.ayatana.bamf", object_name) + view = None + try: + view = dbus.Interface(app, 'org.ayatana.bamf.view') + self.active_application_name = view.Name() + except dbus.DBusException as e: + logger.debug("Could not get current application name", exc_info = e) + self.active_application_name = None + + if view is not None: + screens = list(self.screens) + for s in list(screens): + if self._check_active_application(s, app, view): + screens.remove(s) + + window = dbus.Interface(app, 'org.ayatana.bamf.window') + self.active_window_title = self._get_x_prop(window, '_NET_WM_VISIBLE_NAME') + if not self.active_window_title: + self.active_window_title = self._get_x_prop(window, '_NET_WM_NAME') + for s in list(screens): + if self._check_active_window(s, app, window): + screens.remove(s) + + """ + Start listening for name changes within the view as well + """ + if self.window_title_listener is not None: + self.window_title_listener.remove() + + def _window_title_changed(old_name, new_name): + self.active_window_title = new_name + for s in list(self.screens): + self._check_active_window(s, app, window) + +# self.window_title_listener = view.connect_to_signal('NameChanged', _window_title_changed, None) + + def _get_x_prop(self, window, key): + try : + return window.XProps(key) + except dbus.DBusException as e: + logger.debug("Could not get window XProps", exc_info = e) + return None + + def _check_active_window(self, screen, app, window): + try : + if screen.set_active_application_name(self.active_window_title): + return True + except dbus.DBusException as e: + logger.debug("Could not check active window", exc_info = e) + pass + + def _check_active_application(self, screen, app, view): + try : + if view is not None and view.IsActive() == 1: + vn = view.Name() + if screen.set_active_application_name(vn): + return True + else: + parents = view.Parents() + for parent in parents: + app = self.session_bus.get_object("org.ayatana.bamf", parent) + view = dbus.Interface(app, 'org.ayatana.bamf.view') + if self._check_active_application(screen, app, view): + return True + except dbus.DBusException as e: + logger.debug("Could not check active application", exc_info = e) + pass + + def _check_active_application_with_wnck(self, event=None): + try: + import wnck + window = wnck.screen_get_default().get_active_window() + if window is not None and not window.is_skip_pager(): + app = window.get_application() + active_application_name = app.get_name() if app is not None else "" + if active_application_name != self.active_application_name: + self.active_application_name = active_application_name + self.active_window_title = active_application_name + logger.info("Active application is now %s", self.active_application_name) + for screen in self.screens: + screen.set_active_application_name(active_application_name) + except Exception as e: + logger.warning("Failed to activate profile for active window", exc_info = e) + + gobject.timeout_add(500, self._check_active_application_with_wnck) + + def _check_state_of_all_devices(self, quickly = False): + logger.info("Checking state of %d devices", len(self.devices)) + for d in self.devices: + self._check_device_state(d, quickly) + + def _check_state_of_all_devices_async(self, quickly = False): + logger.info("Checking state of %d devices", len(self.devices)) + t = [] + for d in self.devices: + t.append(CheckThread(d, self._check_device_state, quickly)) + self._join_all(t) + + def _do_start_service(self): + + # Network manager + self.network_manager = g15network.NetworkManager(self) + + # Global plugins + self.session_active = True + self.global_plugins = g15pluginmanager.G15Plugins(None, self, network_manager = self.network_manager) + self.global_plugins.start() + + for listener in self.service_listeners: + listener.service_starting_up() + + # UINPUT + try: + g15uinput.open_devices() + except OSError as e: + logger.debug("Error opening uinput devices", exc_info = e) + if e.errno == 13 or e.errno == 2: + raise Exception("Failed to open uinput devices. Do you have the uinput module loaded (try modprobe uinput), and are the permissions of /dev/uinput correct? If you have just installed Gnome15 for the first time, you may need to simply reboot.") + else: + raise + + self.session_bus = dbus.SessionBus() + self.system_bus = dbus.SystemBus() + + # Create a screen for each device + self.conf_client.add_dir("/apps/gnome15", gconf.CLIENT_PRELOAD_NONE) + logger.info("Looking for devices") + if len(self.devices) == 0: + if g15devices.have_udev and not self.exit_on_no_devices: + logger.error("No devices found yet, waiting for some to appear") + else: + logger.error("No devices found. Gnome15 will now exit") + self.shutdown() + return + else: + # Create the default profile for all devices + for device in self.devices: + g15profile.create_default(device) + + # If there is a single device, it is enabled by default + if len(self.devices) == 1: + self.conf_client.set_bool("/apps/gnome15/%s/enabled" % self.devices[0].uid, True) + + errors = 0 + for device in self.devices: + val = self.conf_client.get("/apps/gnome15/%s/enabled" % device.uid) + h = self.conf_client.notify_add("/apps/gnome15/%s/enabled" % device.uid, self._device_enabled_configuration_changed, device) + self.device_notify_handles[device.uid] = h + if ( val == None and device.model_id != "virtual" ) or ( val is not None and val.get_bool() ): + screen = self._add_screen(device) + if not screen: + errors += 1 + + if len(self.devices) == errors: + logger.error("All screens failed to load. Shutting down") + self.shutdown() + return + + if len(self.screens) == 0: + logger.warning("No screens found yet. Will stay running waiting for one to be enabled.") + + # Load hidden configuration and monitor for changes + self._load_hidden_configuration() + self.notify_handles.append(self.conf_client.notify_add("/apps/gnome15/scroll_delay", self._hidden_configuration_changed)) + self.notify_handles.append(self.conf_client.notify_add("/apps/gnome15/scroll_amount", self._hidden_configuration_changed)) + self.notify_handles.append(self.conf_client.notify_add("/apps/gnome15/animated_menus", self._hidden_configuration_changed)) + self.notify_handles.append(self.conf_client.notify_add("/apps/gnome15/animation_delay", self._hidden_configuration_changed)) + self.notify_handles.append(self.conf_client.notify_add("/apps/gnome15/key_hold_duration", self._hidden_configuration_changed)) + self.notify_handles.append(self.conf_client.notify_add("/apps/gnome15/use_x_test", self._hidden_configuration_changed)) + self.notify_handles.append(self.conf_client.notify_add("/apps/gnome15/disable_svg_glow", self._hidden_configuration_changed)) + + + # Monitor active application + gobject.idle_add(self._configure_window_monitoring) + + # Activate global plugins + self.global_plugins.activate() + + # Start each screen's plugin manager + th = [] + for screen in self.screens: + t = StartThread(screen) + if self.start_in_threads: + t.start() + else: + t.run() + th.append(t) + if len(self.screens) == 1: + if th[0].error is not None: + raise th[0].error + + if self.start_in_threads: + for t in th: + t.join() + + self.starting_up = False + for listener in self.service_listeners: + listener.service_started_up() + self.started = True + + gobject.idle_add(self._monitor_session) + + # Watch for devices changing + g15devices.device_added_listeners.append(self._device_added) + g15devices.device_removed_listeners.append(self._device_removed) + + def _join_all(self, threads, timeout = 30): + for t in threads: + t.join(timeout) + + def _monitor_session(self): + # Monitor active session (we shut down the driver when becoming inactive) + if self.system_bus.name_has_owner('org.freedesktop.ConsoleKit'): + self._connect_to_consolekit() + elif self.system_bus.name_has_owner('org.freedesktop.login1'): + self._connect_to_logind() + else: + logger.warning("None of the supported system session manager available, will not track active desktop session.") + self.session_active = True + + connected_to_session_manager = False + # session manager stuff (watch for logout etc) + try : + logger.info("Connecting to GNOME session manager") + session_manager_object = self.session_bus.get_object("org.gnome.SessionManager", "/org/gnome/SessionManager", "org.gnome.SessionManager") + client_path = session_manager_object.RegisterClient('gnome15.desktop', '', dbus_interface="org.gnome.SessionManager") + + self.session_manager_client_object = self.session_bus.get_object("org.gnome.SessionManager", client_path, "org.gnome.SessionManager.ClientPrivate") + self.session_manager_client_object.connect_to_signal("QueryEndSession", self._sm_query_end_session) + self.session_manager_client_object.connect_to_signal("EndSession", self._sm_end_session) + self.session_manager_client_object.connect_to_signal("CancelEndSession", self._sm_cancel_end_session) + self.session_manager_client_object.connect_to_signal("Stop", self._sm_stop) + + session_manager_client_public_object = self.session_bus.get_object("org.gnome.SessionManager", client_path, "org.gnome.SessionManager.Client") + sm_client_id = session_manager_client_public_object.GetStartupId() + gtk.gdk.set_sm_client_id(sm_client_id) + connected_to_session_manager = True + logger.info("Connected to GNOME session manager") + except Exception as e: + logger.info("GNOME session manager not available.") + logger.debug("GNOME connection attempt below :", exc_info = e) + + if not connected_to_session_manager: + try : + logger.info("Connecting to MATE session manager") + session_manager_object = self.session_bus.get_object("org.mate.SessionManager", "/org/mate/SessionManager", "org.mate.SessionManager") + client_path = session_manager_object.RegisterClient('gnome15.desktop', '', dbus_interface="org.mate.SessionManager") + + self.session_manager_client_object = self.session_bus.get_object("org.mate.SessionManager", client_path, "org.mate.SessionManager.ClientPrivate") + self.session_manager_client_object.connect_to_signal("QueryEndSession", self._sm_query_end_session) + self.session_manager_client_object.connect_to_signal("EndSession", self._sm_end_session) + self.session_manager_client_object.connect_to_signal("CancelEndSession", self._sm_cancel_end_session) + self.session_manager_client_object.connect_to_signal("Stop", self._sm_stop) + + session_manager_client_public_object = self.session_bus.get_object("org.mate.SessionManager", client_path, "org.mate.SessionManager.Client") + sm_client_id = session_manager_client_public_object.GetStartupId() + gtk.gdk.set_sm_client_id(sm_client_id) + connected_to_session_manager = True + logger.info("Connected to MATE session manager") + except Exception as e: + logger.info("MATE session manager not available.") + logger.debug("MATE connection attempt below :", exc_info = e) + + if not connected_to_session_manager: + logger.warning("None of the supported session managers available, will not detect logout signal for clean shutdown.") + + + def _sm_query_end_session(self, flags): + if self._is_monitor_session(): + logger.info("Querying for end session") + self._sm_client_dbus_will_quit(True, "") + + def _sm_cancel_end_session(self): + if self._is_monitor_session(): + if not self.session_active: + logger.info("Cancelled session end, starting up again") + self.session_active = True + self.start_service() + else: + logger.info("Cancelled session end, but we haven't started shutdown yet") + + def _sm_end_session(self, flags): + if self._is_monitor_session(): + logger.info("Ending session") + def e(): + self.stop() + self._sm_client_dbus_will_quit(True, "") + g15scheduler.queue(SERVICE_QUEUE, "endSession", 0.0, e) + + def _sm_client_dbus_will_quit(self, can_quit=True, reason=""): + self.session_manager_client_object.EndSessionResponse(can_quit,reason) + + def _sm_stop(self): + logger.info("Shutdown quickly") + self.shutdown(True) + + def _is_monitor_session(self): + return g15gconf.get_bool_or_default(self.conf_client, "/apps/gnome15/monitor_desktop_session", True) + + def _connect_to_consolekit(self): + try : + logger.info("Connecting to ConsoleKit") + self.system_bus.add_signal_receiver(self._active_session_changed, dbus_interface="org.freedesktop.ConsoleKit.Seat", signal_name="ActiveSessionChanged") + console_kit_object = self.system_bus.get_object("org.freedesktop.ConsoleKit", '/org/freedesktop/ConsoleKit/Manager') + console_kit_manager = dbus.Interface(console_kit_object, 'org.freedesktop.ConsoleKit.Manager') + logger.info("Seats %s ", str(console_kit_manager.GetSeats())) + self.this_session_path = console_kit_manager.GetSessionForCookie (os.environ['XDG_SESSION_COOKIE']) + logger.info("This session %s", self.this_session_path) + + # TODO GetCurrentSession doesn't seem to work as i would expect. Investigate. For now, assume we are the active session +# current_session = console_kit_manager.GetCurrentSession() +# logger.info("Current session %s ", current_session) +# self.session_active = current_session == self.this_session_path + self.session_active = True + + logger.info("Connected to ConsoleKit") + connected_to_system_session_manager = True + except Exception as e: + logger.warning("ConsoleKit not available", exc_info = e) + + def _connect_to_logind(self): + try : + logger.info("Connecting to logind") + self.system_bus.add_signal_receiver(self._logind_seat0_property_changed, "PropertiesChanged", "org.freedesktop.DBus.Properties", "org.freedesktop.login1", "/org/freedesktop/login1/seat/seat0") + self.this_session_path = self._get_systemd_active_session_path() + logger.info("This session %s ", self.this_session_path) + + self.session_active = True + + logger.info("Connected to logind") + connected_to_system_session_manager = True + except Exception as e: + logger.warning("logind not available.", exc_info = e) + + def _get_systemd_active_session_path(self): + seat0_object = self.system_bus.get_object("org.freedesktop.login1", '/org/freedesktop/login1/seat/seat0') + seat0_properties_interface = dbus.Interface(seat0_object, 'org.freedesktop.DBus.Properties') + id, session_path = seat0_properties_interface.Get('org.freedesktop.login1.Seat', 'ActiveSession') + return session_path + + def _logind_seat0_property_changed(self, interface, dicto, properties): + if "ActiveSession" in properties: + if self._is_monitor_session(): + session_path = self._get_systemd_active_session_path() + logger.info("This session %s", session_path) + self.session_active = session_path == self.this_session_path + if self.session_active: + logger.info("g15-desktop service is running on the active session") + else: + logger.info("g15-desktop service is NOT running on the active session") + g15scheduler.queue(SERVICE_QUEUE, "activeSessionChanged", 0.0, self._check_state_of_all_devices) + + def _active_session_changed(self, object_path): + logger.debug("Adding seat %s", object_path) + if self._is_monitor_session(): + self.session_active = object_path == self.this_session_path + if self.session_active: + logger.info("g15-desktop service is running on the active session") + else: + logger.info("g15-desktop service is NOT running on the active session") + g15scheduler.queue(SERVICE_QUEUE, "activeSessionChanged", 0.0, self._check_state_of_all_devices) + + def _configure_window_monitoring(self): + logger.info("Attempting to set up BAMF") + try : + bamf_object = self.session_bus.get_object('org.ayatana.bamf', '/org/ayatana/bamf/matcher') + self.bamf_matcher = dbus.Interface(bamf_object, 'org.ayatana.bamf.matcher') + self.session_bus.add_signal_receiver(self._active_window_changed, dbus_interface = 'org.ayatana.bamf.matcher', signal_name="ActiveWindowChanged") + active_window = self.bamf_matcher.ActiveWindow() + logger.info("Will be using BAMF for window matching") + if active_window: + self._active_window_changed("", active_window) + except Exception as e: + logger.info("BAMF not available, falling back to polling WNCK.") + logger.debug("BAMF attempt below :", exc_info = e) + try : + import wnck + wnck.__file__ + self._check_active_application_with_wnck() + except Exception as e: + logger.warning("Python Wnck not available either, no automatic profile switching", exc_info = e) + + def _add_screen(self, device): + try: + screen = g15screen.G15Screen(g15pluginmanager, self, device) + self.screens.append(screen) + for listener in self.service_listeners: + listener.screen_added(screen) + return screen + except Exception as e: + logger.error("Failed to load driver for device %s.", device.uid, exc_info = e) + + def _hidden_configuration_changed(self, client, connection_id, entry, device): + self._load_hidden_configuration() + + def _load_hidden_configuration(self): + self.scroll_delay = float(g15gconf.get_int_or_default(self.conf_client, '/apps/gnome15/scroll_delay', 500)) / 1000.0 + self.scroll_amount = g15gconf.get_int_or_default(self.conf_client, '/apps/gnome15/scroll_amount', 5) + self.animated_menus = g15gconf.get_bool_or_default(self.conf_client, '/apps/gnome15/animated_menus', True) + self.animation_delay = g15gconf.get_int_or_default(self.conf_client, '/apps/gnome15/animation_delay', 100) / 1000.0 + self.key_hold_duration = g15gconf.get_int_or_default(self.conf_client, '/apps/gnome15/key_hold_duration', 2000) / 1000.0 + self.macro_handler.use_x_test = g15gconf.get_bool_or_default(self.conf_client, '/apps/gnome15/use_x_test', True) + self.disable_svg_glow = g15gconf.get_bool_or_default(self.conf_client, '/apps/gnome15/disable_svg_glow', False) + self.fade_screen_on_close = g15gconf.get_bool_or_default(self.conf_client, '/apps/gnome15/fade_screen_on_close', True) + self.all_off_on_disconnect = g15gconf.get_bool_or_default(self.conf_client, '/apps/gnome15/all_off_on_disconnect', True) + self.fade_keyboard_backlight_on_close = g15gconf.get_bool_or_default(self.conf_client, '/apps/gnome15/fade_keyboard_backlight_on_close', True) + self.start_in_threads = g15gconf.get_bool_or_default(self.conf_client, '/apps/gnome15/start_in_threads', False) + self._mark_all_pages_dirty() + + def _mark_all_pages_dirty(self): + for screen in self.screens: + for page in screen.pages: + page.mark_dirty() + + def _device_enabled_configuration_changed(self, client, connection_id, entry, device): + g15scheduler.queue(SERVICE_QUEUE, "deviceStateChanged", 0, self._check_device_state, device) + + def _check_device_state(self, device, quickly = False): + enabled = device in self.devices and g15devices.is_enabled(self.conf_client, device) and self.session_active + screen = self._get_screen_for_device(device) + if enabled and not screen: + logger.info("Enabling device %s", device.uid) + # Enable screen + screen = self._add_screen(device) + if screen: + screen.start() + logger.info("Enabled device %s", device.uid) + elif not enabled and screen: + # Disable screen + logger.info("Disabling device %s", device.uid) + screen.stop(quickly) + self.screens.remove(screen) + for listener in self.service_listeners: + listener.screen_removed(screen) + logger.info("Disabled device %s", device.uid) + + # If there is a single device, stop the service as well + if len(self.devices) == 0 and ( not g15devices.have_udev or self.exit_on_no_devices ): + self.shutdown(False) + + def _device_added(self, device): + self.devices = g15devices.find_all_devices() + self._check_device_state(device) + self.device_notify_handles[device.uid] = self.conf_client.notify_add("/apps/gnome15/%s/enabled" % device.uid, self._device_enabled_configuration_changed, device) + + def _device_removed(self, device): + self.devices = g15devices.find_all_devices() + self._check_device_state(device) + self.conf_client.notify_remove(self.device_notify_handles[device.uid]) + del self.device_notify_handles[device.uid] + + def _get_screen_for_device(self, device): + for screen in self.screens: + if screen.device.uid == device.uid: + return screen + + def __del__(self): + for screen in self.screens: + if screen.plugins.is_active(): + screen.plugins.deactivate() + if screen.plugins.is_started(): + screen.plugins.destroy() + del self.screens diff --git a/src/gnome15/g15system.py b/src/gnome15/g15system.py new file mode 100644 index 0000000..66750de --- /dev/null +++ b/src/gnome15/g15system.py @@ -0,0 +1,229 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2011 Brett Smith +# +# 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 . + +import gobject +import g15globals +import signal +import dbus.service +import os.path +import g15devices +import g15driver +import util.g15scheduler as g15scheduler + +# Logging +import logging +logger = logging.getLogger(__name__) + +NAME = "Gnome15" +VERSION = g15globals.version +BUS_NAME="org.gnome15.SystemService" +OBJECT_PATH="/org/gnome15/SystemService" +IF_NAME="org.gnome15.SystemService" + +""" +Maps model id's to driver names +""" +driver_names = { + g15driver.MODEL_G15_V1: "g15", + g15driver.MODEL_G15_V2: "g15", + g15driver.MODEL_G19: "g19", + g15driver.MODEL_G110: "g110", + g15driver.MODEL_G13: "g13", + } + +class SystemService(dbus.service.Object): + + def __init__(self, bus, controller): + bus_name = dbus.service.BusName(BUS_NAME, bus=bus, replace_existing=False, allow_replacement=False, do_not_queue=True) + dbus.service.Object.__init__(self, None, OBJECT_PATH, bus_name) + self._controller = controller + + """ + DBUS API + """ + + @dbus.service.method(IF_NAME) + def Stop(self): + self._controller.stop() + + @dbus.service.method(IF_NAME, in_signature='', out_signature='ssss') + def GetInformation(self): + return ( "%s System Service" % g15globals.name, "Gnome15 Project", g15globals.version, "1.0" ) + + @dbus.service.method(IF_NAME, in_signature='ssn') + def SetLight(self, device, light, value): + self._controller.devices[device].leds[light].set_led_value(value); + + @dbus.service.method(IF_NAME, in_signature='ss', out_signature='n') + def GetLight(self, device, light): + return self._controller.devices[device].leds[light].get_value() + + @dbus.service.method(IF_NAME, out_signature='as') + def GetDevices(self): + c = [] + for l in self._controller.devices: + c.append(l) + return c + + @dbus.service.method(IF_NAME, in_signature='s', out_signature='as') + def GetLights(self, device): + return self._controller.devices[device].leds.keys() + + @dbus.service.method(IF_NAME, in_signature='ss', out_signature='n') + def GetMaxLight(self, device, light): + return self._controller.devices[device].leds[light].get_max() + + +def get_int_value(filename): + return int(get_value(filename)) + +def get_value(filename): + fd = open(filename, "r") + try : + return fd.read() + finally : + fd.close() + +def set_value(filename, value): + g15scheduler.execute("System", "setValue", _do_set_value, filename, value); + +def _do_set_value(filename, value): + logger.debug("Writing %s to %s", filename, value) + fd = open(filename, "w") + try : + fd.write("%s\n" % str(value)) + finally : + fd.close() + +class KeyboardDevice(): + def __init__(self, device, device_path, index): + self.leds = {} + self.device = device + self.device_path = device_path + self.minor = get_int_value(os.path.join(device_path, "minor")) + self.uid = "%s_%d" % ( device.model_id, index ) + leds_path = os.path.join(device_path, "leds") + for d in os.listdir(leds_path): + f = os.path.join(leds_path, d) + keyboard_device, color, control = d.split(":") + keyboard_device, index = keyboard_device.split("_") + light_key = "%s:%s" % ( color, control ) + self.leds[light_key] = LED(light_key, self, f) + + +class LED(): + """ + Represents a single LED, the keyboard it is linked to, + and the /sys filename for the LED + """ + def __init__(self, light_key, keyboard_device, filename): + self.light_key = light_key + self.keyboard_device = keyboard_device + self.filename = filename + + def set_led_value(self, val): + """ + Set the current brightness of the LED + + Keyword arguments: + val -- + """ + if val < 0 or val > self.get_max(): + raise Exception("LED value out of range") + set_value(os.path.join(self.filename, "brightness"), val) + + def get_value(self): + return get_int_value(os.path.join(self.filename, "brightness")) + + def get_max(self): + return get_int_value(os.path.join(self.filename, "max_brightness")) + +DEVICES_PATH="/sys/bus/hid/devices" + +class G15SystemServiceController(): + + def __init__(self, bus, no_trap=False): + self._page_sequence_number = 1 + self._bus = bus + self.devices = {} + logger.debug("Exposing service") + + if not no_trap: + signal.signal(signal.SIGINT, self.sigint_handler) + signal.signal(signal.SIGTERM, self.sigterm_handler) + + self._loop = gobject.MainLoop() + gobject.idle_add(self._start_service) + + def stop(self): + self._loop.quit() + + def start_loop(self): + logger.info("Starting GLib loop") + self._loop.run() + logger.debug("Exited GLib loop") + + def sigint_handler(self, signum, frame): + logger.info("Got SIGINT signal, shutting down") + self.shutdown() + + def sigterm_handler(self, signum, frame): + logger.info("Got SIGTERM signal, shutting down") + self.shutdown() + + def shutdown(self): + logger.info("Shutting down") + self._loop.quit() + + """ + Private + """ + def _start_service(self): + self._scan_devices() + SystemService(self._bus, self) + + def _scan_devices(self): + self.devices = {} + indices = {} + if os.path.exists(DEVICES_PATH): + for device in os.listdir(DEVICES_PATH): + # Only want devices with leds + device_path = os.path.join(DEVICES_PATH, device) + leds_path = os.path.join(device_path, "leds") + if os.path.exists(leds_path): + # Extract the USB ID + a = device.split(":") + usb_id = ( int("0x%s" % a[1], 16), int("0x%s" % a[2].split(".")[0], 16) ) + logger.info("Testing if device %04x:%04x is supported by Gnome15", + usb_id[0], + usb_id[1]) + + # Look for a matching Gnome15 device + for device in g15devices.find_all_devices(): + if device.controls_usb_id == usb_id: + # Found a device we want + logger.info("Found device %s", str(device)) + + # Work out UID + # TODO this is not quite right - if there is more than one device of same type, indexs might not match + index = 0 if not device.model_id in indices else indices[device.model_id] + keyboard_device = KeyboardDevice(device, device_path, index) + self.devices[device.uid] = keyboard_device + indices[device.model_id] = index + 1 + + else: + logger.info("No devices found at %s", DEVICES_PATH) + diff --git a/src/gnome15/g15text.py b/src/gnome15/g15text.py new file mode 100644 index 0000000..65dd07f --- /dev/null +++ b/src/gnome15/g15text.py @@ -0,0 +1,161 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2011 Brett Smith +# +# 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 . + +import pango +import pangocairo +import cairo +import gobject +import logging +logger = logging.getLogger(__name__) + +# Shared pango context +pango_context = pangocairo.cairo_font_map_get_default().create_context() + +""" +Handles drawing and measuring of text on a screen. +""" + + +def new_text(screen = None): + """ + Create a new text handler. This should be used rather than directly constructing + the G15PangoText + """ + if screen: + return G15PangoText(screen.driver.get_antialias()) + else: + return G15PangoText(True) + +class G15Text(object): + + def __init__(self, antialias): + self.antialias = antialias + + def set_attributes(self, text, bounds): + self.text = text + self.bounds = bounds + + def measure(self): + raise Exception("Not implemented") + + def draw(self, x, y, clip = None): + raise Exception("Not implemented") + + def set_canvas(self, canvas): + self.canvas = canvas + + """ + Private + """ + def _create_font_options(self): + fo = cairo.FontOptions() + fo.set_antialias(self.antialias) + if self.antialias == cairo.ANTIALIAS_NONE: + fo.set_hint_style(cairo.HINT_STYLE_NONE) + fo.set_hint_metrics(cairo.HINT_METRICS_OFF) + +class G15PangoText(G15Text): + + def __init__(self, antialias): + G15Text.__init__(self, antialias) + pangocairo.context_set_font_options(pango_context, self._create_font_options()) + self.__pango_cairo_context = None + self.__layout = None + self.valign = pango.ALIGN_CENTER + self.__layout = pango.Layout(pango_context) + + def set_canvas(self, canvas): + G15Text.set_canvas(self, canvas) + self.__pango_cairo_context = pangocairo.CairoContext(self.canvas) + + def set_attributes(self, text, bounds = None, wrap = None, align = pango.ALIGN_LEFT, width = None, spacing = None, \ + font_desc = None, font_absolute_size = None, attributes = None, + weight = None, style = None, font_pt_size = None, + valign = None, pxwidth = None): + + logger.debug("Text: %s, bounds = %s, wrap = %s, align = %s, width = %s, " \ + "attributes = %s, spacing = %s, font_desc = %s, weight = %s, " \ + "style = %s, font_pt_size = %s", + str(text), + str(bounds), + str(wrap), + str(align), + str(width), + str(attributes), + str(spacing), + str(font_desc), + str(weight), + str(style), + str(font_pt_size)) + + G15Text.set_attributes(self, text, bounds) + self.valign = valign + + font_desc_name = "Sans" if font_desc == None else font_desc + if weight: + font_desc_name += " %s" % weight + if style: + font_desc_name += " %s" % style + if font_pt_size: + font_desc_name += " " + str(font_pt_size) + font_desc = pango.FontDescription(font_desc_name) + if font_absolute_size is not None: + font_desc.set_absolute_size(font_absolute_size) + self.__layout.set_font_description(font_desc) + + if align != None: + self.__layout.set_alignment(align) + if spacing != None: + self.__layout.set_spacing(spacing) + if width != None: + self.__layout.set_width(width) + if pxwidth != None: + self.__layout.set_width(int(pango.SCALE * pxwidth)) + if wrap: + self.__layout.set_wrap(wrap) + if attributes: + self.__layout.set_attributes(attributes) + + self.__layout.set_text(text) + self.metrics = pango_context.get_metrics(self.__layout.get_font_description()) + + def measure(self): + text_extents = self.__layout.get_extents()[1] + return text_extents[0] / pango.SCALE, text_extents[1] / pango.SCALE, text_extents[2] / pango.SCALE, text_extents[3] / pango.SCALE + + def draw(self, x = None, y = None): + self.__pango_cairo_context.save() + + if self.bounds is not None: + if x == None: + x = self.bounds[0] + if y == None: + y = self.bounds[1] + + self.__pango_cairo_context.rectangle(self.bounds[0] - 1, self.bounds[1] - 1, self.bounds[2] + 2, self.bounds[3] + 2) + self.__pango_cairo_context.clip() + + if self.valign == pango.ALIGN_RIGHT: + y += self.bounds[3] - ( self.metrics.get_ascent() / 1000.0 ) + elif self.valign == pango.ALIGN_CENTER: + y += ( self.bounds[3] - ( self.metrics.get_ascent() / 1000.0 ) ) / 2 + + if x is not None and y is not None: + self.__pango_cairo_context.move_to(x, y) + + self.__pango_cairo_context.show_layout(self.__layout) + self.__pango_cairo_context.restore() + diff --git a/src/gnome15/g15theme.py b/src/gnome15/g15theme.py new file mode 100644 index 0000000..a5f1cde --- /dev/null +++ b/src/gnome15/g15theme.py @@ -0,0 +1,2227 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2010 Brett Smith +# +# 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 . + +""" +This module contains all the classes required for the Gnome15 component and themeing +system. + +The basis of this is a component hierarchy that starts with a G15Page (actually, the +'screen' is kind of the root component, but that anomaly will be fixed). + +All components may contain children. Whether or not they are rendered is down to +the individual parent component. There is currently a limited set of container type +components, including Component itself, Page and Menu. Menu's children must be MenuItem +objects (or a subclass of MenuItem). Page's may contain any type of child. + +Each component has a 'Theme' associated with it. This is an SVG file that is rendered +at painting time. A Page's theme will take up all available space and be rendered at +0,0, where as child components will be rendered at and within bounds defined in the +parent's theme file (by using an svg:rect with an ID that links the two), or at a +place calculated by the component itself (for example, MenuItem children). + +The Theme is also responsible for handling automatic text scrolling, as well as processing +the SVG by doing string replacements and other manipulations such as those required +for progress bars, scroll bars. +""" + +import os +import cairo +import rsvg +import sys +import pango +import g15driver +import g15globals +import g15screen +import util.g15convert as g15convert +import util.g15scheduler as g15scheduler +import g15text +import g15locale +import util.g15cairo as g15cairo +import util.g15svg as g15svg +import util.g15icontools as g15icontools +import xml.sax.saxutils as saxutils +import base64 +import dbusmenu +import logging +import time +logger = logging.getLogger(__name__) +from string import Template +from copy import deepcopy +from cStringIO import StringIO +from lxml import etree +from threading import RLock +import ConfigParser + +BASE_PX=18.0 +DEBUG_SVG=False + +# The color in SVG theme files that by default gets replaced with the current 'highlight' color +DEFAULT_HIGHLIGHT_COLOR="#ff0000" + +class ThemeDefinition(object): + def __init__(self, theme_id, directory, plugin_module = None): + self.theme_id = theme_id + self.plugin_module = plugin_module + self.directory = directory + self.supported = [] + self.unsupported = [] + filename = os.path.join(self.directory, "%s.theme" % theme_id) + if not os.path.exists(filename): + if theme_id == "default": + self.name = "Simple" + self.description = "Default theme supplied with Gnome15" + else: + raise Exception("No theme descriptor %s" % filename) + else: + parser = ConfigParser.ConfigParser({}) + parser.read(filename) + self.name = parser.get("theme", "name") + self.description = parser.get("theme", "description") + if parser.has_option("theme", "supported_models"): + self.supported = parser.get("theme", "supported_models").split(",") + if parser.has_option("theme", "unsupported_models"): + self.unsupported = parser.get("theme", "unsupported_models").split(",") + + # Load any translations for this theme + tdomain = "%s.%s" % ( plugin_module.id, theme_id ) + self.translation = g15locale.get_translation(tdomain, self.directory) + if self.translation: + logger.info("Found translation %s", tdomain) + + def supports(self, model_id): + return ( len(self.supported) == 0 or model_id in self.supported ) \ + and not model_id in self.unsupported + +def get_theme(theme_id, plugin_module): + """ + Get a theme definition give it's ID and the plugin that contains it + + Keyword arguments: + theme_id -- theme ID + plugin_module -- plugin + """ + module_dir = os.path.dirname(plugin_module.__file__) + theme_dir = os.path.join(module_dir, theme_id) + if os.path.isdir(theme_dir) and ( theme_id == "default" or \ + os.path.exists(os.path.join(theme_dir, "%s.theme" % theme_id))): + return ThemeDefinition(theme_id, theme_dir, plugin_module) + +def get_themes(model_id, plugin_module): + """ + Get a list of themes this plugin supports for the requested model + + Keyword arguments: + model_id -- model support is required for + plugin_module -- plugin + """ + themes = [] + module_dir = os.path.dirname(plugin_module.__file__) + for d in os.listdir(module_dir): + theme_dir = os.path.join(module_dir, d) + if os.path.isdir(theme_dir) and ( d == "default" or \ + os.path.exists(os.path.join(theme_dir, "%s.theme" % d))): + definition = ThemeDefinition(d, theme_dir, plugin_module) + if definition.supports(model_id): + themes.append(definition) + return themes + +class Render(object): + def __init__(self, document, properties, text_boxes, attributes, processing_result): + self.document = document + self.properties = properties + self.text_boxes = text_boxes + self.attributes = attributes + self.processing_result = processing_result + +class ScrollState(object): + + def __init__(self): + self.range = (0.0, 0.0) + self.adjust = 0.0 + self.reversed = True + self.step = 1.0 + self.alignment = pango.ALIGN_LEFT + self.val = 0 + self.original = 0 + + def reset(self): + self.adjust = 0.0 + self.do_transform() + + def next(self): + self.adjust += -self.step if self.reversed else self.step + if self.adjust < self.range[0] and self.reversed: + self.reversed = False + self.adjust = self.range[0] + elif self.adjust > self.range[1] and not self.reversed: + self.adjust = self.range[1] + self.reversed = True + self.do_transform() + + def do_transform(self): + self.val = self.adjust + self.original + self.transform_elements() + +class HorizontalScrollState(ScrollState): + + def __init__(self, element = None): + ScrollState.__init__(self) + self.element = element + self.other_elements = [] + + def transform_elements(self): + self.element.set("x", str(int(self.val))) + for e in self.other_elements: + e.set("x", str(int(self.val))) + + def next(self): + ScrollState.next(self) + +class VerticalWrapScrollState(ScrollState): + def __init__(self, text_box): + ScrollState.__init__(self) + self.text_box = text_box + + def transform_elements(self): + self.text_box.base = self.val + +class TextBox(object): + def __init__(self): + self.bounds = ( ) + self.clip = () + self.align = "start" + self.text = "" + self.wrap = False + self.css = { } + self.normal_shadow = False + self.reverse_shadow = False + self.transforms = [] + self.base = 0 + +class LayoutManager(object): + def __init__(self): + pass + + def layout(self, parent): + raise Exception("Not implemeted") + +class GridLayoutManager(LayoutManager): + + def __init__(self, columns, rows = -1): + self.rows = rows + self.columns = columns + + def layout(self, parent): + x = 0 + y = 0 + col = 0 + row_height = 0 + for c in parent.get_children(): + if c.is_showing(): + bounds = c.view_bounds + if bounds is None: + logger.warning("No bounds on component %s", c.id) + else: + c.view_bounds = ( x, y, bounds[2], bounds[3]) + x += bounds[2] + row_height = max(row_height, bounds[3]) + col += 1 + if col >= self.columns: + x = 0 + y += row_height + row_height = 0 + col = 0 + + +class Childmap(dict): + def __init__(self): + type(self).__name__ = "Childmap" + dict.__init__(self) + +class Childlist(list): + def __init__(self, l = []): + type(self).__name__ = "Childlist" + list.__init__(self, l) + +class Component(object): + + def __init__(self, id): + self.id = id + self.theme = None + self._children = Childlist() + self.child_map = Childmap() + self.parent = None + self.screen = None + self.enabled = True + self.theme_properties = {} + self.theme_attributes = {} + self.theme_properties_callback = None + self.theme_attributes_callback = None + self.view_bounds = None + self.view_element = None + self.layout_manager = None + self.base = 0 + self.focusable = False + self._tree_lock = RLock() + self.do_clip = False + self.allow_scrolling = None + self.showing = True + self.activatable = False + self.scrollbar = None + + def set_scrollbar(self, scrollbar): + self.scrollbar = scrollbar + scrollbar.viewport = self + + def is_enabled(self): + return self.enabled + + def get_tree_lock(self): +# if self.parent == None: +# return self._tree_lock +# else: +# return self.parent.get_tree_lock() + return self._tree_lock + + def clear_scroll(self): + if self.theme: + self.theme.clear_scroll() + + def is_showing(self): + return self.showing + + def set_showing(self, showing): + self.showing = showing + + def get_showing_count(self): + i = 0 + for c in self._children: + if c.is_showing(): + i += 1 + return i + + def is_focused(self): + return self.get_root().focused_component == self + + def set_focused_component(self, component): + self.focused_component = component + + def set_focused(self, focused): + if not self.focusable: + raise Exception("%s is not focusable" % self.id) + if focused: + self.get_root().set_focused_component(self) + elif self.get_root().focused == self: + self.get_root().set_focused_component(None) + self.get_root().next_focus() + + def set_theme(self, theme): + self.get_tree_lock().acquire() + try: + if self.theme is not None: + self.theme._set_component(None) + self.theme = theme + theme._set_component(self) + self.view_bounds = theme.bounds + for c in self.get_children(): + c.configure(self) + finally: + self.get_tree_lock().release() + + def mark_dirty(self): + if self.theme is not None: + self.theme.mark_dirty() + for c in self.get_children(): + c.mark_dirty() + if c.scrollbar is not None: + c.scrollbar.mark_dirty() + + def get_allow_scrolling(self): + c = self + while c is not None: + if c.allow_scrolling is not None: + return c.allow_scrolling + c = c.parent + return True + + def do_scroll(self): + for c in self._children: + c.do_scroll() + if self.theme and self.get_allow_scrolling(): + self.theme.do_scroll() + + def check_for_scroll(self): + scroll = False + for c in self._children: + if c.check_for_scroll(): + scroll = True + if self.theme and self.get_allow_scrolling() and self.theme.is_scroll_required(): + scroll = True + return scroll + + def get_theme(self): + c = self + while c is not None: + if c.theme: + return c.theme + c = c.parent + + def get_screen(self): + c = self + while c is not None: + if c.screen: + return c.screen + c = c.parent + + def get_root(self): + c = self + r = None + while c is not None: + r = c + c = c.parent + return r + + def index_of_child(self, child): + return self._children.index(child) + + def get_child(self, index): + return self._children[index] + + def get_child_by_id(self, id): + return self.child_map[id] if id in self.child_map else None + + def contains_child(self, child): + return child in self._children + + def get_child_count(self): + return len(self._children) + + def set_children(self, children): + g15screen.check_on_redraw() + self.get_tree_lock().acquire() + try: + # Remove any children that we currently have, but are not in the new list + for c in list(set(self._children) - set(children)): + self.remove_child(c) + + # Add any new children + for c in list(set(children) - set(self._children)): + self.add_child(c) + + # Now just change out child list to the new one so the order is correct + self._children = Childlist(children) + finally: + self.get_tree_lock().release() + + def get_children(self): + return list(self._children) + + def add_child(self, child, index = -1): + g15screen.check_on_redraw() + self.get_tree_lock().acquire() + try: + if child.parent: + raise Exception("Child %s already has a parent. Remove it from it's last parent first before adding to %s." % (child.id, self.id)) + if child.id in self.child_map: + raise Exception("Child with ID of %s already exists in component %s. Trying to add %s, but %s exists" % (child.id, self.id, str(child), str(self.child_map[child.id]))) + self._check_has_parent() + child.configure(self) + self.child_map[child.id] = child + if index == -1: + self._children.append(child) + else: + self._children.insert(index, child) + self.mark_dirty() + self.notify_add(child) + finally: + self.get_tree_lock().release() + + def remove_all_children(self): + g15screen.check_on_redraw() + self.get_tree_lock().acquire() + try: + for c in list(self._children): + self.remove_child(c) + finally: + self.get_tree_lock().release() + + def remove_child(self, child): + g15screen.check_on_redraw() + self.get_tree_lock().acquire() + try: + if not child in self._children: + raise Exception("Not a child of this component.") + child.notify_remove() + if child.theme: + child.theme._component_removed() + child.parent = None + del self.child_map[child.id] + self._children.remove(child) + finally: + self.get_tree_lock().release() + + def remove_child_at(self, index): + g15screen.check_on_redraw() + self.get_tree_lock().acquire() + try: + self.remove_child(self._children[index]) + finally: + self.get_tree_lock().release() + + def remove_from_parent(self): + g15screen.check_on_redraw() + if not self.parent: + raise Exception("Not added to a parent.") + self.parent.remove_child(self) + + def configure(self, parent): + self.parent = parent + self.on_configure() + theme = self.get_theme() + if theme == None: + logger.warning("No theme for component with ID of %s", self.id) + else: + self.view_element = theme.get_element(self.id) + if self.view_element is None: + self.view_element = theme.get_element() + self.view_bounds = g15svg.get_actual_bounds(self.view_element) if self.view_element is not None else None + + def is_visible(self): + return self.parent != None and self.parent.is_visible() + + def on_configure(self): + pass + + def get_default_theme_dir(self): + return os.path.join(g15globals.themes_dir, "default") + + def draw(self, element, theme): + """ + Called by the theme for the component to adjust the SVG document ID if required + """ + pass + + def paint_theme(self, canvas, properties, attributes): + """ + Paint the theme. Do not call directly, instead call paint() + """ + self.theme.draw(canvas, properties, self.get_theme_attributes()) + + def paint(self, canvas): + g15screen.check_on_redraw() + self.get_tree_lock().acquire() + try: + canvas.save() + + # Translate to the components bounds and clip to the size of the view + if self.view_bounds: + canvas.translate(self.view_bounds[0], self.view_bounds[1]) + canvas.rectangle(0, 0, self.view_bounds[2], self.view_bounds[3]) + canvas.clip() + + # Translate against the base, this allows components to be scrolled within their viewport + canvas.translate(0, -self.base) + + # Draw any theme for this component + if self.theme is not None: + canvas.save() + properties = self.get_theme_properties() + + # Add some common properties + if self.get_root().focused_component is not None: + properties['%s_focused' % self.get_root().focused_component.id ] = "true" + + screen = self.get_screen() + if screen: + states = screen.key_handler.get_key_states() + for k in states: + ks = states[k] + if ks.state_id == g15driver.KEY_STATE_DOWN: + properties['key_%s' % k ] = True + elif ks.state_id == g15driver.KEY_STATE_HELD: + properties['key_%s_held' % k ] = True + + self.paint_theme(canvas, properties, self.get_theme_attributes()) + canvas.restore() + + # Layout any children + if self.layout_manager != None: + self.layout_manager.layout(self) + + # Paint children + for c in self._children: + if c.is_showing(): + canvas.save() + if not self.do_clip or c.view_bounds is None or self.overlaps(self.view_bounds, c.view_bounds): + c.paint(canvas) + canvas.restore() + + canvas.restore() + finally: + self.get_tree_lock().release() + + def overlaps(self, bounds1, bounds2): + return bounds2[1] >= ( self.base - bounds2[3] ) and bounds2[1] < ( self.base + bounds1[3] ) + + def get_theme_properties(self): + p = None + if self.theme_properties_callback is not None: + p = self.theme_properties_callback() + if p is None: + p = self.theme_properties + return p + + def get_theme_attributes(self): + p = None + if self.theme_attributes_callback is not None: + p = self.theme_attributes_callback() + if p is None: + p = self.theme_attributes + return p + + def notify_add(self, component): + if self.parent: + self.parent.notify_add(component) + + def notify_remove(self): + self.remove_all_children() + + ''' + Private + ''' + def _check_has_parent(self): +# if not self.parent: +# raise Exception("%s must be added to a parent before children can be added to it." % self.id) + pass + + +class G15Page(Component): + def __init__(self, page_id, screen, painter = None, priority = g15screen.PRI_NORMAL, on_shown=None, on_hidden=None, on_deleted=None, \ + thumbnail_painter = None, panel_painter = None, theme_properties_callback = None, \ + theme_attributes_callback = None, theme = None, title = None, + originating_plugin = None): + Component.__init__(self, page_id) + self.title = title if title else self.id + self.time = time.time() + self.originating_plugin = originating_plugin + self.thumbnail_painter = thumbnail_painter + self.panel_painter = panel_painter + self.on_shown = on_shown + self.on_hidden = on_hidden + self.on_deleted = on_deleted + self.priority = priority + self.value = self.priority * self.time + self.painter = painter + self.cairo = cairo + self.theme_scroll_timer = None + self.opacity = 0 + self.key_handlers = [] + self.properties = {} + self.attributes = {} + self.back_buffer = None + self.buffer = None + self.back_context = None + self.font_size = 12.0 + self.font_family = "Sans" + self.font_style = "normal" + self.font_weight = "normal" + self.on_shown_listeners = [] + self.on_hidden_listeners = [] + self.on_deleted_listeners = [] + self.theme_properties_callback = theme_properties_callback + self.theme_attributes_callback = theme_attributes_callback + self.screen = screen + self.scroll_lock = RLock() + self.focused_component = None + self.text_handler = g15text.new_text(screen) + if theme: + self.set_theme(theme) + + def set_focused_component(self, focused_component, redraw = True): + self.focused_component = focused_component + if redraw: + self.redraw() + + def notify_add(self, component): + Component.notify_add(self, component) + if not self.focused_component and component.focusable: + self.next_focus(False) + + def redraw(self, queue = True): + screen = self.get_screen() + if screen: + screen.redraw(self, queue) + + def next_focus(self, redraw = True): + focus_list = self._add_to_focus_list(self, []) + if len(focus_list) == 0: + self.focused_component = None + return + + if self.focused_component and self.focused_component in focus_list: + i = focus_list.index(self.focused_component) + i += 1 + if i >= len(focus_list): + i = 0 + self.focused_component = focus_list[i] + else: + self.focused_component = focus_list[0] + self.mark_dirty() + if redraw: + self.redraw() + + def _add_to_focus_list(self, component, focus_list = []): + if component.focusable: + focus_list.append(component) + for c in component.get_children(): + self._add_to_focus_list(c, focus_list) + return focus_list + + def is_visible(self): + screen = self.get_screen() + return screen and screen.get_visible_page() == self + + def set_title(self, title): + self.title = title + screen = self.get_screen() + if screen and screen.get_page(self.id) is not None: + screen.page_title_changed(self, title) + + def set_priority(self, priority): + screen = self.get_screen() + if not screen: + raise Exception("Cannot set priority, not added to screen") + screen.set_priority(self, priority) + + def set_time(self, time): + self.time = time + self.value = self.priority * self.time + + def get_val(self): + return self.time * self.priority + + def new_surface(self): + screen = self.get_screen() + if not screen: + raise Exception("Cannot create new surface, not added to screen") + sw = screen.driver.get_size()[0] + sh = screen.driver.get_size()[1] + self.back_buffer = cairo.ImageSurface (cairo.FORMAT_ARGB32,sw, sh) + self.back_context = cairo.Context(self.back_buffer) + self.text_handler = g15text.new_text(screen) + screen.configure_canvas(self.back_context) + self.text_handler.set_canvas(self.back_context) + self.set_line_width(1.0) + rgb = screen.driver.get_color(g15driver.HINT_FOREGROUND, ( 0, 0, 0 )) + self.foreground(rgb[0],rgb[1],rgb[2], 255) + + def draw_surface(self): + self.buffer = self.back_buffer + + def foreground(self, r, g, b, a = 255): + self.foreground_rgb = (r, g, b, a) + self.back_context.set_source_rgba(float(r) / 255.0, float(g) / 255.0, float(b) / 255.0, float(a) / 255.0) + + def save(self): + self.back_context.save() + + def delete(self): + self.screen.del_page(self) + + def restore(self): + self.back_context.restore() + + def set_line_width(self, line_width): + self.back_context.set_line_width(line_width) + + def arc(self, x, y, radius, angle1, angle2, fill = False): + self.back_context.arc(x, y, radius, g15convert.degrees_to_radians(angle1), g15convert.degrees_to_radians(angle2)) + if fill: + self.back_context.fill() + else: + self.back_context.stroke() + + def line(self, x1, y1, x2, y2): + self.back_context.line_to(x1, y1) + self.back_context.line_to(x2, y2) + self.back_context.stroke() + + def image(self, image, x, y): + self.back_context.translate(x, y) + self.back_context.set_source_surface(image) + self.back_context.paint() + self.back_context.translate(-x, -y) + + def rectangle(self, x, y, width, height, fill = False): + self.back_context.rectangle(x, y, width, height) + if fill: + self.back_context.fill() + else: + self.back_context.stroke() + + def paint(self, canvas): + self.text_handler.set_canvas(canvas) + if self.painter != None: + self.painter(canvas) + + Component.paint(self, canvas) + + # Paint the canvas + if self.buffer != None: + canvas.save() + canvas.set_source_surface(self.buffer) + canvas.paint() + canvas.restore() + + # Check the theme tree to see if anything needs scrolling + self.check_scroll_and_reschedule() + + def check_scroll_and_reschedule(self): + self.scroll_lock.acquire() + try: + scroll = self.check_for_scroll() + if scroll and self.theme_scroll_timer == None: + self.theme_scroll_timer = g15scheduler.schedule("ScrollRedraw", self.screen.service.scroll_delay, self.scroll_and_reschedule) + elif not scroll and self.theme_scroll_timer != None: + self.theme_scroll_timer.cancel() + self.theme_scroll_timer = None + finally: + self.scroll_lock.release() + + def scroll_and_reschedule(self): + self.scroll_lock.acquire() + try: + self.do_scroll() + self.theme_scroll_timer = None + self.redraw() + finally: + self.scroll_lock.release() + + def set_font(self, font_size = None, font_family = None, font_style = None, font_weight = None): + if font_size: + self.font_size = font_size + if font_family: + self.font_family = font_family + if font_style: + self.font_style = font_style + if font_weight: + self.font_weight = font_weight + + def text(self, text, x, y, width, height, constraints = ""): + bounds = None + if width > 0 and height > 0: + bounds = (x, y, width, height) + + al = constraints.split(",") + align = None + valign = None + wrap = None + wrap_width = None + for con in al: + if con == "wrapchar": + wrap = pango.WRAP_CHAR + elif con == "wrapword": + wrap = pango.WRAP_WORD + elif con == "wrapwordchar": + wrap = pango.WRAP_WORD_CHAR + else: + if align == None: + align = self._parse_align(con) + else: + valign = self._parse_align(con) + + wrap_width = int(pango.SCALE * width) if width > 0 and height > 0 else None + + self.text_handler.set_attributes(text, bounds, align = align, valign = valign, \ + font_desc = self.font_family, font_pt_size = self.font_size, \ + style = self.font_style, weight = self.font_weight, \ + width = wrap_width, wrap = wrap) + self.text_handler.draw(x, y) + + """ + Private + """ + def _parse_align(self, align): + if align == "center": + return pango.ALIGN_CENTER + elif align == "right" or align == "bottom": + return pango.ALIGN_RIGHT + else: + return pango.ALIGN_LEFT + + def _do_on_shown(self): + for l in self.on_shown_listeners: + l() + if self.on_shown: + self.on_shown() + + def _do_on_hidden(self): + for l in self.on_hidden_listeners: + l() + if self.on_hidden: + self.on_hidden() + + def _do_on_deleted(self): + if self.theme: + self.theme._component_removed() + for l in self.on_deleted_listeners: + l() + if self.on_deleted: + self.on_deleted() + + def _check_has_parent(self): + # Theme is the root, needs no parent + pass + + def _do_set_priority(self, priority): + self.priority = priority + self.value = self.priority * self.time + +class Scrollbar(Component): + + def __init__(self, id, values_callback = None): + Component.__init__(self, id) + self.values_callback = values_callback + + def on_configure(self): + Component.on_configure(self) + self._configure_track_and_bounds(self.get_theme(), self.get_theme().get_element(self.id)) + + def _configure_track_and_bounds(self, theme, element): + max_s, view_size, position = self.values_callback() + knob = element.xpath('svg:*[@class=\'knob\']',namespaces=theme.nsmap)[0] + track = element.xpath('svg:*[@class=\'track\']',namespaces=theme.nsmap)[0] + track_bounds = g15svg.get_bounds(track) + knob_bounds = g15svg.get_bounds(knob) + scale = max(1.0, max_s / view_size) + knob.set("y", str( int( knob_bounds[1] + ( position / max(scale, 0.01) ) ) ) ) + knob.set("height", str(int(track_bounds[3] / max(scale, 0.01) ))) + # TODO - don't destroy current styles + if scale == 1: + element.set("style", "visibility: hidden;") + else: + element.set("style", "") + + def draw(self, theme, element): + self._configure_track_and_bounds(theme, element) + +class Menu(Component): + def __init__(self, component_id): + Component.__init__(self, component_id) + self.selected = None + self.on_selected = None + self.on_move = None + self.i = 0 + self.do_clip = True + self.layout_manager = GridLayoutManager(1) + self.scroll_timer = None + + def set_scrollbar(self, scrollbar): + scrollbar.values_callback = self.get_scroll_values + Component.set_scrollbar(self, scrollbar) + + def select_last_item(self): + c = self.get_child_count() + if c > 0: + self.set_selected_item(self.get_children()[c - 1]) + + def set_selected_item(self, item): + i = self.index_of_child(item) + if i >= 0: + self.i = i + self._do_selected() + + def add_separator(self): + self.add_child(MenuSeparator()) + + def sort(self): + pass + + def on_configure(self): + menu_theme = self.load_theme() + if menu_theme: + self.set_theme(menu_theme) + + def configure(self, parent): + Component.configure(self, parent) + self._recalc_scroll_values() + if not self in self.get_screen().key_handler.action_listeners: + self.get_screen().key_handler.action_listeners.append(self) + + def notify_remove(self): + Component.notify_remove(self) + self.get_screen().key_handler.action_listeners.remove(self) + + def load_theme(self): + pass + + def add_child(self, child, index = -1): + Component.add_child(self, child, index) + self.select_first() + self._recalc_scroll_values() + self.centre_on_selected() + + def remove_child(self, child): + Component.remove_child(self, child) + self.select_first() + self._recalc_scroll_values() + self.centre_on_selected() + + def set_children(self, children): + was_selected = self.selected + Component.set_children(self, children) + if was_selected in self.get_children(): + self.selected = was_selected + else: + self.select_first() + self.centre_on_selected() + + def centre_on_selected(self): + y = 0 + c = self.get_children() + for r in range(0, self._get_selected_index()): + if c[r].is_showing(): + y += self.get_item_height(c[r], True) + self.base = max(0, y - ( self.view_bounds[3] / 2 )) + self._recalc_scroll_values() + self.get_root().redraw() + + def get_scroll_values(self): + return self.scroll_values + + def get_item_height(self, item, group = False): + if item.theme is None: + logger.warning("Component %s has no theme and so no height", item.id) + return 10 + else: + return item.theme.bounds[3] + + def paint(self, canvas): + g15screen.check_on_redraw() + self.get_tree_lock().acquire() + try: + + self.select_first() + + # Get the Y position of the selected item + y = 0 + selected_y = -1 + for item in self.get_children(): + # Only include items that are "showing" + if item.is_showing(): + ih = self.get_item_height(item, True) + if item == self.selected: + selected_y = y + y += ih + + new_base = self.base + + # How much vertical space there is + v_space = self.view_bounds[3] + + # If the position of the selected item is offscreen below, change the offset so it is just visible + if self.selected != None: + ih = self.get_item_height(self.selected, True) + if selected_y >= new_base + v_space - ih: + new_base = ( selected_y + ih ) - v_space + # If the position of the selected item is offscreen above base, change the offset so it is just visible + elif selected_y < new_base: + new_base = selected_y + + if new_base != self.base: + # Stop all of the children from scrolling horizontally while we scroll vertically + if self.get_screen().service.animated_menus: + if new_base < self.base: + self.base -= max(1, int(( self.base - new_base ) / 3)) + else: + self.base += max(1, int(( new_base - self.base ) / 3)) + else: + self.base = new_base + + self.get_root().mark_dirty() + self._recalc_scroll_values() + if self.scroll_timer is not None: + self.scroll_timer.cancel() + if self.get_screen().service.animated_menus: + self.scroll_timer = g15scheduler.schedule("ScrollTo", self.get_screen().service.animation_delay, self.get_root().redraw) + else: + self.get_root().redraw() + + Component.paint(self, canvas) + finally: + self.get_tree_lock().release() + + def get_items_per_page(self): + self.get_tree_lock().acquire() + try: + total_size = 0 + for item in self.get_children(): + total_size += self.get_item_height(item, True) + avg_size = total_size / self.get_child_count() + return int(self.view_bounds[3] / avg_size) + finally: + self.get_tree_lock().release() + + def action_performed(self, binding): + if self.is_visible(): + if binding.action == g15driver.NEXT_SELECTION: + self.get_screen().resched_cycle() + self._move_down(1) + return True + elif binding.action == g15driver.PREVIOUS_SELECTION: + self.get_screen().resched_cycle() + self._move_up(1) + return True + if binding.action == g15driver.NEXT_PAGE: + self.get_screen().resched_cycle() + self._move_down(10) + return True + elif binding.action == g15driver.PREVIOUS_PAGE: + self.get_screen().resched_cycle() + self._move_up(10) + return True + elif binding.action == g15driver.SELECT: + self.get_screen().resched_cycle() + if self.selected: + self.selected.activate() + return True + + def handle_key(self, keys, state, post): + self.select_first() + if not post and state == g15driver.KEY_STATE_DOWN: + if g15driver.G_KEY_UP in keys: + self._move_up(1) + return True + elif g15driver.G_KEY_DOWN in keys or g15driver.G_KEY_L4 in keys: + self._move_down(1) + return True + elif g15driver.G_KEY_RIGHT in keys: + self._move_down(self.get_items_per_page()) + return True + elif g15driver.G_KEY_LEFT in keys: + self._move_up(self.get_items_per_page()) + return True + elif g15driver.G_KEY_OK in keys or g15driver.G_KEY_L5 in keys: + if self.selected and self.selected.activate(): + return True + + + return False + + def select_first(self): + self.get_tree_lock().acquire() + try: + if not self.selected == None and not self.contains_child(self.selected): + self.selected = None + if self.selected == None: + cc = self.get_child_count() + if cc > 0: + for i in range(0, cc): + s = self.get_child(i) + if s.is_enabled() and not isinstance(s, MenuSeparator) : + self.selected = s + break + else: + self.selected = None + finally: + self.get_tree_lock().release() + + ''' + Private + ''' + + def _recalc_scroll_values(self): + max_val = 0 + for item in self.get_children(): + if item.is_showing(): + max_val += self.get_item_height(item, True) + + self.scroll_values = max(max_val, self.view_bounds[3]), self.view_bounds[3], self.base + + def _check_selected(self): + if not self.selected in self.get_children(): + if self.i >= self.get_child_count(): + return + self.selected = self.get_child(self.i) + + def _do_selected(self): + self.selected = self.get_child(self.i) + if self.on_selected: + self.on_selected() + self._recalc_scroll_values() + self.clear_scroll() + self.mark_dirty() + self.get_root().redraw() + + def _get_selected_index(self): + c = self.get_children() + if not self.selected in c: + return 0 if len(c) > 0 else -1 + else: + return self.index_of_child(self.selected) + + def _move_up(self, amount = 1): + self.get_tree_lock().acquire() + try: + if self.get_child_count() == 0: + return + if self.on_move: + self.on_move() + self._check_selected() + self.i = self._get_selected_index() + items = self.get_child_count() + try: + if self.i == 0: + self.i = items - 1 + return + + first_enabled = self._get_first_enabled() + if first_enabled > -1: + for a in range(0, abs(amount), 1): + while True: + self.i -= 1 + if self.i < first_enabled: + if a == 0: + self.i = self._get_last_enabled() + return + else: + self.i = first_enabled + c = self.get_child(self.i) + if not isinstance(c, MenuSeparator) and c.is_enabled() and c.is_showing() and c.activatable: + break + finally: + self._do_selected() + finally: + self.get_tree_lock().release() + + def _get_first_enabled(self): + for ci in range(0, self.get_child_count()): + c = self.get_child(ci) + if not isinstance(c, MenuSeparator) and c.is_enabled() and c.is_showing() and c.activatable: + return ci + return -1 + + def _get_last_enabled(self): + for ci in range(self.get_child_count() - 1, 0, -1): + c = self.get_child(ci) + if not isinstance(c, MenuSeparator) and c.is_enabled() and c.is_showing() and c.activatable: + return ci + return -1 + + + def _move_down(self, amount = 1): + self.get_tree_lock().acquire() + try: + if self.get_child_count() == 0: + return + if self.on_move: + self.on_move() + self._check_selected() + self.i = self._get_selected_index() + + items = self.get_child_count() + try: + if self.i == items - 1: + self.i = 0 + return + + first_enabled = self._get_first_enabled() + + if first_enabled > -1: + for a in range(0, abs(amount), 1): + while True: + self.i += 1 + if self.i == items: + if a == 0: + self.i = first_enabled + return + else: + self.i = self._get_last_enabled() + c = self.get_child(self.i) + if not isinstance(c, MenuSeparator) and c.is_enabled() and c.is_showing() and c.activatable: + break + finally: + self._do_selected() + finally: + self.get_tree_lock().release() + +class MenuScrollbar(Scrollbar): + def __init__(self, id, menu): + Scrollbar.__init__(self, id) + menu.set_scrollbar(self) + +class MenuItem(Component): + def __init__(self, component_id="menu-entry", group = True, name = None, alt = "", activate = None, icon = None, activatable = True): + Component.__init__(self, component_id) + self.group = group + self.name = name if name is not None else component_id + self.alt = alt + if activate is not None: + self.activate = activate + self.icon = icon + self.activatable = activatable + + def on_configure(self): + self.set_theme(G15Theme(self.parent.get_theme().dir, "menu-entry" if self.group else "menu-child-entry")) + + def get_theme_properties(self): + return { + "item_selected" : self.parent is not None and self == self.parent.selected, + "item_name" : self.name, + "item_alt" : self.alt, + "item_icon": self.icon + } + + def get_allow_scrolling(self): + self.get_tree_lock().acquire() + try: + return self.parent is not None and self == self.parent.selected + finally: + self.get_tree_lock().release() + + def activate(self): + return False + +class MenuSeparator(MenuItem): + def __init__(self, id = "menu-separator"): + MenuItem.__init__(self, id) + + def on_configure(self): + self.set_theme(G15Theme(self.parent.get_theme().dir, "menu-separator")) + +class DBusMenuItem(MenuItem): + def __init__(self, id, dbus_menu_entry): + MenuItem.__init__(self, id) + self.dbus_menu_entry = dbus_menu_entry + + def activate(self): + self.dbus_menu_entry.activate() + + def is_enabled(self): + return self.dbus_menu_entry.enabled + + def get_theme_properties(self): + properties = MenuItem.get_theme_properties(self) + properties["item_name"] = self.dbus_menu_entry.get_label() + properties["item_type"] = self.dbus_menu_entry.type + properties["item_enabled"] = self.dbus_menu_entry.enabled + properties["item_radio"] = self.dbus_menu_entry.toggle_type == dbusmenu.TOGGLE_TYPE_RADIO + properties["item_radio_selected"] = self.dbus_menu_entry.toggle_state == 1 + properties["item_alt"] = self.dbus_menu_entry.get_alt_label() + icon_name = self.dbus_menu_entry.get_icon_name() + if icon_name != None: + properties["item_icon"] = g15cairo.load_surface_from_file(g15icontools.get_icon_path(icon_name), self.theme.bounds[3]) + else: + properties["item_icon"] = self.dbus_menu_entry.get_icon() + return properties + +class DBusMenu(Menu): + def __init__(self, dbus_menu): + Menu.__init__(self, "menu") + self.dbus_menu = dbus_menu + + def on_configure(self): + Menu.on_configure(self) + self.populate() + + def menu_changed(self, menu = None, property = None, value = None): + current_ids = [] + for item in self.get_children(): + current_ids.append(item.id) + + self.populate() + + # Scroll to item if it is newly visible + if menu != None: + if property != None and property == dbusmenu.VISIBLE and value and menu.type != "separator": + self.selected = menu + else: + # Layout change + + # See if the selected item is still there + if self.selected != None: + sel = self.selected + self.selected = None + for i in self.get_children(): + if i.id == sel.id: + self.selected = i + + # See if there are new items, make them selected + for item in self.get_children(): + if not item.id in current_ids: + self.selected = item + break + + self.select_first() + + def populate(self): + self.get_tree_lock().acquire() + try: + self.remove_all_children() + i = 0 + for item in self.dbus_menu.root_item.children: + if item.is_visible(): + if item.type == dbusmenu.TYPE_SEPARATOR: + self.add_child(MenuSeparator("dbus-menu-separator-%d" % i)) + else: + self.add_child(DBusMenuItem("dbus-menu-item-%d" % i, item)) + i += 1 + finally: + self.get_tree_lock().release() + +class ErrorScreen(G15Page): + + def __init__(self, screen, title, text, icon = "dialog-error"): + self.page = G15Page.__init__(self, title, screen, priority = g15screen.PRI_HIGH, \ + theme = G15Theme(os.path.join(g15globals.themes_dir, "default"), "error-screen")) + self.theme_properties = { + "title": title, + "text": text, + "icon": g15icontools.get_icon_path(icon) + } + self.get_screen().add_page(self) + self.redraw() + self.get_screen().key_handler.action_listeners.append(self) + + def action_performed(self, binding): + if binding.action == g15driver.SELECT: + self.get_screen().del_page(self) + self.get_screen().key_handler.action_listeners.remove(self) + +class ConfirmationScreen(G15Page): + + def __init__(self, screen, title, text, icon, callback, arg, cancel_callback = None): + G15Page.__init__(self, title, screen, priority = g15screen.PRI_HIGH, \ + theme = G15Theme(os.path.join(g15globals.themes_dir, "default"), "confirmation-screen")) + self.theme_properties = { + "title": title, + "text": text, + "icon": icon + } + self.arg = arg + self.callback = callback + self.cancel_callback = cancel_callback + self.get_screen().add_page(self) + self.redraw() + self.get_screen().key_handler.action_listeners.append(self) + + def action_performed(self, binding): + if binding.action == g15driver.PREVIOUS_SELECTION: + self.get_screen().del_page(self) + self.get_screen().key_handler.action_listeners.remove(self) + if self.cancel_callback is not None: + self.cancel_callback(self.arg) + elif binding.action == g15driver.NEXT_SELECTION: + self.get_screen().del_page(self) + self.get_screen().key_handler.action_listeners.remove(self) + self.callback(self.arg) + +class G15Theme(object): + def __init__(self, dir_path, variant = None, svg_text = None, prefix = None, auto_dirty = True, translation = None): + self.translation = translation + self.plugin = None + if isinstance(dir_path, ThemeDefinition): + self.dir = dir_path.directory + self.translation = dir_path.translation + self.plugin_module = dir_path.plugin_module + elif isinstance(dir_path, str): + self.dir = dir_path + elif dir is not None: + self.plugin = dir_path + self.dir = os.path.join(os.path.dirname(sys.modules[dir_path.__module__].__file__), "default") + else: + self.dir = None + self.document = None + self.variant = variant + self.page = None + self.instance = None + self.svg_processor = None + self.svg_text = svg_text + self.prefix = prefix + self.render_lock = RLock() + self.scroll_timer = None + self.dirty = True + self.component = None + self.auto_dirty = auto_dirty + self.render = None + self.scroll_state = {} + self.nsmap = { + 'sodipodi': 'http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd', + 'cc': 'http://web.resource.org/cc/', + 'svg': 'http://www.w3.org/2000/svg', + 'dc': 'http://purl.org/dc/elements/1.1/', + 'xlink': 'http://www.w3.org/1999/xlink', + 'rdf': 'http://www.w3.org/1999/02/22-rdf-syntax-ns#', + 'inkscape': 'http://www.inkscape.org/namespaces/inkscape', + } + + def set_variant(self, variant): + self.variant = variant + self._set_component(self.component) + self.mark_dirty() + + def clear_scroll(self): + for s in self.scroll_state: + self.scroll_state[s].reset() + + def _set_component(self, component): + self.render_lock.acquire() + try: + if self.component is not None and component is None: + # Give the python portion of the theme chance to de-initialize + if self.instance is not None and hasattr(self.instance, 'destroy'): + try: + self.instance.destroy(self) + except Exception as e: + logger.debug("Error destroying instance", exc_info = e) + + + self.component = component + page = component.get_root() if component is not None else None + + if self.page is not None: + self.page.on_shown_listeners.remove(self._page_visibility_changed) + self.page.on_hidden_listeners.remove(self._page_visibility_changed) + self.page = page if page is not None and isinstance(page, G15Page) else None + if self.page is not None: + self.page.on_shown_listeners.append(self._page_visibility_changed) + self.page.on_hidden_listeners.append(self._page_visibility_changed) + + if self.page is None: + self.document = None + self.screen = None + self.text = None + self.driver = None + self.bounds = None + else: + self.screen = self.page.get_screen() + self.text = g15text.new_text(self.screen) + self.driver = self.screen.driver + if self.dir != None: + self.theme_name = os.path.basename(self.dir) + prefix_path = self.prefix if self.prefix != None else os.path.basename(os.path.dirname(self.dir)).replace("-", "_")+ "_" + self.theme_name + "_" + + # The theme may have a python portion + module_name = self.get_path_for_variant(self.dir, self.variant, "py", fatal = False, prefix = prefix_path) + module = None + if module_name != None: + if not dir in sys.path: + sys.path.insert(0, self.dir) + module = __import__(os.path.basename(module_name)[:-3]) + self.instance = module + + path = self.get_path_for_variant(self.dir, self.variant, "svg") + + # Load translation for this variant + actual_variant = os.path.splitext(os.path.basename(path))[0] + self.translation = g15locale.get_translation(actual_variant, self.dir) + + self.document = etree.parse(path) + + + # Give the python portion of the theme chance to initialize + if self.instance is not None and hasattr(self.instance, 'create'): + try: + self.instance.create(self) + except Exception as e: + logger.debug("Error creating instance", exc_info = e) + + elif self.svg_text != None: + self.document = etree.ElementTree(etree.fromstring(self.svg_text)) + else: + raise Exception("Must either supply theme directory or SVG text") + + self.process_svg() + self.bounds = g15svg.get_bounds(self.document.getroot()) + finally: + self.render_lock.release() + + def process_svg(self): + self.driver.process_svg(self.document) + root = self.document.getroot() + + # Remove glow effects + if self.screen.service.disable_svg_glow: + for element in root.xpath('//svg:filter[@inkscape:label="Glow"]',namespaces=self.nsmap): + element.getparent().remove(element) + + # Remove sodipodi attributes + self.del_namespace("sodipodi", "http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd") + self.del_namespace("inkscape", "http://www.inkscape.org/namespaces/inkscape") + + # Translate text + if self.translation is not None: + for textel in root.xpath('//text()',namespaces=self.nsmap): + tpar = textel.getparent() + text = tpar.text + if text is not None and len(text) > 0 and text.startswith("_("): + tpar.text = self.translation.ugettext(text[2:-1].strip()) + + + def del_namespace(self, prefix, uri): + for e in self.document.getroot().xpath("//*[namespace-uri()='%s' or @*[namespace-uri()='%s']]" % ( uri, uri ) ,namespaces=self.nsmap): + attr = e.attrib + for k in list(attr.keys()): + if k.startswith("{%s}" % uri): + del attr[k] + if e.getparent() is not None and e.prefix == prefix: + e.getparent().remove(e) + + + def get_path_for_variant(self, dir, variant, extension, fatal = True, prefix = ""): + if variant == None: + variant = "" + elif variant != "": + variant = "-" + variant + + # First try the provided path (i.e the plugin directory) + path = os.path.join(dir, prefix + self.driver.get_model_name() + variant + "." + extension ) + if not os.path.exists(path): + # Next try the theme directory + path = os.path.join(dir, g15globals.themes_dir, "default", self.driver.get_model_name() + variant + "." + extension) + if not os.path.exists(path): + # Now look for a default theme file in the provided path (i.e the plugin directory) + path = os.path.join(dir, prefix + "default" + variant + "." + extension) + if not os.path.exists(path): + # Finally look for a default theme file in the theme directory + path = os.path.join(dir, g15globals.themes_dir, "default", "default" + variant + "." + extension) + if not os.path.exists(path): + if fatal: + raise Exception("Missing %s. No .%s file for model %s in %s for variant %s" % ( path, extension, self.driver.get_model_name(), dir, variant )) + else: + return None + return path + + def convert_css_size(self, css_size): + em = 1.0 + if css_size.endswith("px"): + # Get EM based on size of 10px (the default cairo context is 10 so this should be right?) + px = float(css_size[:len(css_size) - 2]) + em = px / BASE_PX + elif css_size.endswith("pt"): + # Convert to px first, then use same algorithm + pt = float(css_size[:len(css_size) - 2]) + px = ( pt * 96.0 ) / 72.0 + em = px / BASE_PX + elif css_size.endswith("%"): + em = float(css_size[:len(css_size) - 1]) / 100.0 + elif css_size.endswith("em"): + em = float(css_size) + else: + raise Exception("Unknown font size") + return em + + def get_string_width(self, text, canvas, css): + # Font family + font_family = css.get("font-family") + + # Font size (translate to 'em') + font_size_text = css.get("font-size") + em = self.convert_css_size(font_size_text) + + # Font weight + font_weight = cairo.FONT_WEIGHT_NORMAL + if css.get("font-weight") == "bold": + font_weight = cairo.FONT_WEIGHT_BOLD + + # Font style + font_slant = cairo.FONT_SLANT_NORMAL + if css.get("font-style") == "italic": + font_slant = cairo.FONT_SLANT_ITALIC + elif css.get("font-style") == "oblique": + font_slant = cairo.FONT_SLANT_OBLIQUE + + try : + canvas.save() + canvas.select_font_face(font_family, font_slant, font_weight) + canvas.set_font_size(em * 10.0 * ( 4 / 3) ) + return canvas.text_extents(text)[:4] + finally: + canvas.restore() + + def parse_css(self, styles_text): + # Parse CSS styles + styles = { } + for style in styles_text.split(";") : + style_args = style.lstrip().rstrip().split(":") + if len(style_args) > 1: + styles[style_args[0].rstrip()] = style_args[1].lstrip().rstrip() + if not "text-align" in styles: + styles["text-align"] = "start" + return styles + + def format_styles(self, styles): + buf = "" + for style in styles: + buf += style + ":" + styles[style] + ";" + return buf.rstrip(';') + + def get_element(self, element_id = None, root = None): + if root == None: + root = self.document.getroot() + if element_id is None: + return root + els = root.xpath('//svg:*[@id=\'%s\']' % str(element_id),namespaces=self.nsmap) + return els[0] if len(els) > 0 else None + + def get_element_by_tag(self, tag, root = None): + if root == None: + root = self.document.getroot() + els = root.xpath('svg:%s' % str(tag),namespaces=self.nsmap) + return els[0] if len(els) > 0 else None + + def mark_dirty(self): + self.dirty = True + + def draw(self, canvas, properties = {}, attributes = {}): + if self.render != None and self.auto_dirty: + if self.render.properties != properties or self.render.attributes != attributes or \ + self.render.properties.values() != properties.values() or self.render.attributes.values() != attributes.values(): + self.dirty = True + + if self.render == None or self.dirty: + self.render_lock.acquire() + + if self.document is None: + raise Exception("No document available! Paint called before component finished initialising") + + self.text.set_canvas(canvas) + + try: + document = deepcopy(self.document) + processing_result = None + + # Give the python portion of the theme chance to draw stuff under the SVG + if self.instance is not None and hasattr(self.instance, 'paint_background'): + try: + self.instance.paint_background(properties, attributes) + except Exception as e: + logger.debug("Error painting background", exc_info = e) + + root = document.getroot() + + # Process the SVG + self._process_deletes(root, properties) + self._process_components(root) + self._set_progress_bars(root, properties) + self._set_relative_image_paths(root) + self._convert_image_urls(root, properties) + self._do_shadow("shadow", self.screen.driver.get_color_as_hexrgb(g15driver.HINT_BACKGROUND, (255, 255,255)), root) + self._do_shadow("reverseshadow", self.screen.driver.get_color_as_hexrgb(g15driver.HINT_FOREGROUND, (0, 0, 0)), root) + self._set_highlight_color(root) + + text_boxes = [] + self._handle_text_boxes(root, text_boxes, properties, canvas) + + # Pass the SVG document to the SVG processor if there is one + if self.svg_processor != None: + self.svg_processor(document, properties, attributes) + + # Pass the SVG document to the theme's python code to manipulate the document if required + if self.instance is not None and hasattr(self.instance, 'process_svg'): + try: + processing_result = self.instance.process_svg(self.driver, + root, + properties, + self.nsmap) + except Exception as e: + logger.debug("Error processing SVG", exc_info = e) + + self._set_default_style(root) + + self.render = Render(document, properties, text_boxes, attributes, processing_result) + self.dirty = False + finally: + self.render_lock.release() + else: + self.text.set_canvas(canvas) + + self._render_document(canvas, self.render) + return self.render.document + + def is_scroll_required(self): + return len(self.scroll_state) > 0 + + def do_scroll(self): + try: + self.render_lock.acquire() + if len(self.scroll_state) > 0: + for key in self.scroll_state: + self.scroll_state[key].next() + return True + finally: + self.render_lock.release() + + """ + Private + """ + + def _process_components(self, root): + """ + Find all elements that are associated with child components in the component this + theme is attached to, and draw them too. + + Keyword arguments: + root -- root of document + """ + if self.component: + for component_id in self.component.child_map.keys(): + component_elements = root.xpath('//svg:*[@id=\'%s\']' % component_id,namespaces=self.nsmap) + if len(component_elements) > 0: + c = component_elements[0] + c_class = c.get("class") + if c_class and "hidden-root" in c_class: + c.getparent().remove(c) + self.component.child_map[component_id].draw(self, c) + else: + logger.warning("Cannot find SVG element for component %s", component_id) + + def _process_deletes(self, root, properties): + """ + Remove all elements that are dependent on properties having non blank values + + Keyword arguments: + root -- root of document + properties -- theme properties + """ + for element in root.xpath('//svg:*[@title]',namespaces=self.nsmap): + title = element.get("title") + if title != None: + args = title.split(" ") + if args[0] == "del": + var = args[1] + condition = True + if var.startswith("!"): + var = var[1:] + condition = False + if ( condition and var in properties and properties[var] != "" and properties[var] != False ) or \ + ( not condition and ( not var in properties or properties[var] == "" or properties[var] == False ) ): + element.getparent().remove(element) + + def _set_progress_bars(self, root, properties): + """ + Sets the width attribute for any elements that have a style of "progress" based on + the value in the theme properties (with a key that is equal to the ID of the + element, less the _progress suffix). + + Keyword arguments: + root -- root of document + properties -- theme properties + """ + for element in root.xpath('//svg:rect[@class=\'progress\']',namespaces=self.nsmap): + bounds = g15svg.get_bounds(element) + id = element.get("id") + if id.endswith("_progress"): + property_key = id[:-9] + if property_key in properties: + value = float(properties[property_key]) + if value == 0: + value = 0.1 + element.set("width", str(int((bounds[2] / 100.0) * value))) + else: + logger.warning("Found progress element with an ID that doesn't exist in " + \ + "theme properties. Theme directory is %s, variant is %s." % (self.dir, self.variant )) + else: + logger.warning("Found progress element with an ID that doesn't end in _progress") + + def _set_highlight_color(self, root): + """ + Replaces any elements that have a color equal to the "highlight" colour + default with the configured highlight color + + Keyword arguments: + root -- root of document + """ + if self.screen.driver.get_control_for_hint(g15driver.HINT_HIGHLIGHT): + for element in root.xpath('//svg:*[@style]',namespaces=self.nsmap): + element.set("style", element.get("style").replace(DEFAULT_HIGHLIGHT_COLOR, self.screen.driver.get_color_as_hexrgb(g15driver.HINT_HIGHLIGHT, (255, 0, 0 )))) + + def _set_relative_image_paths(self, root): + for element in root.xpath('//svg:image[@xlink:href]',namespaces=self.nsmap): + href = element.get("{http://www.w3.org/1999/xlink}href") + is_data = href and href.startswith("data:") + is_abs = href and ( href.startswith("http:") or href.startswith("https:") or href.startswith("file:") or href.startswith("/")) + is_var = href and "${" in href + if not is_data and not is_abs and not is_var: + href = os.path.join(self.dir, href) + element.set("{http://www.w3.org/1999/xlink}href", href) + + def _convert_image_urls(self, root, properties): + """ + Inserts either a local file URL or an embedded image URL into all + elements that have 'title' attribute whose value exists as a property + in the theme properties. + + Keyword arguments: + root -- root of document + properties -- theme properties + """ + for element in root.xpath('//svg:image',namespaces=self.nsmap): + id = element.get("title") + if id != None and id in properties and properties[id] != None: + file_str = StringIO() + val = properties[id] + if isinstance(val, str) and str(val).startswith("file:"): + file_str.write(val[5:]) + elif isinstance(val, str) and str(val).startswith("/"): + file_str.write(val) + else: + file_str.write("data:image/png;base64,") + img_data = StringIO() + if isinstance(val, cairo.Surface): + val.write_to_png(img_data) + file_str.write(base64.b64encode(img_data.getvalue())) + else: + file_str.write(val) + element.set("{http://www.w3.org/1999/xlink}href", file_str.getvalue()) + + def _set_default_style(self, root): + """ + Set the default fill color to be the default foreground. If elements don't specify their + own colour, they will inherit this + + Keyword arguments: + root -- root document element + """ + root_style = root.get("style") + fg_c = self.screen.driver.get_control_for_hint(g15driver.HINT_FOREGROUND) + fg_h = None + if fg_c != None: + val = fg_c.value + fg_h = "#%02x%02x%02x" % ( val[0],val[1],val[2] ) + if root_style != None: + root_styles = self.parse_css(root_style) + else: + root_styles = { } + root_styles["fill"] = fg_h + root.set("style", self.format_styles(root_styles)) + + def _handle_text_boxes(self, root, text_boxes, properties, canvas): + + # Look for text elements that have a clip path. If the rendered text is wider than + # the clip path, then this element may be scrolled. This clipped text can also + # be used to wrap and scroll vertical text, replacing the old 'text box' mechanism + + for element in root.xpath('//svg:text[@clip-path]',namespaces=self.nsmap): + id = element.get("id") + clip_path_node = self._get_clip_path_element(element) + vertical_wrap = "vertical-wrap" == element.get("title") + if clip_path_node is not None: + + t_span_node = self.get_element_by_tag("tspan", root = element) + if t_span_node is None: + # Doesn't have t_span + t_span_node = element + + t_span_text = t_span_node.text + if not t_span_text: + raise Exception("Text node had clip path, but no text/tspan->text could be found") + + clip_path_rect_node = self.get_element_by_tag("rect", clip_path_node) + if clip_path_rect_node is None: + raise Exception("No svg:rect for clip %s" % str(clip_path_node)) + clip_path_bounds = g15svg.get_actual_bounds(clip_path_rect_node, element) + text_bounds = g15svg.get_actual_bounds(element) + + text_box = TextBox() + text_box.text = Template(t_span_text).safe_substitute(properties) + text_box.css = self.parse_css(element.get("style")) + text_class = element.get("class") + if text_class: + if "reverseshadow" in text_class: + text_box.reverse_shadow = True + elif "shadow" in text_class: + text_box.normal_shadow = True + text_box.clip = clip_path_bounds + + self._update_text(text_box, vertical_wrap) + tx, ty, text_width, text_height = self.text.measure() +# text_width, text_height = self._get_actual_size(element, text_width, text_height) + text_box.bounds = ( text_bounds[0], text_bounds[1], text_width, text_height ) + + self._scroll_text_boxes(vertical_wrap, text_box, text_boxes, t_span_node, element) + + # Find all of the text boxes. This is a hack to get around rsvg not supporting + # flowText completely. The SVG must contain two elements. The first must have + # a class attribute of 'textbox' and the ID must be the property key that it + # will contain. The next should be the text element (which defines style etc) + # and must have an id attribute of _text. The text layer is + # then rendered after the SVG using Pango. + for element in root.xpath('//svg:rect[@class=\'textbox\']',namespaces=self.nsmap): + id = element.get("id") + logger.warning("DEPRECATED Text box with ID %s in %s", id, self.dir) + text_node = root.xpath('//*[@id=\'' + id + '_text\']',namespaces=self.nsmap)[0] + if text_node != None: + styles = self.parse_css(text_node.get("style")) + + # Store the text box + text_box = TextBox() + text_box.text = properties[id] + text_box.css = styles + text_box.wrap = True + text_boxes.append(text_box) + text_box.bounds = g15svg.get_actual_bounds(element) + text_box.clip = text_box.bounds + + # Remove the textnod SVG element + text_node.getparent().remove(text_node) + element.getparent().remove(element) + + def _scroll_text_boxes(self, vertical_wrap, text_box, text_boxes, t_span_node, element): + id = element.get("id") + text_height = text_box.bounds[3] + text_width = text_box.bounds[2] + clip_path_bounds = text_box.clip + + if vertical_wrap: + text_box.wrap = True + text_boxes.append(text_box) + if self.screen.service.scroll_amount > 0 and text_height > clip_path_bounds[3]: + if id in self.scroll_state: + scroll_item = self.scroll_state[id] + scroll_item.text_box = text_box + text_box.base = scroll_item.val + else: + scroll_item = VerticalWrapScrollState(text_box) + scroll_item.vertical = True + self.scroll_state[id] = scroll_item + diff = text_height - clip_path_bounds[3] + scroll_item.range = ( 0, diff) + scroll_item.step = self.screen.service.scroll_amount + scroll_item.transform_elements() + elif id in self.scroll_state: + del self.scroll_state[id] + + element.getparent().remove(element) + else: +# text_boxes.append(text_box) + + # Enable or disable scrolling + if self.screen.service.scroll_amount > 0 and text_width > clip_path_bounds[2]: + if id in self.scroll_state: + scroll_item = self.scroll_state[id] + scroll_item.element = element + else: + scroll_item = HorizontalScrollState(element) + + self.scroll_state[id] = scroll_item + diff = text_width - clip_path_bounds[2] + + #+ ( clip_path_bounds[0] - text_box.bounds[0] ) + if diff < 0: + raise Exception("Negative diff!?") + scroll_item.alignment = text_box.css["text-align"] + scroll_item.original = float(element.get("x")) + if scroll_item.alignment == "center": + scroll_item.range = ( -(diff / 2), (diff / 2)) + elif scroll_item.alignment == "start": + scroll_item.range = ( -diff, 0) + elif scroll_item.alignment == "end": + scroll_item.range = ( 0, diff) + + scroll_item.reset() + + scroll_item.step = self.screen.service.scroll_amount + scroll_item.other_elements = [t_span_node] + scroll_item.transform_elements() + elif id in self.scroll_state: + del self.scroll_state[id] +# element.getparent().remove(element) + + def _get_clip_path_element(self, element): + clip_val = element.get("clip-path") + if clip_val and len(clip_val) > 0 and clip_val != "none": + id = clip_val[5:-1] + el = self.get_element(id, element.getroottree().getroot()) + if el is None: + raise Exception("Text node had clip path (%s), but no clip path element with matching ID of %s could be found" % ( id, element.get("clip-path") ) ) + return el + + def _component_removed(self): + self.scroll_state = {} + self._set_component(None) + + def _page_visibility_changed(self): + pass + + def _render_document(self, canvas, render): + + encoded_properties = {} + # Encode entities in all the property values + for key in render.properties.keys(): + encoded_properties[key] = saxutils.escape(str(render.properties[key])) + + xml = etree.tostring(render.document) + t = Template(xml) + xml = t.safe_substitute(encoded_properties) + svg = rsvg.Handle() + try : + svg.write(xml) + if DEBUG_SVG: + print "------------------------------------------------------" + print xml + print "------------------------------------------------------" + except Exception as e: + logger.debug("Could not write SVG", exc_info = e) + try : + svg.close() + except Exception as e: + logger.debug("Could not close SVG", exc_info = e) + + svg.render_cairo(canvas) + + if len(render.text_boxes) > 0: + rgb = self.screen.driver.get_color_as_ratios(g15driver.HINT_FOREGROUND, ( 0, 0, 0 )) + bg_rgb = self.screen.driver.get_color_as_ratios(g15driver.HINT_BACKGROUND, ( 255, 255, 255 )) + for text_box in render.text_boxes: + self._render_text_box(canvas, text_box, rgb, bg_rgb) + + # Give the python portion of the theme chance to draw stuff over the SVG + if self.instance is not None and hasattr(self.instance, 'paint_foreground'): + try: + self.instance.paint_foreground(canvas, + render.properties, + render.attributes, + render.processing_result) + except Exception as e: + logger.debug("Error painting foreground", exc_info = e) + + def _render_text_box(self, canvas, text_box, rgb, bg_rgb): + self._update_text(text_box, text_box.wrap) + +# if "fill" in text_css: +# rgb = g15convert. css["fill"] +# else: +# foreground = None + + if text_box.normal_shadow or text_box.reverse_shadow: + if text_box.normal_shadow: + canvas.set_source_rgb(bg_rgb[0], bg_rgb[1], bg_rgb[2]) + else: + canvas.set_source_rgb(rgb[0], rgb[1], rgb[2]) + for x in range(-1, 2): + for y in range(-1, 2): + if x != 0 or y != 0: + self.text.draw(text_box.bounds[0] + x, text_box.bounds[1] + y - text_box.base) + + # Draw primary text to canvas + canvas.set_source_rgb(rgb[0], rgb[1], rgb[2]) + self.text.draw(text_box.bounds[0], text_box.bounds[1] - text_box.base) + + def _get_actual_size(self, element, width, height): + list_transforms = [ cairo.Matrix(width, 0.0, 0.0, height, float(element.get("x")), float(element.get("y"))) ] + el = element + while el != None: + list_transforms += g15svg.get_transforms(el) + el = el.getparent() + list_transforms.reverse() + t = list_transforms[0] + for i in range(1, len(list_transforms)): + t = t.multiply(list_transforms[i]) + xx, yx, xy, yy, x0, y0 = t + return ( xx, yy ) + + def _update_text(self, text_box, wrap = False): + + css = text_box.css + + font_size_css = css["font-size"] if "font-size" in css else None + font_pt_size = None + if font_size_css: + nw = "".join(font_size_css.split()).lower() + if nw.endswith("px"): + fs = float(font_size_css[:-2]) + font_pt_size = int(g15cairo.approx_px_to_pt(fs)) + elif nw.endswith("pt"): + font_pt_size = int(font_size_css[:-2]) + + + font_family = css["font-family"] if "font-family" in css else None + font_weight = css["font-weight"] if "font-weight" in css else None + font_style = css["font-style"] if "font-style" in css else None + if "text-align" in css: + text_align = css["text-align"] + else: + text_align = "start" + alignment = pango.ALIGN_LEFT + if text_align == "end": + alignment =pango.ALIGN_RIGHT + elif text_align == "center": + alignment = pango.ALIGN_CENTER + + # Determine wrap and width to use + if wrap: + width = int(pango.SCALE * text_box.clip[2]) + wrap = pango.WRAP_WORD_CHAR + else: + wrap = 0 + width = -1 + + # Update the text handler + self.text.set_attributes(text_box.text, bounds = text_box.clip, wrap = wrap, align = alignment, \ + width = width, spacing = 0, \ + style = font_style, weight = font_weight, \ + font_pt_size = font_pt_size, \ + font_desc = font_family) + + def _do_shadow(self, id, color, root): + """ + Shadow is a special text effect useful on the G15. It will take 8 copies of a text element, make + them the same color as the background, and render them under the original text element at x-1/y-1, + xy-1,x+1/y,x-1/y etc. This makes the text legible if it overlaps other text or an image ( + at the expense of losing some detail of whatever is underneath) + + Keyword arguments: + id -- id of element to shadow + color -- 3 element tuple for RGB values of colour to use for shadow + root -- SVG document root + """ + + + for element in root.xpath('//svg:*[@class=\'%s\']' % id,namespaces=self.nsmap): + clip_path_element = self._get_clip_path_element(element) + bounds = g15svg.get_bounds(element) + idx = 1 + for x in range(-1, 2): + for y in range(-1, 2): + if x != 0 or y != 0: + element_id = element.get("id") + shadowed_id = element_id + "_" + str(idx) if element_id else None + + # Copy the element itself + shadowed = deepcopy(element) + if shadowed_id: + shadowed.set("id", shadowed_id) + for bound_element in shadowed.iter(): + bound_element.set("x", str(bounds[0] + x)) + bound_element.set("y", str(bounds[1] + y)) + styles = self.parse_css(shadowed.get("style")) + if styles == None: + styles = {} + styles["fill"] = color + shadowed.set("style", self.format_styles(styles)) + element.addprevious(shadowed) + + # Copy the clip path + if clip_path_element is not None: + clip_copy = deepcopy(clip_path_element) + clip_id = clip_path_element.get("id") + new_clip_id = "%s_%d" % ( clip_id, idx ) + clip_copy.set("id", new_clip_id ) + shadowed.set("clip-path", "url(#%s)" % new_clip_id) + clip_path_element.addprevious(clip_copy) + + idx += 1 diff --git a/src/gnome15/g15top.py b/src/gnome15/g15top.py new file mode 100644 index 0000000..6b47305 --- /dev/null +++ b/src/gnome15/g15top.py @@ -0,0 +1,206 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2012 Brett Smith +# +# 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 . + +""" +This module has been written to be API compatible with the python-gtop bindings, +which are no longer available as from Ubuntu 12.10. The suggested replacement +is the GObject based bindings, which would be great except it means converting +ALL of Gnome15 to use such bindings. + +This class is stop gap until a better solution can be found +""" + +import os +import time + +class CPU(): + def __init__(self, name): + self.name = name + self.user = 0 + self.nice = 0 + self.sys = 0 + self.idle = 0 + +class CPUS(CPU): + def __init__(self): + CPU.__init__(self, "CPUS") + cpudata = open('/proc/stat') + self.cpus = [] + try: + for line in cpudata: + if line.startswith("cpu"): + (name, cuse, cn, csys, idle, tail) = line.split(None, 5) + cpu = self if name == "cpu" else CPU(name) + self.user = int(cuse) + self.nice = int(cn) + self.sys = int(csys) + self.idle = int(idle) + self.cpus.append(cpu) + finally: + cpudata.close() + +class ProcState(): + + def __init__(self, pid): + self.uid = 0 + self.cmd = "" + memdata = open('/proc/%d/status' % pid) + try: + for line in memdata: + if line.startswith("Uid:"): + self.uid = int(self._get_value(line)[0]) + elif line.startswith("Name:"): + self.cmd = self._get_value(line)[0] + finally: + memdata.close() + + def _get_value(self, line): + return line[line.index(':') + 1:].strip().split() + +class NetworkLoad(): + + def __init__(self, net, bytes_in, bytes_out): + self.net = net + self.bytes_in = bytes_in + self.bytes_out = bytes_out + +class Mem(): + + def __init__(self): + self.total = 0 + self.free = 0 + self.cached = 0 + memdata = open('/proc/meminfo') + try: + for line in memdata: + if line.startswith("MemTotal"): + self.total = self._get_value(line) + elif line.startswith("MemFree"): + self.free = self._get_value(line) + elif line.startswith("Cached"): + self.cached = self._get_value(line) + finally: + memdata.close() + + def _get_value(self, line): + return int(line[line.index(':') + 1:line.index('kB')]) * 1024 + +def netload(net): + """ + Get the network load details for the network interface described by the + provided network interface name + + Keyword arguments: + net -- network interface name + """ + prefix = '%6s:' % net + netdata = open('/proc/net/dev') + try: + for line in netdata: + if line.startswith(prefix): + data = line[line.index(':') + 1:].split() + return NetworkLoad(net, int(data[0]), int(data[8])) + finally: + netdata.close() + + +def netlist(): + """ + Returns a list of Net objects, one for each available network interface + """ + nets = [] + f = open("/proc/net/dev", "r") + tmp = f.readlines(2000) + f.close() + for line in tmp: + line = line.strip() + line = line.split(' ') + if len(line) > 0 and line[0].endswith(":"): + nets.append(line[0][:-1]) + + return nets + +def cpu(): + """ + Return an object containing data about all available CPUS + """ + return CPUS() + +def mem(): + """ + Return an object containing data about all available CPUS + """ + return Mem() + +def proclist(): + """ + Get a list of all process IDs + """ + n = [] + for d in os.listdir("/proc"): + if os.path.isdir("/proc/%s" % d): + try: + n.append(int(d)) + except ValueError: + pass + return n + +def proc_state(pid): + """ + Get an object describing the state of the given process + + Keyword arguments: + pid -- process ID + """ + return ProcState(pid) + +def proc_args(pid): + """ + Get the arguments used to launch a process + + Keyword arguments: + pid -- process ID + """ + cmddata = open('/proc/%d/cmdline' % pid) + try: + for line in cmddata: + return line.split("\0") + finally: + cmddata.close() + +class Uptime: + def __init__(self, uptime, idletime): + self.uptime = uptime + self.idletime = idletime + self.boot_time = time.time() - self.uptime + +def uptime(): + """ + Get the uptime of the computer + """ + cmddata = open('/proc/uptime') + try: + for line in cmddata: + vals = line.strip('\n').split(' ') + finally: + cmddata.close() + + return Uptime(float(vals[0]), float(vals[1])) + +if __name__ == "__main__": + for d in proclist(): + ps = proc_state(d) + print d,ps.cmd,ps.uid,proc_args(d) \ No newline at end of file diff --git a/src/gnome15/g15uinput.py b/src/gnome15/g15uinput.py new file mode 100644 index 0000000..1e1de45 --- /dev/null +++ b/src/gnome15/g15uinput.py @@ -0,0 +1,403 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2011 Brett Smith +# +# 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 . + +""" +Manages the use of uinput to inject input events (key presses, mouse movement, +joystick events) into the kernel. +""" + +import logging +import uinput +import util.g15os as g15os +import os +import subprocess +from uinput.ev import * +from threading import RLock +from gnome15 import g15globals +logger = logging.getLogger(__name__) + +MOUSE = "mouse" +JOYSTICK = "joystick" +DIGITAL_JOYSTICK = "digital-joystick" +KEYBOARD = "keyboard" +DEVICE_TYPES = [ MOUSE, KEYBOARD, JOYSTICK, DIGITAL_JOYSTICK ] + +""" +Joystick calibration values +""" +JOYSTICK_MIN = -127 +JOYSTICK_MAX = 127 +JOYSTICK_CENTER = 0 + +""" +Value sent by the hardware when the joystick is at the center +""" +DEVICE_JOYSTICK_CENTER=128 + +#capabilities = uinput.capabilities.CAPABILITIES +capabilities = uinput.ev.__dict__ +registered_parameters = { MOUSE: {}, + JOYSTICK: { + uinput.ABS_X: (JOYSTICK_MIN, JOYSTICK_MAX, 0, 0), + uinput.ABS_Y: (JOYSTICK_MIN, JOYSTICK_MAX, 0, 0), + }, + DIGITAL_JOYSTICK: { + uinput.ABS_X: (JOYSTICK_MIN, JOYSTICK_MAX, 0, 0), + uinput.ABS_Y: (JOYSTICK_MIN, JOYSTICK_MAX, 0, 0), + }, + KEYBOARD: {} } +uinput_devices = {} +locks = {} +for t in DEVICE_TYPES: + locks[t] = RLock() + +""" +These are the very unofficial vendor / produce codes used for the virtual +devices +""" +GNOME15_USB_VENDOR_ID = 0xdd55 +GNOME15_MOUSE_PRODUCT_ID = 0x0001 +GNOME15_JOYSTICK_PRODUCT_ID = 0x0002 +GNOME15_KEYBOARD_PRODUCT_ID = 0x0003 +GNOME15_DIGITAL_JOYSTICK_PRODUCT_ID = 0x0004 + +""" +python-uinput currently doesn't expose these constants +""" + +EV_KEY = 0x01 +EV_REL = 0x02 +EV_ABS = 0x03 + +""" +special virtual keys that are actually joystick movement. + +These 'virtual' uinput codes are created so that the user can assign macros to the left, right, up +and down directions of the joystick. +By default uinput only has two codes (ABS_X and ABS_Y) that specify the axis, the direction being +determinated by the value passed to uinput.emit. +""" +JS = 0x9999 +JS_LEFT = 0x9701 +JS_RIGHT = 0x9702 +JS_DOWN = 0x9703 +JS_UP = 0x9704 +JS_MOVEMENT = { + "X_LEFT" : (JS, JS_LEFT), + "X_RIGHT" : (JS, JS_RIGHT), + "Y_UP" : (JS, JS_UP), + "Y_DOWN" : (JS, JS_DOWN), +} +for k in JS_MOVEMENT: + capabilities[k] = JS_MOVEMENT[k] + +""" +Load the X Keysym to UInput map +""" +__keysym_map = {} +__keysym_map_path = "%s/keysym-to-uinput" % g15globals.ukeys_dir +if os.path.exists(__keysym_map_path): + f = open(__keysym_map_path, "r") + b = [] + for line in f.readlines(): + line = line.strip() + if not line == "" and not line.startswith("#"): + arr = line.split("=") + if len(arr) > 1: + __keysym_map[arr[0].lower()] = arr[1] +else: + logger.warning("Could not find keysym to uinput map %s", __keysym_map_path) + +def get_keysym_to_uinput_mapping(keysym): + """ + Get the mapping for the provided keysym. This is case insensitive. + + Keyword arguments: + keysym -- X keysym + """ + if keysym.lower() in __keysym_map: + return __keysym_map[keysym.lower()] + logger.warning("Failed to translate X keysym %s to UInput code. " \ + "You can add a mapping by editing %s. " \ + "Please also report this on the Gnome15 project forums.", + keysym, + __keysym_map_path) + +def are_calibration_tools_available(): + """ + Test for the existence of calibration tools 'jstest-gtk' and 'jscal'. + """ + return os.system("which jstest-gtk >/dev/null 2>&1") == 0 and os.system("which jscal >/dev/null 2>&1") == 0 + +def open_devices(): + """ + Initialize, opening all devices + """ + __check_devices() + +def close_devices(): + """ + Clean up, closing all the devices + """ + for device_type in DEVICE_TYPES: + if device_type in uinput_devices: + logger.debug("Closing UINPUT device %s", device_type) + del uinput_devices[device_type] + +def calibrate(device_type): + """ + Run external joystick calibration utility + + Keyword arguments: + device_type -- device type + """ + if are_calibration_tools_available(): + if not device_type in [ JOYSTICK, DIGITAL_JOYSTICK ]: + raise Exception("Cannot calibrate this device type (%s)" % device_type) + device_file = get_device(device_type) + if device_file: + load_calibration(device_type) + os.system("jstest-gtk '%s'" % (device_file)) + save_calibration(device_type) + +def save_calibration(device_type): + """ + Run external joystick calibration utility + + Keyword arguments: + device_type -- device type + """ + if are_calibration_tools_available(): + if not device_type in [ JOYSTICK, DIGITAL_JOYSTICK ]: + raise Exception("Cannot calibrate this device type (%s)" % device_type) + device_file = get_device(device_type) + if device_file: + proc = subprocess.Popen(["jscal", "-q", device_file ], stdout=subprocess.PIPE) + out = proc.communicate()[0] + js_config_file = _get_js_config_file(device_type) + f = open(js_config_file, "w") + try : + f.write(out) + finally : + f.close() + +def load_calibration(device_type): + """ + Run external joystick calibration utility + + Keyword arguments: + device_type -- device type + """ + if are_calibration_tools_available(): + if not device_type in [ JOYSTICK, DIGITAL_JOYSTICK ]: + raise Exception("Cannot calibrate this device type (%s)" % device_type) + device_file = get_device(device_type) + if device_file: + js_config_file = _get_js_config_file(device_type) + if os.path.exists(js_config_file): + f = open(js_config_file, "r") + try : + cal = f.readline().split() + logger.info("Calibrating using '%s'", cal) + proc = subprocess.Popen(cal, stdout=subprocess.PIPE) + logger.info("Calibrated. %s", proc.communicate()[0]) + except Exception as e: + logger.error("Failed to calibrate joystick device.", exc_info = e) + finally : + f.close() + else: + logger.warning("No joystick calibration available.") + +def _get_js_config_file(device_type): + """ + Returns the filename used for saving the joystick calibration file + + If the directory that should own the file doesn't exist, it will be + created. + + Keyword arguments: + device_type -- device_type + """ + g15os.mkdir_p(g15globals.user_config_dir) + return os.path.join(g15globals.user_config_dir, "%s.js" % device_type) + +def get_device(device_type): + """ + Find the actual input device given the virtual device type + + Keyword arguments: + device_type -- device type + """ + vi_path = "/sys/devices/virtual/input" + if os.path.exists(vi_path): + for p in os.listdir(vi_path): + dev_dir = "%s/%s" % (vi_path, p) + name_file = "%s/name" % (dev_dir) + if os.path.exists(name_file): + f = open(name_file, "r") + try : + device_name = f.readline().replace("\n", "") + if device_name == "gnome15-%s" % device_type: + dev_files = os.listdir(dev_dir) + for dp in dev_files: + if dp.startswith("js"): + return "/dev/input/%s" % dp + for dp in dev_files: + if dp.startswith("event"): + return "/dev/input/%s" % dp + finally : + f.close() + + +def syn(target): + """ + Emit the syn. + + Keyword arguments: + target -- target device type (MOUSE, KEYBOARD or JOYSTICK). + """ + uinput_devices[target].syn() + +def emit(target, code, value, syn=True): + """ + Emit an input event, optionally emit a SYN as well + + Keyword arguments: + target -- The target device type (MOUSE, KEYBOARD or JOYSTICK) + type code. + code -- uinput code (either single code, where type will be + determined by target or a tuple consisting of event + type and event code) + value -- uinput value + syn -- emit SYN (defaults to True) + """ + if not target in DEVICE_TYPES: + raise Exception("Invalid target. '%s' must be one of %s" % (target, str(DEVICE_TYPES))) + + if not isinstance(code, tuple): + if target == MOUSE and code in [ uinput.REL_X[1], uinput.REL_Y[1] ]: + logger.debug("UINPUT mouse event at %s, code = %s, val = %d, syn = %s", + target, + code, + value, + str(syn)) + code = ( EV_REL, code ) + elif ( target == JOYSTICK or target == DIGITAL_JOYSTICK ): + """ We translate the 'virtual' uinput codes into real uinput ones """ + if code == JS_LEFT: + value = JOYSTICK_MIN if value > 0 else JOYSTICK_CENTER + code = ABS_X + elif code == JS_RIGHT: + value = JOYSTICK_MAX if value > 0 else JOYSTICK_CENTER + code = ABS_X + elif code == JS_UP: + value = JOYSTICK_MIN if value > 0 else JOYSTICK_CENTER + code = ABS_Y + elif code == JS_DOWN: + value = JOYSTICK_MAX if value > 0 else JOYSTICK_CENTER + code = ABS_Y + else: + """ If we are simulating a bouton press, then the event is of type EV_KEY """ + code = (EV_KEY, code) + logger.debug("UINPUT joystick event at %s, code = %s, val = %d, syn = %s", + target, + code, + value, + str(syn)) + else: + code = ( EV_KEY, code ) + logger.debug("UINPUT uinput keyboard event at %s, code = %s, val = %d, syn = %s", + target, + code, + value, + str(syn)) + + locks[target].acquire() + try: + uinput_devices[target].emit( code, value, syn) + finally: + locks[target].release() + +def __get_keys(prefix, exclude = None): + l = [] + for k in sorted(capabilities.iterkeys()): + if k.startswith(prefix) and ( exclude == None or not k.startswith(exclude) ): + l.append(capabilities[k]) + return l + +def get_keys(device_type): + if device_type == MOUSE: + return __get_keys("BTN_", "BTN_TOOL_") + elif device_type == JOYSTICK: + return __get_keys("BTN_", "X_", "Y_") + else: + return __get_keys("KEY_") + +def get_buttons(device_type, real_uinput_only = False): + fname = os.path.join(g15globals.ukeys_dir, "%s.keys" % device_type) + f = open(fname, "r") + b = [] + for line in f.readlines(): + line = line.strip() + if not line == "" and not line.startswith("#"): + if line in capabilities: + b.append((line, capabilities[line][1])) + else: + logger.warning("Invalid key name '%s' in %s", line, fname) + return b + +def __check_devices(): + for device_type in DEVICE_TYPES: + if not device_type in uinput_devices: + logger.info("Opening uinput device for %s", device_type) + keys = [] + for b, _ in get_buttons(device_type, True): + if capabilities[b][0] < 0x9999: + keys.append(capabilities[b]) + + if device_type == MOUSE: + virtual_product_id = GNOME15_MOUSE_PRODUCT_ID + keys.append((REL_X[0], REL_X[1], 0, 255, 0, 0)) + keys.append((REL_Y[0], REL_Y[1], 0, 255, 0, 0)) + elif device_type == JOYSTICK: + virtual_product_id = GNOME15_JOYSTICK_PRODUCT_ID + keys.append(ABS_X + (JOYSTICK_MIN, JOYSTICK_MAX, 0, 0)) + keys.append(ABS_Y + (JOYSTICK_MIN, JOYSTICK_MAX, 0, 0)) + elif device_type == DIGITAL_JOYSTICK: + virtual_product_id = GNOME15_JOYSTICK_PRODUCT_ID + keys.append(ABS_X + (JOYSTICK_MIN, JOYSTICK_MAX, 0, 0)) + keys.append(ABS_Y + (JOYSTICK_MIN, JOYSTICK_MAX, 0, 0)) + else: + virtual_product_id = GNOME15_KEYBOARD_PRODUCT_ID + + caps = tuple(keys) + uinput_device = uinput.Device(name="gnome15-%s" % device_type, + events = caps, + vendor = GNOME15_USB_VENDOR_ID, + product = virtual_product_id) + uinput_devices[device_type] = uinput_device + + # Centre the joystick by default + if device_type == JOYSTICK or device_type == DIGITAL_JOYSTICK: + syn(device_type) + load_calibration(device_type) + emit(device_type, ABS_X, JOYSTICK_CENTER, False) + emit(device_type, ABS_Y, JOYSTICK_CENTER, False) + syn(device_type) + else: + emit(device_type, 0, 0) + emit(device_type, 0, 1) + diff --git a/src/gnome15/g15upgrade.py b/src/gnome15/g15upgrade.py new file mode 100644 index 0000000..fc9f924 --- /dev/null +++ b/src/gnome15/g15upgrade.py @@ -0,0 +1,116 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2011 Brett Smith +# +# 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 . + +""" +Utility to upgrade from earlier versions of Gnome15. This should be called +upon startup of either g15-config or g15-desktop-service to check whether +any migration needs to take place. +""" + +import os.path +import g15devices +import g15globals +import util.g15pythonlang as g15pythonlang +import logging +import shutil +import sys +import subprocess + +logger = logging.getLogger(__name__) + +def upgrade(): + version_0_x_0_to_0_7_0() + version_0_x_0_to_0_8_5() + +def version_0_x_0_to_0_8_5(): + """ + Location of mail accounts moved + """ + old_path = os.path.expanduser("~/.gnome2/gnome15/lcdbiff/mailboxes.xml") + new_path = os.path.join(g15globals.user_config_dir, "plugin-data", "lcdbiff", "mailboxes.xml") + if os.path.exists(old_path) and not os.path.exists(new_path): + logger.warning("Upgrading to 0.8.5, moving mailboxes") + os.renames(old_path, new_path) + +def version_0_x_0_to_0_7_0(): + """ + First version to upgrade configuration. This is the version where + multiple device support was introduced, pushing configuration into + sub-directories + """ + macros_dir = os.path.join(g15globals.user_config_dir, "macro_profiles") + if os.path.exists(os.path.join(macros_dir, "0.macros")): + logger.info("Upgrading macros and configuration to 0.7.x format") + + """ + If the default macro profile exists at the root of the macro_profiles directory, + then conversion hasn't yet occurred. So, copy all profiles into all device + sub-directories + """ + devices = g15devices.find_all_devices() + for file in os.listdir(macros_dir): + if file.endswith(".macros"): + profile_file = os.path.join(macros_dir, file) + for device in devices: + device_dir = os.path.join(macros_dir, device.uid) + if not os.path.exists(device_dir): + logger.info("Creating macro_profile directory for %s", device.uid) + os.mkdir(device_dir) + logger.info("Copying macro_profile %s to %s ", file, device.uid) + shutil.copyfile(profile_file, os.path.join(device_dir, file)) + os.remove(profile_file) + + """ + Copy the GConf folders. + """ + gconf_dir = os.path.expanduser("~/.gconf/apps/gnome15") + gconf_file = os.path.join(gconf_dir, "%gconf.xml") + gconf_plugins_dir = os.path.join(gconf_dir, "plugins") + for device in devices: + device_dir = os.path.join(gconf_dir, device.uid) + if not os.path.exists(device_dir): + logger.info("Creating GConf directory for %s", device.uid) + os.mkdir(device_dir) + logger.info("Copying settings %s to %s", gconf_file, device.uid) + shutil.copyfile(gconf_file, os.path.join(device_dir, "%gconf.xml")) + logger.info("Copying plugin settings %s to %s", gconf_plugins_dir, device.uid) + target_plugins_path = os.path.join(device_dir, "plugins") + if not os.path.exists(target_plugins_path): + shutil.copytree(gconf_plugins_dir, target_plugins_path ) + logger.info("Clearing current settings root") + shutil.rmtree(gconf_plugins_dir) + f = open(gconf_file, 'w') + try: + f.write('\n') + f.write('\n') + f.write('\n') + finally: + f.close() + + + """ + Tell GConf to reload it caches by finding it's process ID and sending it + SIGHUP + """ + if sys.version_info > (2, 6): + process_info = subprocess.check_output(["sh", "-c", "ps -U %d|grep gconfd|head -1" % os.getuid()]) + else: + import commands + process_info = commands.getstatusoutput("sh -c \"ps -U %d|grep gconfd|head -1\"" % os.getuid()) + if process_info: + pid = g15pythonlang.split_args(process_info)[0] + logger.info("Sending process %s SIGHUP", pid) + subprocess.check_call([ "kill", "-SIGHUP", pid ]) diff --git a/src/gnome15/g15util.py b/src/gnome15/g15util.py new file mode 100644 index 0000000..21c32fb --- /dev/null +++ b/src/gnome15/g15util.py @@ -0,0 +1,287 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2010 Brett Smith +# Copyright (C) 2013 Nuno Araujo +# +# 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 . + +''' +This file only exists to keep compatibility with 3rd party plugins. +It has been splitted into several files +''' + +import util.g15cairo as g15cairo +import util.g15convert as g15convert +import util.g15gconf as g15gconf +import util.g15icontools as g15icontools +import util.g15markup as g15markup +import util.g15os as g15os +import util.g15pythonlang as g15pythonlang +import util.g15scheduler as g15scheduler +import util.g15svg as g15svg +import util.g15uigconf as g15uigconf +import g15notify +import g15driver + + +def execute_for_output(cmd): + return g15os.get_command_output(cmd) + +def run_script(script, args = None, background = True): + return g15os.run_script(script, args, background) + +def attr_exists(obj, attr_name): + return g15pythonlang.attr_exists(obj, attr_name) + +def call_if_exists(obj, function_name, *args): + g15pythonlang.call_if_exists(obj, function_name, args) + +def configure_colorchooser_from_gconf(gconf_client, gconf_key, widget_id, default_value, widget_tree, default_alpha = None): + g15uigconf.configure_colorchooser_from_gconf(gconf_client, gconf_key, widget_id, default_value, widget_tree, default_alpha) + +def to_cairo_rgba(gconf_client, key, default): + return g15gconf.get_cairo_rgba_or_default(gconf_client, key, default) + +def color_changed(widget, gconf_client, key): + g15uigconf.color_changed(widget, gconf_client, key) + +def rgb_to_string(rgb): + return g15convert.rgb_to_string(rgb) + +def get_alt_color(color): + return g15convert.get_alt_color(color) + +def color_to_rgb(color): + return g15convert.color_to_rgb(color) + +def to_rgb(string_rgb, default = None): + return g15convert.to_rgb(string_rgb, default) + +def to_pixel(rgb): + return g15convert.to_pixel(rgb) + +def to_color(rgb): + return g15convert.to_color(rgb) + +def spinner_changed(widget, gconf_client, key, model, decimal = False): + g15uigconf.spinner_changed(widget, gconf_client, key, model, decimal) + +def configure_spinner_from_gconf(gconf_client, gconf_key, widget_id, default_value, widget_tree, decimal = False): + g15uigconf.configure_spinner_from_gconf(gconf_client, gconf_key, widget_id, default_value, widget_tree, decimal) + +def configure_combo_from_gconf(gconf_client, gconf_key, widget_id, default_value, widget_tree): + g15uigconf.configure_combo_from_gconf(gconf_client, gconf_key, widget_id, default_value, widget_tree) + +def combo_box_changed(widget, gconf_client, key, model, default_value): + g15uigconf.combo_box_changed(widget, gconf_client, key, model, default_value) + +def boolean_conf_value_change(client, connection_id, entry, args): + g15uigconf.boolean_conf_value_change(client, connection_id, entry, args) + +def text_conf_value_change(client, connection_id, entry, args): + g15uigconf.text_conf_value_change(client, connection_id, entry, args) + +def radio_conf_value_change(client, connection_id, entry, args): + g15uigconf.radio_conf_value_change(client, connection_id, entry, args) + +def configure_checkbox_from_gconf(gconf_client, gconf_key, widget_id, default_value, widget_tree, watch_changes = False): + return g15uigconf.configure_checkbox_from_gconf(gconf_client, gconf_key, widget_id, default_value, widget_tree, watch_changes) + +def configure_text_from_gconf(gconf_client, gconf_key, widget_id, default_value, widget_tree, watch_changes = False): + return g15uigconf.configure_text_from_gconf(gconf_client, gconf_key, widget_id, default_value, widget_tree, watch_changes) + +def configure_radio_from_gconf(gconf_client, gconf_key, widget_ids , gconf_values, default_value, widget_tree, watch_changes = False): + return g15uigconf.configure_radio_from_gconf(gconf_client, gconf_key, widget_ids , gconf_values, default_value, widget_tree, watch_changes) + +def configure_adjustment_from_gconf(gconf_client, gconf_key, widget_id, default_value, widget_tree): + g15uigconf.configure_adjustment_from_gconf(gconf_client, gconf_key, widget_id, default_value, widget_tree) + +def adjustment_changed(adjustment, key, gconf_client, integer = True): + g15uigconf.adjustment_changed(adjustment, key, gconf_client, integer) + +def checkbox_changed(widget, key, gconf_client): + g15uigconf.checkbox_changed(widget, key, gconf_client) + +def text_changed(widget, key, gconf_client): + g15uigconf.text_changed(widget, key, gconf_client) + +def radio_changed(widget, key, gconf_client, gconf_value): + g15uigconf.radio_changed(widget, key, gconf_client, gconf_value) + +def get_float_or_default(gconf_client, key, default = None): + return g15gconf.get_float_or_default(gconf_client, key, default) + +def get_string_or_default(gconf_client, key, default = None): + return g15gconf.get_string_or_default(gconf_client, key, default) + +def get_bool_or_default(gconf_client, key, default = None): + return g15gconf.get_bool_or_default(gconf_client, key, default) + +def get_int_or_default(gconf_client, key, default = None): + return g15gconf.get_int_or_default(gconf_client, key, default) + +def get_rgb_or_default(gconf_client, key, default = None): + return g15gconf.get_rgb_or_default(gconf_client, key, default) + +def is_gobject_thread(): + return g15pythonlang.is_gobject_thread() + +def set_gobject_thread(): + g15pythonlang.set_gobject_thread() + +def get_lsb_release(): + return g15os.get_lsb_release() + +def get_lsb_distributor(): + return g15os.get_lsb_distributor() + +def append_if_exists( el, key, val, formatter = "%s"): + return g15pythonlang.append_if_exists( el, key, val, formatter) + +def get_command_output( cmd): + return g15os.get_command_output( cmd) + +def module_exists(module_name): + return g15pythonlang.module_exists(module_name) + +def value_or_empty(d, key): + return g15pythonlang.value_or_empty(d, key) + +def value_or_blank(d, key): + return g15pythonlang.value_or_blank(d, key) + +def value_or_default(d, key, default_value): + return g15pythonlang.value_or_default(d, key, default_value) + +def find(f, seq): + return g15pythonlang.find(f, seq) + +def mkdir_p(path): + g15os.mkdir_p(path) + +def notify(summary, body = "", icon = "dialog-info", actions = [], hints = {}, timeout = 0): + return g15notify.notify(summary, body, icon, actions, hints, timeout, 0) + +def strip_tags(html): + return g15markup.strip_tags(html) + +def total_seconds(time_delta): + return g15pythonlang.total_seconds(time_delta) + +def rgb_to_uint16(r, g, b): + return g15convert.rgb_to_uint16(r, g, b) + +def rgb_to_hex(rgb): + return g15convert.rgb_to_hex(rgb) + +def degrees_to_radians(degrees): + return g15convert.degrees_to_radians(degrees) + +def rotate(context, degrees): + g15cairo.rotate(context, degrees) + +def rotate_around_center(context, width, height, degrees): + g15cairo.rotate_around_center(context, width, height, degrees) + +def flip_horizontal(context, width, height): + g15cairo.flip_horizontal(context, width, height) + +def flip_vertical(context, width, height): + g15cairo.flip_vertical(context, width, height) + +def flip_hv_centered_on(context, fx, fy, cx, cy): + g15cairo.flip_hv_centered_on(context, fx, fy, cx, cy) + +def get_cache_filename(filename, size = None): + return g15cairo.get_cache_filename(filename, size) + +def get_image_cache_file(filename, size = None): + return g15cairo.get_image_cache_file(filename, size) + +def is_url(path): + return g15cairo.is_url(path) + +def load_surface_from_file(filename, size = None): + return g15cairo.load_surface_from_file(filename, size) + +def load_svg_as_surface(filename, size): + return g15cairo.load_svg_as_surface(filename, size) + +def image_to_surface(image, type = "ppm"): + return g15cairo.image_to_surface(image, type) + +def pixbuf_to_surface(pixbuf, size = None): + return g15cairo.pixbuf_to_surface(pixbuf, size) + +def local_icon_or_default(icon_name, size = 128): + return g15icontools.local_icon_or_default(icon_name, size) + +def get_embedded_image_url(path): + return g15icontools.get_embedded_image_url(path) + +def get_icon_path(icon = None, size = 128, warning = True, include_missing = True): + return g15icontools.get_icon_path(icon, size, warning, include_missing) + +def get_app_icon(gconf_client, icon, size = 128): + return g15icontools.get_app_icon(gconf_client, icon, size) + +def get_icon(gconf_client, icon, size = None): + return g15icontools.get_icon(gconf_client, icon, size) + +def paint_thumbnail_image(allocated_size, image, canvas): + return g15cairo.paint_thumbnail_image(allocated_size, image, canvas) + +def get_scale(target, actual): + return g15cairo.get_scale(target, actual) + +def approx_px_to_pt(px): + return g15cairo.approx_px_to_pt(px) + +def rotate_element(element, degrees): + g15svg.rotate_element(element, degrees) + +def split_args(args): + return g15pythonlang.split_args(args) + +def get_transforms(element, position_only = False): + return g15svg.get_transforms(element, position_only) + +def get_location(element): + return g15svg.get_location(element) + +def get_actual_bounds(element, relative_to = None): + return g15svg.get_actual_bounds(element, relative_to) + +def get_bounds(element): + return g15svg.get_bounds(element) + +def image_to_pixbuf(im, type = "ppm"): + return g15cairo.image_to_pixbuf(im, type) + +def surface_to_pixbuf(surface): + return g15cairo.surface_to_pixbuf(surface) + +def get_key_names(keys): + return g15driver.get_key_names(keys) + +def html_escape(text): + return g15markup.html_escape(text) + +def parse_as_properties(properties_string): + return g15pythonlang.parse_as_properties(properties_string) + +def to_int_or_none(s): + return g15pythonlang.to_int_or_none(s) + +def to_float_or_none(s): + return g15pythonlang.to_float_or_none(s) diff --git a/src/gnome15/lcdsink.py b/src/gnome15/lcdsink.py new file mode 100644 index 0000000..432e9d4 --- /dev/null +++ b/src/gnome15/lcdsink.py @@ -0,0 +1,92 @@ +# PiTiVi , Non-linear video editor +# +# pitivi/elements/thumbnailsink.py +# +# Copyright (c) 2005, Edward Hervey +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this program; if not, write to the +# Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, +# Boston, MA 02110-1301, USA. +""" +GdkPixbuf thumbnail sink +""" + +import gobject +import gst +import struct +import time + +big_to_cairo_alpha_mask = struct.unpack('=i', '\xFF\x00\x00\x00')[0] +big_to_cairo_red_mask = struct.unpack('=i', '\x00\xFF\x00\x00')[0] +big_to_cairo_green_mask = struct.unpack('=i', '\x00\x00\xFF\x00')[0] +big_to_cairo_blue_mask = struct.unpack('=i', '\x00\x00\x00\xFF')[0] + +class CairoSurfaceThumbnailSink(gst.BaseSink): + """ + GStreamer thumbnailing sink element. + + Can be used in pipelines to generates gtk.gdk.Pixbuf automatically. + """ + + __gsignals__ = { + "thumbnail": (gobject.SIGNAL_RUN_LAST, + gobject.TYPE_NONE, + ([gobject.TYPE_UINT64])) + } + + __gsttemplates__ = ( + gst.PadTemplate("sink", + gst.PAD_SINK, + gst.PAD_ALWAYS, + gst.Caps("video/x-raw-rgb," + "bpp = (int) 32, depth = (int) 32," + "endianness = (int) BIG_ENDIAN," + "alpha_mask = (int) %i, " + "red_mask = (int) %i, " + "green_mask = (int) %i, " + "blue_mask = (int) %i, " + "width = (int) [ 1, max ], " + "height = (int) [ 1, max ], " + "framerate = (fraction) [ 0, 25 ]" + % (big_to_cairo_alpha_mask, + big_to_cairo_red_mask, + big_to_cairo_green_mask, + big_to_cairo_blue_mask))) + ) + + def __init__(self): + gst.BaseSink.__init__(self) + self.width = 1 + self.height = 1 + self.set_sync(True) + self.data = None + + def do_set_caps(self, caps): + self.log("caps %s" % caps.to_string()) + self.log("padcaps %s" % self.get_pad("sink").get_caps().to_string()) + self.width = caps[0]["width"] + self.height = caps[0]["height"] + if not caps[0].get_name() == "video/x-raw-rgb": + return False + return True + + def do_render(self, buf): + self.data = str(buf.data) + self.emit('thumbnail', buf.timestamp) + return gst.FLOW_OK + + def do_preroll(self, buf): + return self.do_render(buf) + +gobject.type_register(CairoSurfaceThumbnailSink) diff --git a/src/gnome15/objgraph.py b/src/gnome15/objgraph.py new file mode 100644 index 0000000..398ab04 --- /dev/null +++ b/src/gnome15/objgraph.py @@ -0,0 +1,399 @@ +""" +Ad-hoc tools for drawing Python object reference graphs with graphviz. + +This module is more useful as a repository of sample code and ideas, than +as a finished product. For documentation and background, read + + http://mg.pov.lt/blog/hunting-python-memleaks.html + http://mg.pov.lt/blog/python-object-graphs.html + http://mg.pov.lt/blog/object-graphs-with-graphviz.html + +in that order. Then use pydoc to read the docstrings, as there were +improvements made since those blog posts. + +Copyright (c) 2008 Marius Gedminas + +Released under the MIT licence. + + +Changes +======= + +1.1dev (2008-09-05) +------------------- + +New function: show_refs() for showing forward references. + +New functions: typestats() and show_most_common_types(). + +Object boxes are less crammed with useless information (such as IDs). + +Spawns xdot if it is available. +""" +# Permission is hereby granted, free of charge, to any person obtaining a +# copy of this software and associated documentation files (the "Software"), +# to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, +# and/or sell copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. + +__author__ = "Marius Gedminas (marius@gedmin.as)" +__copyright__ = "Copyright (c) 2008 Marius Gedminas" +__license__ = "MIT" +__version__ = "1.1dev" +__date__ = "2008-09-05" + + +import gc +import inspect +import types +import weakref +import operator +import os + + +def count(typename): + """Count objects tracked by the garbage collector with a given class name. + + Example: + + >>> count('dict') + 42 + >>> count('MyClass') + 3 + + Note that the GC does not track simple objects like int or str. + """ + return sum(1 for o in gc.get_objects() if type(o).__name__ == typename) + + +def typestats(): + """Count the number of instances for each type tracked by the GC. + + Note that the GC does not track simple objects like int or str. + + Note that classes with the same name but defined in different modules + will be lumped together. + """ + stats = {} + for o in gc.get_objects(): + stats.setdefault(type(o).__name__, 0) + stats[type(o).__name__] += 1 + return stats + + +def show_most_common_types(limit=10): + """Count the names of types with the most instances. + + Note that the GC does not track simple objects like int or str. + + Note that classes with the same name but defined in different modules + will be lumped together. + """ + stats = sorted(typestats().items(), key=operator.itemgetter(1), + reverse=True) + if limit: + stats = stats[:limit] + width = max(len(name) for name, count in stats) + for name, count in stats[:limit]: + print name.ljust(width), count + + +def by_type(typename): + """Return objects tracked by the garbage collector with a given class name. + + Example: + + >>> by_type('MyClass') + [] + + Note that the GC does not track simple objects like int or str. + """ + return [o for o in gc.get_objects() if type(o).__name__ == typename] + + +def at(addr): + """Return an object at a given memory address. + + The reverse of id(obj): + + >>> at(id(obj)) is obj + True + + Note that this function does not work on objects that are not tracked by + the GC (e.g. ints or strings). + """ + for o in gc.get_objects(): + if id(o) == addr: + return o + return None + + +def find_backref_chain(obj, predicate, max_depth=20, extra_ignore=()): + """Find a shortest chain of references leading to obj. + + The start of the chain will be some object that matches your predicate. + + ``max_depth`` limits the search depth. + + ``extra_ignore`` can be a list of object IDs to exclude those objects from + your search. + + Example: + + >>> find_backref_chain(obj, inspect.ismodule) + [, ..., obj] + + Returns None if such a chain could not be found. + """ + queue = [obj] + depth = {id(obj): 0} + parent = {id(obj): None} + ignore = set(extra_ignore) + ignore.add(id(extra_ignore)) + ignore.add(id(queue)) + ignore.add(id(depth)) + ignore.add(id(parent)) + ignore.add(id(ignore)) + gc.collect() + while queue: + target = queue.pop(0) + if predicate(target): + chain = [target] + while parent[id(target)] is not None: + target = parent[id(target)] + chain.append(target) + return chain + tdepth = depth[id(target)] + if tdepth < max_depth: + referrers = gc.get_referrers(target) + ignore.add(id(referrers)) + for source in referrers: + if inspect.isframe(source) or id(source) in ignore: + continue + if id(source) not in depth: + depth[id(source)] = tdepth + 1 + parent[id(source)] = target + queue.append(source) + return None # not found + + +def show_backrefs(objs, max_depth=3, extra_ignore=(), filter=None, too_many=10, + highlight=None, filename = 'objgraph'): + """Generate an object reference graph ending at ``objs`` + + The graph will show you what objects refer to ``objs``, directly and + indirectly. + + ``objs`` can be a single object, or it can be a list of objects. + + Produces a Graphviz .dot file and spawns a viewer (xdot) if one is + installed, otherwise converts the graph to a .png image. + + Use ``max_depth`` and ``too_many`` to limit the depth and breadth of the + graph. + + Use ``filter`` (a predicate) and ``extra_ignore`` (a list of object IDs) to + remove undesired objects from the graph. + + Use ``highlight`` (a predicate) to highlight certain graph nodes in blue. + + Examples: + + >>> show_backrefs(obj) + >>> show_backrefs([obj1, obj2]) + >>> show_backrefs(obj, max_depth=5) + >>> show_backrefs(obj, filter=lambda x: not inspect.isclass(x)) + >>> show_backrefs(obj, highlight=inspect.isclass) + >>> show_backrefs(obj, extra_ignore=[id(locals())]) + + """ + show_graph(objs, max_depth=max_depth, extra_ignore=extra_ignore, + filter=filter, too_many=too_many, highlight=highlight, + edge_func=gc.get_referrers, swap_source_target=False, filename = filename) + + +def show_refs(objs, max_depth=3, extra_ignore=(), filter=None, too_many=10, + highlight=None): + """Generate an object reference graph starting at ``objs`` + + The graph will show you what objects are reachable from ``objs``, directly + and indirectly. + + ``objs`` can be a single object, or it can be a list of objects. + + Produces a Graphviz .dot file and spawns a viewer (xdot) if one is + installed, otherwise converts the graph to a .png image. + + Use ``max_depth`` and ``too_many`` to limit the depth and breadth of the + graph. + + Use ``filter`` (a predicate) and ``extra_ignore`` (a list of object IDs) to + remove undesired objects from the graph. + + Use ``highlight`` (a predicate) to highlight certain graph nodes in blue. + + Examples: + + >>> show_refs(obj) + >>> show_refs([obj1, obj2]) + >>> show_refs(obj, max_depth=5) + >>> show_refs(obj, filter=lambda x: not inspect.isclass(x)) + >>> show_refs(obj, highlight=inspect.isclass) + >>> show_refs(obj, extra_ignore=[id(locals())]) + + """ + show_graph(objs, max_depth=max_depth, extra_ignore=extra_ignore, + filter=filter, too_many=too_many, highlight=highlight, + edge_func=gc.get_referents, swap_source_target=True) + +# +# Internal helpers +# + +def show_graph(objs, edge_func, swap_source_target, + max_depth=3, extra_ignore=(), filter=None, too_many=10, + highlight=None, filename = 'objects'): + if not isinstance(objs, (list, tuple)): + objs = [objs] + f = file('%s.dot' % filename, 'w') + print >> f, 'digraph ObjectGraph {' + print >> f, ' node[shape=box, style=filled, fillcolor=white];' + queue = [] + depth = {} + ignore = set(extra_ignore) + ignore.add(id(objs)) + ignore.add(id(extra_ignore)) + ignore.add(id(queue)) + ignore.add(id(depth)) + ignore.add(id(ignore)) + for obj in objs: + print >> f, ' %s[fontcolor=red];' % (obj_node_id(obj)) + depth[id(obj)] = 0 + queue.append(obj) + gc.collect() + nodes = 0 + while queue: + nodes += 1 + target = queue.pop(0) + tdepth = depth[id(target)] + print >> f, ' %s[label="%s"];' % (obj_node_id(target), obj_label(target, tdepth)) + h, s, v = gradient((0, 0, 1), (0, 0, .3), tdepth, max_depth) + if inspect.ismodule(target): + h = .3 + s = 1 + if highlight and highlight(target): + h = .6 + s = .6 + v = 0.5 + v * 0.5 + print >> f, ' %s[fillcolor="%g,%g,%g"];' % (obj_node_id(target), h, s, v) + if v < 0.5: + print >> f, ' %s[fontcolor=white];' % (obj_node_id(target)) + if inspect.ismodule(target) or tdepth >= max_depth: + continue + neighbours = edge_func(target) + ignore.add(id(neighbours)) + n = 0 + for source in neighbours: + if inspect.isframe(source) or id(source) in ignore: + continue + if filter and not filter(source): + continue + if swap_source_target: + srcnode, tgtnode = target, source + else: + srcnode, tgtnode = source, target + elabel = edge_label(srcnode, tgtnode) + print >> f, ' %s -> %s%s;' % (obj_node_id(srcnode), obj_node_id(tgtnode), elabel) + if id(source) not in depth: + depth[id(source)] = tdepth + 1 + queue.append(source) + n += 1 + if n >= too_many: + print >> f, ' %s[color=red];' % obj_node_id(target) + break + print >> f, "}" + f.close() + print "Graph written to objects.dot (%d nodes)" % nodes + if os.system('which xdot >/dev/null') == 0: + print "Spawning graph viewer (xdot)" + os.system("xdot %s.dot &" % filename) + else: + os.system("dot -Tpng %s.dot > %s.png" % (filename, filename)) + print "Image generated as objects.png" + + +def obj_node_id(obj): + if isinstance(obj, weakref.ref): + return 'all_weakrefs_are_one' + return ('o%d' % id(obj)).replace('-', '_') + + +def obj_label(obj, depth): + return quote(type(obj).__name__ + ':\n' + + safe_repr(obj)) + + +def quote(s): + return s.replace("\\", "\\\\").replace("\"", "\\\"").replace("\n", "\\n") + + +def safe_repr(obj): + try: + return short_repr(obj) + except: + return '(unrepresentable)' + + +def short_repr(obj): + if isinstance(obj, (type, types.ModuleType, types.BuiltinMethodType, + types.BuiltinFunctionType)): + return obj.__name__ + if isinstance(obj, types.MethodType): + if obj.im_self is not None: + return obj.im_func.__name__ + ' (bound)' + else: + return obj.im_func.__name__ + if isinstance(obj, (tuple, list, dict, set)): + return '%d items' % len(obj) + if isinstance(obj, weakref.ref): + return 'all_weakrefs_are_one' + return repr(obj)[:40] + + +def gradient(start_color, end_color, depth, max_depth): + if max_depth == 0: + # avoid division by zero + return start_color + h1, s1, v1 = start_color + h2, s2, v2 = end_color + f = float(depth) / max_depth + h = h1 * (1-f) + h2 * f + s = s1 * (1-f) + s2 * f + v = v1 * (1-f) + v2 * f + return h, s, v + + +def edge_label(source, target): + if isinstance(target, dict) and target is getattr(source, '__dict__', None): + return ' [label="__dict__",weight=10]' + elif isinstance(source, dict): + for k, v in source.iteritems(): + if v is target: + if isinstance(k, basestring) and k: + return ' [label="%s",weight=2]' % quote(k) + else: + return ' [label="%s"]' % quote(safe_repr(k)) + return '' + diff --git a/src/gnome15/util/Makefile.am b/src/gnome15/util/Makefile.am new file mode 100644 index 0000000..b76f63e --- /dev/null +++ b/src/gnome15/util/Makefile.am @@ -0,0 +1,17 @@ +utildir = $(pkgpythondir)/util +util_PYTHON = \ + __init__.py \ + g15convert.py \ + g15scheduler.py \ + g15pythonlang.py \ + g15uigconf.py \ + g15gconf.py \ + g15os.py \ + g15cairo.py \ + g15svg.py \ + g15icontools.py \ + g15markup.py \ + jobqueue.py + +EXTRA_DIST = \ + $(util_PYTHON) \ No newline at end of file diff --git a/src/gnome15/util/__init__.py b/src/gnome15/util/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/gnome15/util/g15cairo.py b/src/gnome15/util/g15cairo.py new file mode 100644 index 0000000..0e398c4 --- /dev/null +++ b/src/gnome15/util/g15cairo.py @@ -0,0 +1,289 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2010 Brett Smith +# Copyright (C) 2013 Nuno Araujo +# +# 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 . + +''' +Cairo utilities +Has functions to transform, load and convert cairo surfaces +''' + +import gtk.gdk +import os, os.path +import cairo +import math +import rsvg +import urllib +import base64 +import xdg.Mime as mime +import g15convert +import g15os +import gnome15.g15globals + +# Logging +import logging +logger = logging.getLogger(__name__) + +from cStringIO import StringIO + +def rotate(context, degrees): + context.rotate(g15convert.degrees_to_radians(degrees)); + +def rotate_around_center(context, width, height, degrees): + context.translate (height * 0.5, width * 0.5); + context.rotate(degrees * (math.pi / 180)); + context.translate(-width * 0.5, -height * 0.5); + +def flip_horizontal(context, width, height): + flip_hv_centered_on(context, -1, 1, width / 2, height / 2) + +def flip_vertical(context, width, height): + # TODO - Should work according to http://cairographics.org/matrix_transform/, but doesn't + flip_hv_centered_on(context, -1, 1, width / 2, height / 2) + +def flip_hv_centered_on(context, fx, fy, cx, cy): + mtrx = cairo.Matrix(fx,0,0,fy,cx*(1-fx),cy*(fy-1)) + context.transform(mtrx) + +def get_cache_filename(filename, size = None): + cache_file = base64.urlsafe_b64encode("%s-%s" % ( filename, str(size if size is not None else "0,0") ) ) + g15os.mkdir_p(g15globals.user_cache_dir) + return os.path.join(g15globals.user_cache_dir, "%s.img" % cache_file) + +def get_image_cache_file(filename, size = None): + full_cache_path = get_cache_filename(filename, size) + if os.path.exists(full_cache_path): + return full_cache_path + +def is_url(path): + # TODO try harder + return "://" in path + +def load_surface_from_file(filename, size = None): + type = None + if filename == None: + logger.warning("Empty filename requested") + return None + + if filename.startswith("http:") or filename.startswith("https:"): + full_cache_path = get_image_cache_file(filename, size) + if full_cache_path: + meta_fileobj = open(full_cache_path + "m", "r") + type = meta_fileobj.readline() + meta_fileobj.close() + if type == "image/svg+xml" or filename.lower().endswith(".svg"): + return load_svg_as_surface(filename, size) + else: + return pixbuf_to_surface(gtk.gdk.pixbuf_new_from_file(full_cache_path), size) + + if is_url(filename): + type = None + try: + file = urllib.urlopen(filename) + data = file.read() + type = file.info().gettype() + + if filename.startswith("file://"): + type = str(mime.get_type(filename)) + + if filename.startswith("http:") or filename.startswith("https:"): + full_cache_path = get_cache_filename(filename, size) + cache_fileobj = open(full_cache_path, "w") + cache_fileobj.write(data) + cache_fileobj.close() + meta_fileobj = open(full_cache_path + "m", "w") + meta_fileobj.write(type + "\n") + meta_fileobj.close() + + if type == "image/svg+xml" or filename.lower().endswith(".svg"): + svg = rsvg.Handle() + try: + if not svg.write(data): + raise Exception("Failed to load SVG") + svg_size = svg.get_dimension_data()[2:4] + if size == None: + size = svg_size + surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, int(size[0]) if not isinstance(size, int) else size, int(size[1]) if not isinstance(size, int) else size) + context = cairo.Context(surface) + if size != svg_size: + scale = get_scale(size, svg_size) + context.scale(scale, scale) + svg.render_cairo(context) + surface.flush() + return surface + finally: + svg.close() + else: + if type == "text/plain": + if filename.startswith("file://"): + pixbuf = gtk.gdk.pixbuf_new_from_file(filename[7:]) + return pixbuf_to_surface(pixbuf, size) + raise Exception("Could not determine type") + else: + pbl = gtk.gdk.pixbuf_loader_new_with_mime_type(type) + pbl.write(data) + pixbuf = pbl.get_pixbuf() + pbl.close() + return pixbuf_to_surface(pixbuf, size) + return None + except Exception as e: + logger.warning("Failed to get image %s (%s).", filename, type, exc_info = e) + return None + else: + if os.path.exists(filename): + try: + if filename.lower().endswith(".svg"): + if os.path.islink(filename): + filename = os.path.realpath(filename) + return load_svg_as_surface(filename, size) + else: + return pixbuf_to_surface(gtk.gdk.pixbuf_new_from_file(filename), size) + + except Exception as e: + logger.warning("Failed to get image %s (%s).", filename, type, exc_info = e) + return None + +def load_svg_as_surface(filename, size): + svg = rsvg.Handle(filename) + try: + svg_size = svg.get_dimension_data()[2:4] + if size == None: + size = svg_size + sx = int(size) if isinstance(size, int) or isinstance(size, float) else int(size[0]) + sy = int(size) if isinstance(size, int) or isinstance(size, float) else int(size[1]) + surface = cairo.ImageSurface(0, sx, sy) + context = cairo.Context(surface) + if size != svg_size: + scale = get_scale(size, svg_size) + context.scale(scale, scale) + svg.render_cairo(context) + return surface + finally: + svg.close() + +def image_to_surface(image, type = "ppm"): + # TODO make better + return pixbuf_to_surface(image_to_pixbuf(image, type)) + +def pixbuf_to_surface(pixbuf, size = None): + x = pixbuf.get_width() + y = pixbuf.get_height() + scale = get_scale(size, (x, y)) + surface = cairo.ImageSurface(0, int(x * scale), int(y * scale)) + context = cairo.Context(surface) + gdk_context = gtk.gdk.CairoContext(context) + if size != None: + gdk_context.scale(scale, scale) + gdk_context.set_source_pixbuf(pixbuf,0,0) + gdk_context.paint() + gdk_context.scale(1 / scale, 1 / scale) + return surface + + +''' +Convert a PIL image to a GDK pixbuf +''' +def image_to_pixbuf(im, type = "ppm"): + p_type = type + if type == "ppm": + p_type = "pnm" + file1 = StringIO() + try: + im.save(file1, type) + contents = file1.getvalue() + finally: + file1.close() + loader = gtk.gdk.PixbufLoader(p_type) + loader.write(contents, len(contents)) + pixbuf = loader.get_pixbuf() + loader.close() + return pixbuf + +def surface_to_pixbuf(surface): + try: + file1 = StringIO() + surface.write_to_png(file1) + contents = file1.getvalue() + finally: + file1.close() + loader = gtk.gdk.PixbufLoader("png") + loader.write(contents, len(contents)) + pixbuf = loader.get_pixbuf() + loader.close() + return pixbuf + +def paint_thumbnail_image(allocated_size, image, canvas): + s = float(allocated_size) / image.get_height() + canvas.save() + canvas.scale(s, s) + canvas.set_source_surface(image) + canvas.paint() + canvas.scale(1 / s, 1 / s) + canvas.restore() + return image.get_width() * s + +def get_scale(target, actual): + scale = 1.0 + if target != None: + if isinstance(target, int) or isinstance(target, float): + sx = float(target) / actual[0] + sy = float(target) / actual[1] + else: + sx = float(target[0]) / actual[0] + sy = float(target[1]) / actual[1] + scale = max(sx, sy) + return scale + +pt_to_px = { + 6.0: 8.0, + 7.0: 9, + 7.5: 10, + 8.0: 11, + 9.0: 12, + 10.0: 13, + 10.5: 14, + 11.0: 15, + 12.0: 16, + 13.0: 17, + 13.5: 18, + 14.0: 19, + 14.5: 20, + 15.0: 21, + 16.0: 22, + 17.0: 23, + 18.0: 24, + 20.0: 26, + 22.0: 29, + 24.0: 32, + 26.0: 35, + 27.0: 36, + 28.0: 37, + 29.0: 38, + 30.0: 40, + 32.0: 42, + 34.0: 45, + 36.0: 48 + } +px_to_pt = {} +for pt in pt_to_px: + px_to_pt[pt_to_px[pt]] = pt + +def approx_px_to_pt(px): + px = round(px) + if px in px_to_pt: + return px_to_pt[px] + else: + return int(px * 72.0 / 96) + diff --git a/src/gnome15/util/g15convert.py b/src/gnome15/util/g15convert.py new file mode 100644 index 0000000..a1924e4 --- /dev/null +++ b/src/gnome15/util/g15convert.py @@ -0,0 +1,85 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2010 Brett Smith +# Copyright (C) 2013 Nuno Araujo +# +# 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 . + +''' +Various conversions +''' + +import gtk.gdk +import math + +def rgb_to_string(rgb): + if rgb == None: + return None + else: + return "%d,%d,%d" % rgb + +def get_alt_color(color): + if color[0] == color[1] == color[2]: + return (1.0-color[0], 1.0-color[1], 1.0-color[2], color[3]) + else: + return (color[1],color[2],color[0],color[3]) + +def color_to_rgb(color): + i = ( color.red >> 8, color.green >> 8, color.blue >> 8 ) + return ( i[0],i[1],i[2] ) + +def to_rgb(string_rgb, default = None): + # Currently this method is implemented in g15gconf so that it avoids + # a dependency on g15convert. + # g15convert depends on gtk, and when initializing the gtk module a + # DISPLAY needs to be available. + # Unfortunately, for running g15-system-service, there is no DISPLAY + # set in it's environment, so it would make it throw an error. + # See https://projects.russo79.com/issues/173 + import g15gconf + return g15gconf._to_rgb(string_rgb, default) + +def to_pixel(rgb): + return ( rgb[0] << 24 ) + ( rgb[1] << 16 ) + ( rgb[2] < 8 ) + 0 + +def to_color(rgb): + return gtk.gdk.Color(rgb[0] <<8, rgb[1] <<8,rgb[2] <<8) + +def rgb_to_uint16(r, g, b): + rBits = r * 32 / 255 + gBits = g * 64 / 255 + bBits = b * 32 / 255 + + rBits = rBits if rBits <= 31 else 31 + gBits = gBits if gBits <= 63 else 63 + bBits = bBits if bBits <= 31 else 31 + + valueH = (rBits << 3) | (gBits >> 3) + valueL = (gBits << 5) | bBits + + return chr(valueL & 0xff) + chr(valueH & 0xff) + +def rgb_to_hex(rgb): + # Currently this method is implemented in g15driver so that it avoids + # a dependency on g15convert. + # g15convert depends on gtk, and when initializing the gtk module a + # DISPLAY needs to be available. + # Unfortunately, for running g15-system-service, there is no DISPLAY + # set in it's environment, so it would make it throw an error. + # See https://projects.russo79.com/issues/173 + import g15driver + return g15driver.rgb_to_hex(rgb) + +def degrees_to_radians(degrees): + return degrees * (math.pi / 180.0) + diff --git a/src/gnome15/util/g15gconf.py b/src/gnome15/util/g15gconf.py new file mode 100644 index 0000000..6e03af3 --- /dev/null +++ b/src/gnome15/util/g15gconf.py @@ -0,0 +1,121 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2010 Brett Smith +# Copyright (C) 2013 Nuno Araujo +# +# 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 . + +''' +Set of utility methods to read values stored in gconf +''' + +def get_float_or_default(gconf_client, key, default = None): + """ + Tries to read a float value from GConf and return a default value it + doesn't exist. + + Keyword arguments: + gconf_client : GConf client instance that will read the value + key : full path of the key to be read + default : value to return if key was not found + """ + float_val = gconf_client.get(key) + return default if float_val == None else float_val.get_float() + +def get_string_or_default(gconf_client, key, default = None): + """ + Tries to read a string value from GConf and return a default value it + doesn't exist. + + Keyword arguments: + gconf_client : GConf client instance that will read the value + key : full path of the key to be read + default : value to return if key was not found + """ + str_val = gconf_client.get(key) + return default if str_val == None else str_val.get_string() + +def get_bool_or_default(gconf_client, key, default = None): + """ + Tries to read a boolean value from GConf and return a default value it + doesn't exist. + + Keyword arguments: + gconf_client : GConf client instance that will read the value + key : full path of the key to be read + default : value to return if key was not found + """ + bool_val = gconf_client.get(key) + return default if bool_val == None else bool_val.get_bool() + +def get_int_or_default(gconf_client, key, default = None): + """ + Tries to read a integer value from GConf and return a default value it + doesn't exist. + + Keyword arguments: + gconf_client : GConf client instance that will read the value + key : full path of the key to be read + default : value to return if key was not found + """ + int_val = gconf_client.get(key) + return default if int_val == None else int_val.get_int() + +def get_rgb_or_default(gconf_client, key, default = None): + """ + Tries to read a "rgb" value from GConf and return a default value it + doesn't exist. + A "rgb" value is in fact a comma separated string with the Red, Green and + Blue components encoded from 0 to 255. + + Keyword arguments: + gconf_client : GConf client instance that will read the value + key : full path of the key to be read + default : value to return if key was not found + """ + val = gconf_client.get_string(key) + return default if val == None or val == "" else _to_rgb(val) + +def get_cairo_rgba_or_default(gconf_client, key, default): + """ + Tries to read a "rgba" value from GConf and return a default value if + it doesn't exist. + A "rgba" value is encoded as two key on gconf. The first one is similar to + the rgb value described in get_rgb_or_default. The second one is stored in + _opacity and it represents the alpha value. + The returned value is encoded in a float tuple with each component ranging + from 0.0 to 1. + + Keyword arguments: + gconf_client : GConf client instance that will read the value + key : full path of the key to be read + default : value to return if key was not found + """ + str_val = gconf_client.get_string(key) + if str_val == None or str_val == "": + val = default + else: + v = _to_rgb(str_val) + alpha = gconf_client.get_int(key + "_opacity") + val = ( v[0], v[1],v[2], alpha) + return (float(val[0]) / 255.0, float(val[1]) / 255.0, float(val[2]) / 255.0, float(val[3]) / 255.0) + +def _to_rgb(string_rgb, default = None): + #This method should be in g15convert. The thing is that + #g15convert depends on gtk and on Fedora it raises an error when launching + #g15-system-service. + #(See https://projects.russo79.com/issues/173) + if string_rgb == None or string_rgb == "": + return default + rgb = string_rgb.split(",") + return (int(rgb[0]), int(rgb[1]), int(rgb[2])) diff --git a/src/gnome15/util/g15icontools.py b/src/gnome15/util/g15icontools.py new file mode 100644 index 0000000..e2e71da --- /dev/null +++ b/src/gnome15/util/g15icontools.py @@ -0,0 +1,137 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2010 Brett Smith +# Copyright (C) 2013 Nuno Araujo +# +# 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 . + +''' +Icon utilities +''' + +from gnome15 import g15globals +import g15cairo +import gtk.gdk +import os +import cairo +from PIL import Image +import urllib +import base64 + +# Logging +import logging +logger = logging.getLogger(__name__) + +from cStringIO import StringIO + +''' +Look for icons locally as well if running from source +''' +gtk_icon_theme = gtk.icon_theme_get_default() +if g15globals.dev: + gtk_icon_theme.prepend_search_path(g15globals.icons_dir) + +def local_icon_or_default(icon_name, size = 128): + return get_icon_path(icon_name, size) + +def get_embedded_image_url(path): + + file_str = StringIO() + try: + img_data = StringIO() + try: + file_str.write("data:") + + if isinstance(path, cairo.ImageSurface): + # Cairo canvas + file_str.write("image/png") + path.write_to_png(img_data) + else: + if not "://" in path: + # File + surface = load_surface_from_file(path) + file_str.write("image/png") + surface.write_to_png(img_data) + else: + # URL + pagehandler = urllib.urlopen(path) + file_str.write(pagehandler.info().gettype()) + while 1: + data = pagehandler.read(512) + if not data: + break + img_data.write(data) + + file_str.write(";base64,") + file_str.write(base64.b64encode(img_data.getvalue())) + return file_str.getvalue() + finally: + img_data.close() + finally: + file_str.close() + +def get_icon_path(icon = None, size = 128, warning = True, include_missing = True): + o_icon = icon + if isinstance(icon, list): + for i in icon: + p = get_icon_path(i, size, warning = False, include_missing = False) + if p != None: + return p + logger.warning("Icon %s (%d) not found", str(icon), size) + if include_missing and not icon in [ "image-missing", "gtk-missing-image" ]: + return get_icon_path(["image-missing", "gtk-missing-image"], size, warning) + else: + if icon != None: + icon = gtk_icon_theme.lookup_icon(icon, size, 0) + if icon != None: + if icon.get_filename() == None and warning: + logger.warning("Found icon %s (%d), but no filename was available", o_icon, size) + fn = icon.get_filename() + if os.path.isfile(fn): + return fn + elif include_missing and not icon in [ "image-missing", "gtk-missing-image" ]: + if warning: + logger.warning("Icon %s (%d) not found, using missing image", o_icon, size) + return get_icon_path(["image-missing", "gtk-missing-image"], size, warning) + else: + if os.path.isfile(o_icon): + return o_icon + else: + if warning: + logger.warning("Icon %s (%d) not found", o_icon, size) + if include_missing and not icon in [ "image-missing", "gtk-missing-image" ]: + return get_icon_path(["image-missing", "gtk-missing-image"], size, warning) + +def get_app_icon(gconf_client, icon, size = 128): + icon_path = get_icon_path(icon, size) + if icon_path == None: + icon_path = os.path.join(g15globals.icons_dir,"hicolor", "scalable", "apps", "%s.svg" % icon) + return icon_path + +def get_icon(gconf_client, icon, size = None): + real_icon_file = get_icon_path(icon, size) + if real_icon_file != None: + if real_icon_file.endswith(".svg"): + pixbuf = gtk.gdk.pixbuf_new_from_file(real_icon_file) + scale = g15cairo.get_scale(size, (pixbuf.get_width(), pixbuf.get_height())) + if scale != 1.0: + pixbuf = pixbuf.scale_simple(pixbuf.get_width() * scale, pixbuf.get_height() * scale, gtk.gdk.INTERP_BILINEAR) + img = Image.fromstring("RGBA", (pixbuf.get_width(), pixbuf.get_height()), pixbuf.get_pixels()) + else: + img = Image.open(real_icon_file) + scale = g15cairo.get_scale(size, img.size) + if scale != 1.0: + img = img.resize((img.size[0] * scale, img.size[1] * scale),Image.BILINEAR) + + return img + diff --git a/src/gnome15/util/g15markup.py b/src/gnome15/util/g15markup.py new file mode 100644 index 0000000..5c5c017 --- /dev/null +++ b/src/gnome15/util/g15markup.py @@ -0,0 +1,48 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2010 Brett Smith +# Copyright (C) 2013 Nuno Araujo +# +# 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 . + +''' +Markup utilities +''' + +from HTMLParser import HTMLParser + +class MLStripper(HTMLParser): + def __init__(self): + self.reset() + self.fed = [] + def handle_data(self, d): + self.fed.append(d) + def get_data(self): + return ''.join(self.fed) + +def strip_tags(html): + s = MLStripper() + s.feed(html) + return s.get_data() + +html_escape_table = { + "&": "&", + '"': """, + "'": "'", + ">": ">", + "<": "<", + } + +def html_escape(text): + return "".join(html_escape_table.get(c,c) for c in text) + diff --git a/src/gnome15/util/g15os.py b/src/gnome15/util/g15os.py new file mode 100644 index 0000000..79f7474 --- /dev/null +++ b/src/gnome15/util/g15os.py @@ -0,0 +1,133 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2010 Brett Smith +# Copyright (C) 2013 Nuno Araujo +# +# 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 . + +''' +Gnome15 utilities to work with the system (running commands, manipulating the +filesystem, getting OS information...) +''' + +from gnome15 import g15globals +import os + +# Logging +import logging +logger = logging.getLogger(__name__) + +def run_script(script, args = None, background = True): + """ + Runs a python script from the scripts directory. + + Keyword arguments: + script: the filename of the script to run + args: an array of arguments to pass to the script (optional, None by default) + background: Set to run the script in the background (optional, True by default) + """ + a = "" + if args: + for arg in args: + a += "\"%s\"" % arg + p = os.path.realpath(os.path.join(g15globals.scripts_dir,script)) + logger.info("Running '%s'", p) + return os.system("\"%s\" %s %s" % ( p, a, " &" if background else "" )) + +def get_command_output(cmd): + """ + Runs a command on the shell and returns it's status code and output + + Keyword arguments: + cmd: the command to run (either full path, or just the name if the command + is in the %PATH) + + Returns + A tuple with the exit code of the command and the output made on stdout by + the command. + Note: the last '\n' is stripped from the output. + """ + pipe = os.popen('{ ' + cmd + '; } 2>/dev/null', 'r') + text = pipe.read() + sts = pipe.close() + if sts is None: sts = 0 + if text[-1:] == '\n': text = text[:-1] + return sts, text + +def mkdir_p(path): + """ + Creates a directory and it's parents if needed unless it already exists.. + + Keyword arguments: + path: the full path to the directory to create. + """ + try: + os.makedirs(path) + except OSError as exc: # Python >2.5 + logger.debug("Error when trying to create path %s", path, exc_info = exc) + import errno + if exc.errno == errno.EEXIST: + pass + else: raise + +def full_path_of_program(program_name): + """ + Search for program_name in all the directories declared in the PATH + environment variable + + Keyword arguments: + program_name: the name of the program to search for + + Returns: + Full path name of the program_name, None if program_name was not + found in PATH. + """ + for dir in os.environ['PATH'].split(':'): + full_path = os.path.join(dir, program_name) + if os.path.exists(full_path): + return full_path + return None + +def is_program_in_path(program_name): + """ + Checks if a program_name is available in PATH environment variable + + Keyword arguments: + program_name: the name of the program to check + + Returns True if program_name is in PATH, else False + """ + return full_path_of_program(program_name) != None + +def get_lsb_release(): + """ + Gets the release number of the distribution + + Return: + ret: Return code of the lsb_release command + r: The release number + """ + ret, r = get_command_output('lsb_release -rs') + return float(r) if ret == 0 else 0 + +def get_lsb_distributor(): + """ + Gets the Linux distribution distributor id + + Return: + ret: Return code of the lsb_release command + r: The distributor id or "Unknown" if an error occurred + """ + ret, r = get_command_output('lsb_release -is') + return r if ret == 0 else "Unknown" + diff --git a/src/gnome15/util/g15pythonlang.py b/src/gnome15/util/g15pythonlang.py new file mode 100644 index 0000000..2f5a6b1 --- /dev/null +++ b/src/gnome15/util/g15pythonlang.py @@ -0,0 +1,186 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2010 Brett Smith +# Copyright (C) 2013 Nuno Araujo +# +# 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 . + +import re +import threading + +import logging +logger = logging.getLogger(__name__) + +''' +Helper methods "extending" the python syntax +''' + +def attr_exists(obj, attr_name): + """ + Get if an attribute exists on an object + + Keyword arguments: + obj -- object + attr_name -- attribute name + """ + return getattr(obj, attr_name, None) is not None + +def call_if_exists(obj, function_name, *args): + """ + Call a function on an object if it exists, ignoring any errors if it doesn't + """ + func = getattr(obj, function_name, None) + if callable(func): + func(*args) + +def module_exists(module_name): + """ + Get if a module exists + + Keyword arguments: + module_name: the name of the module to check + """ + try: + __import__(module_name) + except ImportError as e: + logger.debug("Could not find module %s", module_name, exc_info = e) + return False + else: + return True + +def value_or_empty(d, key): + """ + Returns the value corresponding to a given key in a dictionnary. + If no value is found, then an empty array is returned. + + Keyword arguments: + d: The dictionnary where to search for the value + key: The key to use for the lookup + """ + return value_or_default(d, key, []) + +def value_or_blank(d, key): + """ + Returns the value corresponding to a given key in a dictionnary. + If no value is found, then an empty string is returned. + + Keyword arguments: + d: The dictionnary where to search for the value + key: The key to use for the lookup + """ + return value_or_default(d, key, "") + +def value_or_default(d, key, default_value): + """ + Returns the value corresponding to a given key in a dictionnary. + If no value is found, then a default value is returned. + + Keyword arguments: + d: The dictionnary where to search for the value + key: The key to use for the lookup + default_value: The default value to return if no value is found + """ + try : + return d[key] + except KeyError as ke: + logger.debug("Didn't found %s in %s", key, d, exc_info = ke) + return default_value + +def to_int_or_none(s): + """ + Converts a string to a int or returns None if there was an error converting + """ + try: + return int(s) + except (ValueError, TypeError) as e: + logger.debug("Error converting %s to int", s, exc_info = e) + return None + +def to_float_or_none(s): + """ + Converts a string to a float or returns None if there was an error converting + """ + try: + return float(s) + except (ValueError, TypeError) as e: + logger.debug("Error converting %s to float", s, exc_info = e) + return None + +def find(f, seq): + """Return first item in sequence where f(item) == True.""" + for item in seq: + if f(item): + return item + +def append_if_exists(el, key, val, formatter = "%s"): + """ + Appends a value from a dictionnary to a string applying a formatter. + The value is only appended if it exists in the dictionnary and it's value is not None + + Keyword arguments: + el: The dictionnary where to search for the value + key: The key to search of the dictionnary + val: The string to which the found value will be appended + formatter: A format string to apply when appending the found value + + Returns: A new string with the found value appended to (prefixed by a comma) + """ + if key in el and el[key] is not None and len(str(el[key])) > 0: + if len(val) > 0: + val += "," + val += formatter % el[key] + return val + +def parse_as_properties(properties_string): + """ + Create a dictionnary [key,value] from a string containing a set of + name=value pairs separated by '\n' + + Keyword elements: + properties_string: string containing a set of name=value fields + """ + d = {} + for l in properties_string.split("\n"): + a = l.split("=") + if len(a) > 1: + d[a[0]] = a[1] + return d + +def split_args(args): + return re.findall(r'\w+', args) + +''' +Date / time utilities +''' +def total_seconds(time_delta): + """ + Calculate the total of seconds ellapsed in a timedelta value + + Keyword arguments: + time_delta: The timedelta value for which the number of seconds should be + calculated. + """ + return (time_delta.microseconds + (time_delta.seconds + time_delta.days * 24.0 * 3600.0) * 10.0**6.0) / 10.0**6.0 + +''' +GObject thread. Hosting applications may set this so that is_gobject_thread() +function works +''' +gobject_thread = [ None ] + +def is_gobject_thread(): + return threading.currentThread() == gobject_thread[0] + +def set_gobject_thread(): + gobject_thread[0] = threading.currentThread() + diff --git a/src/gnome15/util/g15scheduler.py b/src/gnome15/util/g15scheduler.py new file mode 100644 index 0000000..8902ae0 --- /dev/null +++ b/src/gnome15/util/g15scheduler.py @@ -0,0 +1,61 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2010 Brett Smith +# Copyright (C) 2013 Nuno Araujo +# +# 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 . + +import gobject +import g15pythonlang + +# Logging +import logging +logger = logging.getLogger(__name__) + +import jobqueue + +''' +Default scheduler +''' +scheduler = jobqueue.JobScheduler() + +''' +Task scheduler. Tasks may be added to the queue to execute +after a specified interval. The timer is done by the gobject +event loop, which then executes the job on a different thread +''' + +def clear_jobs(queue_name = None): + scheduler.clear_jobs(queue_name) + +def execute(queue_name, job_name, function, *args): + return scheduler.execute(queue_name, job_name, function, *args) + +def schedule(job_name, interval, function, *args): + return scheduler.schedule(job_name, interval, function, *args) + +def run_on_gobject(function, *args): + if g15pythonlang.is_gobject_thread(): + return False + else: + gobject.idle_add(function, *args) + return True + +def stop_queue(queue_name): + scheduler.stop_queue(queue_name) + +def queue(queue_name, job_name, interval, function, *args): + return scheduler.queue(queue_name, job_name, interval, function, *args) + +def stop_all_schedulers(): + scheduler.stop_all() diff --git a/src/gnome15/util/g15svg.py b/src/gnome15/util/g15svg.py new file mode 100644 index 0000000..5628e12 --- /dev/null +++ b/src/gnome15/util/g15svg.py @@ -0,0 +1,152 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2010 Brett Smith +# Copyright (C) 2013 Nuno Araujo +# +# 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 . + +''' +SVG utilities +''' + +import cairo +import g15pythonlang + +# Logging +import logging +logger = logging.getLogger(__name__) + + +def rotate_element(element, degrees): + transforms = get_transforms(element) + if len(transforms) > 0: + t = transforms[0] + for i in range(1, len(transforms)): + t = t.multiply(transforms[i]) + else: + t = cairo.Matrix() + + t.rotate(g15convert.degrees_to_radians(degrees)) + ts = "m" + str(t)[7:] + element.set("transform", ts) + +def get_transforms(element, position_only = False): + transform_val = element.get("transform") + list = [] + if transform_val != None: + start = 0 + while True: + start_args = transform_val.find("(", start) + if start_args == -1: + break + name = transform_val[:start_args].lstrip() + end_args = transform_val.find(")", start_args) + if end_args == -1: + break + args = transform_val[start_args + 1:end_args].split(",") + if name == "translate": + list.append(cairo.Matrix(1.0, 0.0, 0.0, 1.0, float(args[0]), float(args[1]))) + elif name == "matrix": + if position_only: + list.append(cairo.Matrix(float(args[0]), float(args[1]), float(args[2]), float(args[3]),float(args[4]),float(args[5]))) + else: + list.append(cairo.Matrix(1, 0, 0, 1, float(args[4]),float(args[5]))) + elif name == "scale": + list.append(cairo.Matrix(float(args[0]), 0.0, 0.0, float(args[1]), 0.0, 0.0)) + else: + logger.warning("Unsupported transform %s", name) + start = end_args + 1 + + return list + +def get_location(element): + list = [] + while element != None: + x = element.get("x") + y = element.get("y") + if x != None and y != None: + list.append((float(x), float(y))) + transform_val = element.get("transform") + if transform_val != None: + start = 0 + while True: + start_args = transform_val.find("(", start) + if start_args == -1: + break + name = transform_val[:start_args].lstrip() + end_args = transform_val.find(")", start_args) + if end_args == -1: + logger.warning("Unexpected end of transform arguments") + break + args = g15pythonlang.split_args(transform_val[start_args + 1:end_args]) + if name == "translate": + list.append((float(args[0]), float(args[1]))) + elif name == "matrix": + list.append((float(args[4]),float(args[5]))) + else: + logger.warning("WARNING: Unsupported transform %s", name) + start = end_args + 1 + element = element.getparent() + list.reverse() + x = 0 + y = 0 + for i in list: + x += i[0] + y += i[1] + return (x, y) + +def get_actual_bounds(element, relative_to = None): + id = element.get("id") + + bounds = get_bounds(element) + transforms = [] + t = cairo.Matrix() + t.translate(bounds[0],bounds[1]) + transforms.append(t) + + # If the element is a clip path and the associated clipped_node is provided, the work out the transforms from + # the parent of the clipped_node, not the clip itself + if relative_to is not None: + element = relative_to.getparent() + + while element != None: + transforms += get_transforms(element, position_only=True) + element = element.getparent() + transforms.reverse() + if len(transforms) > 0: + t = transforms[0] + for i in range(1, len(transforms)): + t = t.multiply(transforms[i]) + + xx, yx, xy, yy, x0, y0 = t + return x0, y0, bounds[2], bounds[3] + +def get_bounds(element): + x = 0.0 + y = 0.0 + w = 0.0 + h = 0.0 + v = element.get("x") + if v != None: + x = float(v) + v = element.get("y") + if v != None: + y = float(v) + v = element.get("width") + if v != None: + w = float(v) + v = element.get("height") + if v != None: + h = float(v) + return (x, y, w, h) + diff --git a/src/gnome15/util/g15uigconf.py b/src/gnome15/util/g15uigconf.py new file mode 100644 index 0000000..776cfd2 --- /dev/null +++ b/src/gnome15/util/g15uigconf.py @@ -0,0 +1,278 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2010 Brett Smith +# Copyright (C) 2013 Nuno Araujo +# +# 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 . + +import g15convert + +''' +Set of utility methods to ease the binding between UI widgets and gconf settings +''' + +def configure_colorchooser_from_gconf(gconf_client, gconf_key, widget_id, default_value, widget_tree, default_alpha = None): + """ + Sets the color and alpha values of a colorchooser widget from a gconf key + and initialize an event to store the value set by the user in the same + gconf key + + gconf_client: Instance of GConfClient to communicate with GConf + gconf_key: Name of the key having the value + widget_id: Id of the widget + default_value: Default value to set if it isn't set in GConf + widget_tree: Widget tree containing the widget to setup + default_alpha: If different than None, the alpha value for the color is read + from the gconf_key + "_opacity" key. + """ + widget = widget_tree.get_object(widget_id) + if widget == None: + raise Exception("No widget with id %s." % widget_id) + val = gconf_client.get_string(gconf_key) + if val == None or val == "": + col = g15convert.to_color(default_value) + else: + col = g15convert.to_color(g15convert.to_rgb(val)) + if default_alpha != None: + alpha = gconf_client.get_int(gconf_key + "_opacity") + widget.set_use_alpha(True) + widget.set_alpha(alpha << 8) + else: + widget.set_use_alpha(False) + widget.set_color(col) + handler_id = widget.connect("color-set", color_changed, gconf_client, gconf_key) + return handler_id + +def color_changed(widget, gconf_client, key): + val = widget.get_color() + gconf_client.set_string(key, "%d,%d,%d" % ( val.red >> 8, val.green >> 8, val.blue >> 8 )) + if widget.get_use_alpha(): + gconf_client.set_int(key + "_opacity", widget.get_alpha() >> 8) + +def configure_spinner_from_gconf(gconf_client, gconf_key, widget_id, default_value, widget_tree, decimal = False): + """ + Sets the value of a spinner from a gconf key and initializes an event to + store the spinner value in the same gconf key when changed by the user. + + gconf_client: Instance of GConfClient to communicate with GConf + gconf_key: Name of the key having the value + widget_id: Id of the widget + default_value: Default value to set if it isn't set in GConf + widget_tree: Widget tree containing the widget to set up + decimal: If True, then the spinner value is a float else an int + """ + widget = widget_tree.get_object(widget_id) + if widget == None: + raise Exception("No widget with id %s." % widget_id) + model = widget.get_adjustment() + entry = gconf_client.get(gconf_key) + val = default_value + if entry != None: + if decimal: + val = entry.get_float() + else: + val = entry.get_int() + model.set_value(val) + handler_id = widget.connect("value-changed", spinner_changed, gconf_client, gconf_key, model) + return handler_id + + +def spinner_changed(widget, gconf_client, key, model, decimal = False): + if decimal: + gconf_client.set_float(key, widget.get_value()) + else: + gconf_client.set_int(key, int(widget.get_value())) + +def configure_combo_from_gconf(gconf_client, gconf_key, widget_id, default_value, widget_tree): + """ + Selects an item of a combobox from a gconf key and initializes an event to + store the current selected item value in the same gconf key when changed by + the user. + + gconf_client: Instance of GConfClient to communicate with GConf + gconf_key: Name of the key having the value + widget_id: Id of the widget + default_value: Default value to set if it isn't set in GConf. + This value can either be an int or a string. + When an int, it represents the index of the combobox item to select. + When a string, it represents the value that must be selected. + widget_tree: Widget tree containing the widget to set up + """ + widget = widget_tree.get_object(widget_id) + if widget == None: + raise Exception("No widget with id %s." % widget_id) + model = widget.get_model() + handler_id = widget.connect("changed", combo_box_changed, gconf_client, gconf_key, model, default_value) + + if isinstance(default_value, int): + e = gconf_client.get(gconf_key) + if e: + val = e.get_int() + else: + val = default_value + else: + val = gconf_client.get_string(gconf_key) + if val == None or val == "": + val = default_value + idx = 0 + for row in model: + if isinstance(default_value, int): + row_val = int(row[0]) + else: + row_val = str(row[0]) + if row_val == val: + widget.set_active(idx) + idx += 1 + + return handler_id + +def combo_box_changed(widget, gconf_client, key, model, default_value): + if isinstance(default_value, int): + gconf_client.set_int(key, int(model[widget.get_active()][0])) + else: + gconf_client.set_string(key, model[widget.get_active()][0]) + +def configure_checkbox_from_gconf(gconf_client, gconf_key, widget_id, default_value, widget_tree, watch_changes = False): + """ + Sets the state of a checkbox from a gconf key and initializes an event to + store the current state in the same gconf key when changed by the user. + + gconf_client: Instance of GConfClient to communicate with GConf + gconf_key: Name of the key having the value + widget_id: Id of the widget + default_value: Default value to set if it isn't set in GConf. + widget_tree: Widget tree containing the widget to set up + watch_changes: If True, then keeps updating the state of the checkbox when + the value of the gconf key changes. + """ + widget = widget_tree.get_object(widget_id) + entry = gconf_client.get(gconf_key) + connection_id = None + if entry != None: + widget.set_active(entry.get_bool()) + else: + widget.set_active(default_value) + handler_id = widget.connect("toggled", checkbox_changed, gconf_key, gconf_client) + if watch_changes: + connection_id = gconf_client.notify_add(gconf_key, boolean_conf_value_change,( widget, gconf_key )); + return (handler_id, connection_id) + +def boolean_conf_value_change(client, connection_id, entry, args): + widget, key = args + widget.set_active( entry.get_value().get_bool()) + +def checkbox_changed(widget, key, gconf_client): + gconf_client.set_bool(key, widget.get_active()) + +def configure_text_from_gconf(gconf_client, gconf_key, widget_id, default_value, widget_tree, watch_changes = False): + """ + Sets the text of a text entry widget from a gconf key and initializes an + event to store the text in the same gconf key when changed by the user. + + gconf_client: Instance of GConfClient to communicate with GConf + gconf_key: Name of the key having the value + widget_id: Id of the widget + default_value: Default value to set if it isn't set in GConf. + widget_tree: Widget tree containing the widget to set up + watch_changes: If True, then keeps updating the value of the text entry when + the value of the gconf key changes. + """ + widget = widget_tree.get_object(widget_id) + entry = gconf_client.get(gconf_key) + connection_id = None + if entry != None: + widget.set_text(entry.get_string()) + else: + widget.set_text(default_value) + handler_id = widget.connect("changed", text_changed, gconf_key, gconf_client) + if watch_changes: + connection_id = gconf_client.notify_add(gconf_key, text_conf_value_change,( widget, gconf_key )); + return (handler_id, connection_id) + +def text_conf_value_change(client, connection_id, entry, args): + widget, key = args + widget.set_text( entry.get_value().get_string()) + +def text_changed(widget, key, gconf_client): + gconf_client.set_string(key, widget.get_text()) + +def configure_radio_from_gconf(gconf_client, gconf_key, widget_ids, gconf_values, default_value, widget_tree, watch_changes = False): + """ + Sets the checked state of a set of radioboxes from a gconf key and initializes + an event to store their state in the same gconf key when changed by the user. + + gconf_client: Instance of GConfClient to communicate with GConf + gconf_key: Name of the key having the value + widget_ids: Ids of the widgets + gconf_values: The values that should be read from the gconf_key to activate the + radiobox + default_value: Default value to set if it isn't set in GConf. + widget_tree: Widget tree containing the widget to set up + watch_changes: If True, then keeps updating the value of the text entry when + the value of the gconf key changes. + """ + entry = gconf_client.get(gconf_key) + handler_ids = [] + connection_ids = [] + sel_entry = entry.get_string() if entry else None + for i in range(0, len(widget_ids)): + gconf_value = gconf_values[i] + active = ( entry != None and gconf_value == sel_entry ) or ( entry == None and default_value == gconf_value ) + widget_tree.get_object(widget_ids[i]).set_active(active) + + for i in range(0, len(widget_ids)): + widget = widget_tree.get_object(widget_ids[i]) + handler_ids.append(widget.connect("toggled", radio_changed, gconf_key, gconf_client, gconf_values[i])) + if watch_changes: + connection_ids.append(gconf_client.notify_add(gconf_key, radio_conf_value_change,( widget, gconf_key, gconf_values[i] ))) + else: + connection_ids.append(None) + return (handler_ids, connection_ids) + +def radio_conf_value_change(client, connection_id, entry, args): + widget, key, gconf_value = args + str_value = entry.get_value().get_string() + widget.set_active(str_value == gconf_value) + +def radio_changed(widget, key, gconf_client, gconf_value): + gconf_client.set_string(key, gconf_value) + +def configure_adjustment_from_gconf(gconf_client, gconf_key, widget_id, default_value, widget_tree): + """ + Sets the value of a adjustment from a gconf key and initializes an event to + store the value in the same gconf key when changed by the user. + + gconf_client: Instance of GConfClient to communicate with GConf + gconf_key: Name of the key having the value + widget_id: Id of the widget + default_value: Default value to set if it isn't set in GConf + widget_tree: Widget tree containing the widget to set up + """ + adj = widget_tree.get_object(widget_id) + entry = gconf_client.get(gconf_key) + if entry != None: + if isinstance(default_value, int): + adj.set_value(entry.get_int()) + else: + adj.set_value(entry.get_float()) + else: + adj.set_value(default_value) + handler_id = adj.connect("value-changed", adjustment_changed, gconf_key, gconf_client, isinstance(default_value, int)) + return handler_id + +def adjustment_changed(adjustment, key, gconf_client, integer = True): + if integer: + gconf_client.set_int(key, int(adjustment.get_value())) + else: + gconf_client.set_float(key, adjustment.get_value()) + diff --git a/src/gnome15/util/jobqueue.py b/src/gnome15/util/jobqueue.py new file mode 100644 index 0000000..6128df4 --- /dev/null +++ b/src/gnome15/util/jobqueue.py @@ -0,0 +1,287 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2010 Brett Smith +# +# 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 . + +import Queue +import threading +import traceback +import sys +import gobject +import time +from threading import RLock +from threading import local + +# Can be adjusted to speed up time to aid debugging. +TIME_FACTOR=1 + +# Logging +import logging +logger = logging.getLogger(__name__) + +# Thread local to allow threads to detect what queue they are on +queue_names = local() + +def get_current_queue(): + if hasattr(queue_names, 'queue_name'): + return queue_names.queue_name + return "None" + +def is_on_queue(queue_name): + """ + Get if the current thread came from the queue with the specified name + + Keyword arguments: + queue_name -- queue name + """ + if hasattr(queue_names, 'queue_name') and queue_names.queue_name == queue_name: + return True + return False + +class GTimer: + def __init__(self, scheduler, task_queue, task_name, interval, function, stack, *args): + self.function = function + if function == None: + logger.warning("Attempt to run empty job %s on %s", task_name, task_queue.name) + traceback.print_stack() + return + self.stack = stack + self.scheduler = scheduler + self.task_queue = task_queue + self.task_name = task_name + self.source = gobject.timeout_add(int(float(interval) * 1000.0 * TIME_FACTOR), self.exec_item, function, *args) + self.complete = False + self.scheduler.all_jobs.append(self) + + def exec_item(self, function, *args): + try: + logger.debug("Executing GTimer %s", str(self.task_name)) + ji = self.task_queue.run(self.stack, function, *args) + logger.debug("Executed GTimer %s", str(self.task_name)) + finally: + self.scheduler.all_jobs_lock.acquire() + try: + if self in self.scheduler.all_jobs: + self.scheduler.all_jobs.remove(self) + self.complete = True + finally: + self.scheduler.all_jobs_lock.release() + # Destroy the timeout, don't execute this function again. + return False + + def is_complete(self): + return self.complete + + def cancel(self, *args): + self.scheduler.all_jobs_lock.acquire() + try: + if self in self.scheduler.all_jobs: + self.scheduler.all_jobs.remove(self) + # Check if callback function was executed, if yes this means that the timeout + # was automatically destroyed since the callback function returns False. + # Avoid thousands of warnings from source_remove(). + if not self.is_complete(): + gobject.source_remove(self.source) + logger.debug("Cancelled GTimer %s", str(self.task_name)) + finally: + self.scheduler.all_jobs_lock.release() + +''' +Task scheduler. Tasks may be added to the queue to execute +after a specified interval. The timer is done by the gobject +event loop, which then executes the job on a different thread +''' + +class JobScheduler(): + + def __init__(self): + self.queues = {} + self.all_jobs = [] + self.all_jobs_lock = RLock() + + def print_all_jobs(self): + print "Scheduled" + print "------" + for j in self.all_jobs: + print " %s - %s" % ( j.task_name, str(j.function)) + print + print "Running" + print "-------" + for q in self.queues: + self.queues[q].print_all_jobs() + + def schedule(self, name, interval, function, *args): + return self.queue("default", name, interval, function, *args) + + def stop_all(self): + logger.info("Stopping all queues") + for queue_name in self.queues: + self.queues[queue_name].stop() + + def clear_jobs(self, queue_name): + if queue_name in self.queues: + self.queues[queue_name].clear() + + def stop_queue(self, queue_name): + if queue_name in self.queues: + self.queues[queue_name].stop() + del self.queues[queue_name] + + def execute(self, queue_name, name, function, *args): + logger.debug("Executing on queue %s", queue_name) + if not queue_name in self.queues: + self.queues[queue_name] = JobQueue(name=queue_name) + self.queues[queue_name].run(self._get_stack(), function, *args) + + def _get_stack(self): + try: 1/0 + except: + tb = sys.exc_info()[2] + return traceback.extract_stack()[:-5] + + def queue(self, queue_name, name, interval, function, *args): + if not hasattr(function, "__call__"): + raise Exception("Not a function") + logger.debug("Queueing %s on %s for execution in %f", name, queue_name, interval) + if not queue_name in self.queues: + self.queues[queue_name] = JobQueue(name=queue_name) + + if interval == 0: + # Optimisation, if this is un-timed, avoid putting on main loop + self.queues[queue_name].run(self._get_stack(), function, *args) + else: + timer = GTimer(self, self.queues[queue_name], name, interval, function, self._get_stack(), *args) + logger.debug("Queued %s", name) + return timer + + +class JobQueue(): + + class JobItem(): + def __init__(self, stack, item, args = None): + self.args = args + self.item = item + self.queued = time.time() + self.started = None + self.finished = None + self.stack = stack + + def __init__(self,number_of_workers=1, name="JobQueue"): + logger.debug("Creating job queue %s with %d workers", name, number_of_workers) + self.work_queue = Queue.Queue() + self.queued_jobs = [] + self.name = name + self.stopping = False + self.all_jobs_lock = threading.Lock() + self.number_of_workers = number_of_workers + self.threads = [] + for __ in range(number_of_workers): + t = threading.Thread(target = self.worker) + t.name = name + t.setDaemon(True) + t.start() + self.threads.append(t) + + def print_all_jobs(self): + print "Queue %s" % self.name + for s in self.queued_jobs: + print " %s - %s" % (str(s.item), str(s.queued)) + + def stop(self): + logger.info("Stopping queue %s", self.name) + self.stopping = True + self.clear() + for i in range(0, self.number_of_workers): + self.work_queue.put(self.JobItem("Stopping", self._dummy)) + logger.info("Stopped queue %s", self.name) + + def _dummy(self): + pass + + def clear(self): + jobs = self.work_queue.qsize() + if jobs > 0: + logger.info("Clearing queue %s as it has %d jobs", self.name, jobs) + try : + while True: + item = self.work_queue.get_nowait() + logger.debug("Removed func = %s, args = %s, queued = %s, " \ + "started = %s, finished = %s", + str(item.item), + str(item.args), + str(item.queued), + str(item.started), + str(item.finished)) + if item in self.queued_jobs: + self.queued_jobs.remove(item) + except Queue.Empty as e: + logger.debug("The queue is already empty", exc_info = e) + pass + logger.info("Cleared queue %s", self.name) + + def run(self, stack, item, *args): + if self.stopping: + return + if item == None: + logger.warning("Attempt to run empty job.") + traceback.print_stack() + return + self.all_jobs_lock.acquire() + try : + logger.debug("Queued task on %s", self.name) + ji = self.JobItem(stack, item, args) + self.queued_jobs.append(ji) + self.work_queue.put(ji) + jobs = self.work_queue.qsize() + if jobs > 1: + logger.debug("Queue %s filling, now at %d jobs.", self.name, jobs) + + finally : + self.all_jobs_lock.release() + return ji + + def worker(self): + queue_names.queue_name = self.name + while not self.stopping: + item = self.work_queue.get() + try: + if item != None: + try: + logger.debug("Running task on %s", self.name) + item.started = time.time() + if item.args and len(item.args) > 0: + item.item(*item.args) + else: + item.item() + item.finished = time.time() + logger.debug("Ran task on %s", self.name) + finally: + if item in self.queued_jobs: + self.queued_jobs.remove(item) + except Exception as a: + try: + logger.debug("Error on worker", exc_info = a) + logger.debug("Caused by job") + logger.debug("%s\n", item.stack) + except Exception as e: + logger.debug("Could not log error on worker", exc_info = e) + pass + self.work_queue.task_done() + + if logger: + try: + logger.info("Exited queue %s", self.name) + except Exception as e: + pass + diff --git a/src/libimpulse/Impulse.c b/src/libimpulse/Impulse.c new file mode 100644 index 0000000..9b5e8cd --- /dev/null +++ b/src/libimpulse/Impulse.c @@ -0,0 +1,285 @@ +/* + * + *+ Copyright (c) 2009 Ian Halpern + *@ http://impulse.ian-halpern.com + * + * This file is part of Impulse. + * + * Impulse 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. + * + * Impulse 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 Impulse. If not, see . + */ + +#include +#include +#include +#include +#include + +#define CHUNK 1024 + +static const long fft_max[] = { 12317168L, 7693595L, 5863615L, 4082974L, 5836037L, 4550263L, 3377914L, 3085778L, 3636534L, 3751823L, 2660548L, 3313252L, 2698853L, 2186441L, 1697466L, 1960070L, 1286950L, 1252382L, 1313726L, 1140443L, 1345589L, 1269153L, 897605L, 900408L, 892528L, 587972L, 662925L, 668177L, 686784L, 656330L, 1580286L, 785491L, 761213L, 730185L, 851753L, 927848L, 891221L, 634291L, 833909L, 646617L, 804409L, 1015627L, 671714L, 813811L, 689614L, 727079L, 853936L, 819333L, 679111L, 730295L, 836287L, 1602396L, 990827L, 773609L, 733606L, 638993L, 604530L, 573002L, 634570L, 1015040L, 679452L, 672091L, 880370L, 1140558L, 1593324L, 686787L, 781368L, 605261L, 1190262L, 525205L, 393080L, 409546L, 436431L, 723744L, 765299L, 393927L, 322105L, 478074L, 458596L, 512763L, 381303L, 671156L, 1177206L, 476813L, 366285L, 436008L, 361763L, 252316L, 204433L, 291331L, 296950L, 329226L, 319209L, 258334L, 388701L, 543025L, 396709L, 296099L, 190213L, 167976L, 138928L, 116720L, 163538L, 331761L, 133932L, 187456L, 530630L, 131474L, 84888L, 82081L, 122379L, 82914L, 75510L, 62669L, 73492L, 68775L, 57121L, 94098L, 68262L, 68307L, 48801L, 46864L, 61480L, 46607L, 45974L, 45819L, 45306L, 45110L, 45175L, 44969L, 44615L, 44440L, 44066L, 43600L, 57117L, 43332L, 59980L, 55319L, 54385L, 81768L, 51165L, 54785L, 73248L, 52494L, 57252L, 61869L, 65900L, 75893L, 65152L, 108009L, 421578L, 152611L, 135307L, 254745L, 132834L, 169101L, 137571L, 141159L, 142151L, 211389L, 267869L, 367730L, 256726L, 185238L, 251197L, 204304L, 284443L, 258223L, 158730L, 228565L, 375950L, 294535L, 288708L, 351054L, 694353L, 477275L, 270576L, 426544L, 362456L, 441219L, 313264L, 300050L, 421051L, 414769L, 244296L, 292822L, 262203L, 418025L, 579471L, 418584L, 419449L, 405345L, 739170L, 488163L, 376361L, 339649L, 313814L, 430849L, 275287L, 382918L, 297214L, 286238L, 367684L, 303578L, 516246L, 654782L, 353370L, 417745L, 392892L, 418934L, 475608L, 284765L, 260639L, 288961L, 301438L, 301305L, 329190L, 252484L, 272364L, 261562L, 208419L, 203045L, 229716L, 191240L, 328251L, 267655L, 322116L, 509542L, 498288L, 341654L, 346341L, 451042L, 452194L, 467716L, 447635L, 644331L, 1231811L, 1181923L, 1043922L, 681166L, 1078456L, 1088757L, 1221378L, 1358397L, 1817252L, 1255182L, 1410357L, 2264454L, 1880361L, 1630934L, 1147988L, 1919954L, 1624734L, 1373554L, 1865118L, 2431931L }; + +static uint32_t source_index = 0; +static int context_ready = 0; +static int16_t buffer[ CHUNK / 2 ], snapshot[ CHUNK / 2 ]; +static size_t buffer_index = 0; + +static pa_context *context = NULL; +static pa_stream *stream = NULL; +static pa_threaded_mainloop* mainloop = NULL; +static pa_io_event* stdio_event = NULL; +static pa_mainloop_api *mainloop_api = NULL; +static char *stream_name = NULL, *client_name = NULL, *device = NULL; + +static pa_sample_spec sample_spec = { + .format = PA_SAMPLE_S16LE, + .rate = 44100, + .channels = 2 +}; + +static pa_stream_flags_t flags = 0; + +static pa_channel_map channel_map; +static int channel_map_set = 0; + +/* A shortcut for terminating the application */ +static void quit( int ret ) { + assert( mainloop_api ); + mainloop_api->quit( mainloop_api, ret ); +} + +static void unmute_source_success_cb( pa_context *c, int success, void *userdata ) { + printf("unmute: %d\n", success); +} + +static void get_source_info_callback( pa_context *c, const pa_source_info *i, int is_last, void *userdata ) { + + if ( !i ) + return; + + printf("source index: %u\n", i->index ); + + // snprintf(t, sizeof(t), "%u", i->monitor_of_sink); + +// if ( i->monitor_of_sink != PA_INVALID_INDEX ) { + puts( i->name ); + // if ( device && strcmp( device, i->name ) == 0 ) return; + + device = pa_xstrdup( i->name ); + + if ( ( pa_stream_connect_record( stream, device, NULL, flags ) ) < 0 ) { + fprintf(stderr, "pa_stream_connect_record() failed: %s\n", pa_strerror(pa_context_errno(c))); + quit(1); + } +// } +} + +/* This is called whenever new data is available */ +static void stream_read_callback(pa_stream *s, size_t length, void *userdata) { + const void *data; + assert(s); + assert(length > 0); +// printf("stream index: %d\n", pa_stream_get_index( s ) ); + if (stdio_event) + mainloop_api->io_enable(stdio_event, PA_IO_EVENT_OUTPUT); + + if (pa_stream_peek(s, &data, &length) < 0) { + fprintf(stderr, "pa_stream_peek() failed: %s\n", pa_strerror(pa_context_errno(context))); + quit(1); + return; + } + + assert(data); + assert(length > 0); + + int excess = buffer_index * 2 + length - ( CHUNK ); + + if ( excess < 0 ) excess = 0; + + memcpy( buffer + buffer_index, data, length - excess ); + buffer_index += ( length - excess ) / 2; + + if ( excess ) { + memcpy( snapshot, buffer, buffer_index * 2 ); + buffer_index = 0; + } + + pa_stream_drop(s); +} + +static void stream_state_callback( pa_stream *s, void* userdata ); + +static void init_source_stream_for_recording() { + + if (!(stream = pa_stream_new( context, stream_name, &sample_spec, channel_map_set ? &channel_map : NULL))) { + fprintf(stderr, "pa_stream_new() failed: %s\n", pa_strerror(pa_context_errno(context))); + quit(1); + } + + pa_stream_set_read_callback(stream, stream_read_callback, NULL); + pa_stream_set_state_callback( stream, stream_state_callback, NULL ); + pa_operation_unref( pa_context_set_source_mute_by_index( context, source_index, 0, unmute_source_success_cb, NULL ) ); + pa_operation_unref( pa_context_get_source_info_by_index( context, source_index, get_source_info_callback, NULL ) ); +} + +static void stream_state_callback( pa_stream *s, void* userdata ) { + if ( pa_stream_get_state( s ) == PA_STREAM_TERMINATED ) { + pa_stream_unref( stream ); + init_source_stream_for_recording(); + } +} + +static void context_state_callback( pa_context *c, void *userdata ) { + + switch (pa_context_get_state(c)) { + case PA_CONTEXT_CONNECTING: + case PA_CONTEXT_AUTHORIZING: + case PA_CONTEXT_SETTING_NAME: + break; + case PA_CONTEXT_READY: + assert(c); + assert(!stream); + + /*if (!(stream = pa_stream_new(c, stream_name, &sample_spec, channel_map_set ? &channel_map : NULL))) { + fprintf(stderr, "pa_stream_new() failed: %s\n", pa_strerror(pa_context_errno(c))); + quit(1); + } + + pa_stream_set_read_callback(stream, stream_read_callback, NULL);*/ + init_source_stream_for_recording(); + + break; + case PA_CONTEXT_TERMINATED: + quit(0); + break; + + case PA_CONTEXT_FAILED: + default: + fprintf(stderr, "Connection failure: %s\n", pa_strerror(pa_context_errno(c))); + quit(1); + } +} + +void im_stop (void) { + + pa_threaded_mainloop_stop( mainloop ); + + //printf( "exit\n" ); +} + +void im_setSourceIndex( uint32_t index ) { + source_index = index; + if ( !stream ) return; + + if ( pa_stream_get_state( stream ) != PA_STREAM_UNCONNECTED ) + pa_stream_disconnect( stream ); + else + init_source_stream_for_recording(); +} + +double *im_getSnapshot( int fft ) { + + static double magnitude[ CHUNK / 4 ]; + + if ( ! fft ) { + int i; + for ( i = 0; i < CHUNK / 2; i += sample_spec.channels ) { + magnitude[ i / sample_spec.channels ] = 0; + int j; + for ( j = 0; j < sample_spec.channels; j++ ) + magnitude[ i / sample_spec.channels ] += fabs( ( (double) snapshot[ i + j ] / ( pow( 2, 16 ) / 2 ) ) / sample_spec.channels ); + } + } else { + + double *in; + fftw_complex *out; + fftw_plan p; + + in = (double*) malloc( sizeof( double ) * ( CHUNK / 2 ) ); + out = (fftw_complex*) fftw_malloc( sizeof( fftw_complex ) * ( CHUNK / 2 ) ); + + if ( snapshot != NULL ) { + int i; + for ( i = 0; i < CHUNK / 2; i++ ) { + in[ i ] = (double) snapshot[ i ]; + } + } + + p = fftw_plan_dft_r2c_1d( CHUNK / 2, in, out, 0 ); + + fftw_execute( p ); + + fftw_destroy_plan( p ); + + if ( out != NULL ) { + int i; + for ( i = 0; i < CHUNK / 2 / sample_spec.channels; i++ ) { + magnitude[ i ] = (double) sqrt( pow( out[ i ][ 0 ], 2 ) + pow( out[ i ][ 1 ], 2 ) ) / (double) fft_max[ i ]; + if ( magnitude[ i ] > 1.0 ) magnitude[ i ] = 1.0; + } + } + + free( in ); + fftw_free(out); + } + + return magnitude; // PyString_FromStringAndSize( (char *) snapshot, CHUNK ); +} + + +void im_start ( void ) { + + // Pulseaudio + int r; + char *server = NULL; + + client_name = pa_xstrdup( "impulse" ); + stream_name = pa_xstrdup( "impulse" ); + + // Set up a new main loop + + if ( ! ( mainloop = pa_threaded_mainloop_new( ) ) ) { + fprintf( stderr, "pa_mainloop_new() failed.\n" ); + return; + } + + mainloop_api = pa_threaded_mainloop_get_api( mainloop ); + + r = pa_signal_init( mainloop_api ); + assert( r == 0 ); + + /*if (!(stdio_event = mainloop_api->io_new(mainloop_api, + STDOUT_FILENO, + PA_IO_EVENT_OUTPUT, + stdout_callback, NULL))) { + fprintf(stderr, "io_new() failed.\n"); + goto quit; + }*/ + + // create a new connection context + if ( ! ( context = pa_context_new( mainloop_api, client_name ) ) ) { + fprintf( stderr, "pa_context_new() failed.\n" ); + return; + } + + pa_context_set_state_callback( context, context_state_callback, NULL ); + + /* Connect the context */ + pa_context_connect( context, server, 0, NULL ); + + // pulseaudio thread + pa_threaded_mainloop_start( mainloop ); + + return; +} + diff --git a/src/libimpulse/Impulse.h b/src/libimpulse/Impulse.h new file mode 100644 index 0000000..ab5979b --- /dev/null +++ b/src/libimpulse/Impulse.h @@ -0,0 +1,32 @@ +/* + * + *+ Copyright (c) 2009 Ian Halpern + *@ http://impulse.ian-halpern.com + * + * This file is part of Impulse. + * + * Impulse 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. + * + * Impulse 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 Impulse. If not, see . + */ + + +#define IM_NOFFT 0 +#define IM_FFT 1 + +double *im_getSnapshot( int fft ); + +void im_setSourceIndex( uint32_t index ); + +void im_start( void ); + +void im_stop( void ); diff --git a/src/libimpulse/Makefile.am b/src/libimpulse/Makefile.am new file mode 100644 index 0000000..caec52d --- /dev/null +++ b/src/libimpulse/Makefile.am @@ -0,0 +1,14 @@ +lib_LTLIBRARIES = libimpulse.la +libimpulse_la_SOURCES = Impulse.c +libimpulse_la_CFLAGS = ${PULSE_CFLAGS} ${FFTW_CFLAGS} +libimpulse_la_LDFLAGS = -version-info 1:0:1 -no-undefined -pthread -shared -Wl ${PULSE_LIBS} ${FFTW_LIBS} -fPIC + +pyexec_LTLIBRARIES = impulse.la +impulse_la_SOURCES = impulsemodule.c +impulse_la_CFLAGS = $(PYTHON_CPPFLAGS) +impulse_la_LDFLAGS = -avoid-version -module $(PYTHON_LDFLAGS) +impulse_la_LIBADD = libimpulse.la + +pkginclude_HEADERS = Impulse.h + +METASOURCES = AUTO diff --git a/src/libimpulse/impulsemodule.c b/src/libimpulse/impulsemodule.c new file mode 100644 index 0000000..6eb032a --- /dev/null +++ b/src/libimpulse/impulsemodule.c @@ -0,0 +1,88 @@ +/* + * + *+ Copyright (c) 2009 Ian Halpern + *@ http://impulse.ian-halpern.com + * + * This file is part of Impulse. + * + * Impulse 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. + * + * Impulse 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 Impulse. If not, see . + */ + +#include +#include "Impulse.h" +#include +#include +#include +#include + +static PyObject * impulse_getSnapshot( PyObject *self, PyObject *args, PyObject *kwargs ) { + PyObject *magnitude; + + int fft = 0; + + static char *kwlist[] = { "fft", NULL }; + + if ( !PyArg_ParseTupleAndKeywords( args, kwargs, "b", kwlist, &fft ) ) + return NULL; + + magnitude = PyTuple_New( 256 ); + + double *m = im_getSnapshot( fft ); + + int i; + for ( i = 0; i < 256; i++ ) + PyTuple_SetItem( magnitude, i, PyFloat_FromDouble( m[ i ] ) ); + + return magnitude; // PyString_FromStringAndSize( (char *) snapshot, CHUNK ); +} + +static PyObject* impulse_setSourceIndex( PyObject* self, PyObject* args, PyObject* kwargs ) { + uint32_t index; + + static char *kwlist[] = { "index", NULL }; + + if ( !PyArg_ParseTupleAndKeywords( args, kwargs, "i", kwlist, &index ) ) + return NULL; + + im_setSourceIndex( index ); + + Py_RETURN_NONE; +} + +static PyObject *ImpulseError; + +static PyMethodDef ImpulseMethods[ ] = { + { "getSnapshot", (PyCFunction)impulse_getSnapshot, METH_VARARGS | METH_KEYWORDS, "Returns the current audio snapshot from Pulseaudio." }, + { "setSourceIndex", (PyCFunction)impulse_setSourceIndex, METH_VARARGS | METH_KEYWORDS, "Changes the Pulseaudio source by it's index." }, + { NULL } /* Sentinel */ +}; + +PyMODINIT_FUNC initimpulse ( void ) { + PyObject *m; + + m = Py_InitModule( "impulse", ImpulseMethods ); + if (m == NULL) + return; + + ImpulseError = PyErr_NewException( "impulse.error", NULL, NULL ); + Py_INCREF( ImpulseError ); + PyModule_AddObject( m, "error", ImpulseError ); + + Py_AtExit( &im_stop ); + + im_start( ); + + return; +} + diff --git a/src/libimpulse/test-libimpulse.c b/src/libimpulse/test-libimpulse.c new file mode 100644 index 0000000..1debec6 --- /dev/null +++ b/src/libimpulse/test-libimpulse.c @@ -0,0 +1,42 @@ +/* + * + *+ Copyright (c) 2009 Ian Halpern + *@ http://impulse.ian-halpern.com + * + * This file is part of Impulse. + * + * Impulse 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. + * + * Impulse 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 Impulse. If not, see . + */ + + +#include "Impulse.h" +#include + +int main( ) { + + im_start( ); + + while ( 1 ) { + usleep( 1000000 / 30 ); + double *array = im_getSnapshot( IM_FFT ); + int i; + for ( i = 0; i < 256; i+=32 ) + printf( " %.2f", array[ i ] ); + printf( "\n" ); + fflush( stdout ); + } + im_stop( ); + + return 0; +} diff --git a/src/plugins/Makefile.am b/src/plugins/Makefile.am new file mode 100644 index 0000000..ef48dbe --- /dev/null +++ b/src/plugins/Makefile.am @@ -0,0 +1,163 @@ +if ENABLE_PLUGIN_VOLUME + MAYBE_VOLUME = volume +endif +if ENABLE_PLUGIN_RSS + MAYBE_RSS = rss +endif +if ENABLE_PLUGIN_SYSMON + MAYBE_SYSMON = sysmon +endif +if ENABLE_PLUGIN_PROCESSES + MAYBE_PROCESSES = processes +endif +if ENABLE_PLUGIN_CAL + MAYBE_CAL = cal +endif +if ENABLE_PLUGIN_CAL_EVOLUTION + MAYBE_CAL_EVOLUTION = cal-evolution +endif +if ENABLE_PLUGIN_CAL_GOOGLE + MAYBE_CAL_GOOGLE = cal-google +endif +if ENABLE_PLUGIN_LCDBIFF + MAYBE_LCDBIFF = lcdbiff +endif +if ENABLE_PLUGIN_BACKGROUND + MAYBE_BACKGROUND = background +endif +if ENABLE_PLUGIN_CAIRO_CLOCK + MAYBE_CAIRO_CLOCK = cairo-clock +endif +if ENABLE_PLUGIN_CLOCK + MAYBE_CLOCK = clock +endif +if ENABLE_PLUGIN_FX + MAYBE_FX = fx +endif +if ENABLE_PLUGIN_MACRO_RECORDER + MAYBE_MACRO_RECORDER = macro-recorder +endif +if ENABLE_PLUGIN_MACROS + MAYBE_MACROS = macros +endif +if ENABLE_PLUGIN_PROFILES + MAYBE_PROFILES = profiles +endif +if ENABLE_PLUGIN_MOUNTS + MAYBE_MOUNTS = mounts +endif +if ENABLE_PLUGIN_NOTIFY_LCD + MAYBE_NOTIFY_LCD = notify-lcd +endif +if ENABLE_PLUGIN_IM + MAYBE_IM = im +endif +if ENABLE_PLUGIN_WEATHER + MAYBE_WEATHER = weather +endif +if ENABLE_PLUGIN_WEATHER_NOAA + MAYBE_WEATHER_NOAA = weather-noaa +endif +if ENABLE_PLUGIN_WEATHER_YAHOO + MAYBE_WEATHER_YAHOO = weather-yahoo +endif +if ENABLE_PLUGIN_MPRIS + MAYBE_MPRIS = mpris +endif +if ENABLE_PLUGIN_MENU + MAYBE_MENU = menu +endif +if ENABLE_PLUGIN_PANEL + MAYBE_PANEL = panel +endif +if ENABLE_PLUGIN_MEDIAPLAYER + MAYBE_MEDIAPLAYER = mediaplayer +endif +if ENABLE_PLUGIN_G15DAEMON_SERVER + MAYBE_G15DAEMON_SERVER = g15daemon-server +endif +if ENABLE_PLUGIN_STOPWATCH + MAYBE_STOPWATCH = stopwatch +endif +if ENABLE_PLUGIN_SCREENSAVER + MAYBE_SCREENSAVER = screensaver +endif +if ENABLE_PLUGIN_INDICATOR_MESSAGES + MAYBE_INDICATOR_MESSAGES = indicator-messages +endif +if ENABLE_PLUGIN_SENSE + MAYBE_SENSE = sense +endif +if ENABLE_PLUGIN_LCDSHOT + MAYBE_LCDSHOT = lcdshot +endif +if ENABLE_PLUGIN_TWEAK + MAYBE_TWEAK = tweak +endif +if ENABLE_PLUGIN_TAILS + MAYBE_TAILS = tails +endif +if ENABLE_PLUGIN_DISPLAY + MAYBE_DISPLAY = display +endif +if ENABLE_PLUGIN_VOIP + MAYBE_VOIP = voip +endif +if ENABLE_PLUGIN_VOIP_TEAMSPEAK3 + MAYBE_VOIP_TEAMSPEAK3 = voip-teamspeak3 +endif +if ENABLE_PLUGIN_GOOGLE_ANALYTICS + MAYBE_GOOGLE_ANALYTICS = google-analytics +endif +if ENABLE_PLUGIN_DEBUG + MAYBE_DEBUG = debug +endif +if ENABLE_PLUGIN_TRAFFIC_STATS + MAYBE_TRAFFIC_STATS = trafficstats +endif +if ENABLE_PLUGIN_POMMODORO + MAYBE_POMMODORO = pommodoro +endif +if ENABLE_PLUGIN_GAME_NEXUIZ + MAYBE_GAME_NEXUIZ = game-nexuiz +endif +if ENABLE_PLUGIN_BACKLIGHT + MAYBE_BACKLIGHT = backlight +endif +if ENABLE_PLUGIN_NOTIFY_LCD2 + MAYBE_NOTIFY_LCD2 = notify-lcd2 +endif +if ENABLE_PLUGIN_PPASTATS + MAYBE_PPASTATS = ppastats +endif +if ENABLE_PLUGIN_NM + MAYBE_NM = nm +endif +if ENABLE_PLUGIN_LENS + MAYBE_LENS = lens +endif +if ENABLE_PLUGIN_WEBKIT_BROWSER + MAYBE_WEBKIT_BROWSER = webkitbrowser +endif +if ENABLE_PLUGIN_THINGS + MAYBE_THINGS = things +endif +if ENABLE_PLUGIN_IMPULSE15 + MAYBE_IMPULSE15 = impulse15 +endif + + +SUBDIRS = runapp $(MAYBE_BACKGROUND) $(MAYBE_CAIRO_CLOCK) $(MAYBE_CLOCK) $(MAYBE_FX) $(MAYBE_MACRO_RECORDER) $(MAYBE_MACROS) $(MAYBE_MOUNTS) \ + $(MAYBE_NOTIFY_LCD) $(MAYBE_IM) $(MAYBE_WEATHER) $(MAYBE_MPRIS) $(MAYBE_MENU) $(MAYBE_PANEL) $(MAYBE_MEDIAPLAYER) $(MAYBE_G15DAEMON_SERVER) \ + $(MAYBE_STOPWATCH) $(MAYBE_SCREENSAVER) $(MAYBE_INDICATOR_MESSAGES) $(MAYBE_PROFILES) \ + $(MAYBE_SYSMON) $(MAYBE_VOLUME) $(MAYBE_RSS) $(MAYBE_PROCESSES) $(MAYBE_CAL) $(MAYBE_CAL_EVOLUTION) $(MAYBE_CAL_GOOGLE) $(MAYBE_LCDBIFF) $(MAYBE_SENSE) \ + $(MAYBE_LCDSHOT) $(MAYBE_TWEAK) $(MAYBE_TAILS) $(MAYBE_DISPLAY) $(MAYBE_VOIP) $(MAYBE_VOIP_TEAMSPEAK3) $(MAYBE_WEATHER_NOAA) \ + $(MAYBE_WEATHER_YAHOO) $(MAYBE_GOOGLE_ANALYTICS) $(MAYBE_DEBUG) $(MAYBE_TRAFFIC_STATS) $(MAYBE_POMMODORO) $(MAYBE_GAME_NEXUIZ) \ + $(MAYBE_BACKLIGHT) $(MAYBE_NOTIFY_LCD2) $(MAYBE_PPASTATS) $(MAYBE_NM) $(MAYBE_LENS) \ + $(MAYBE_WEBKIT_BROWSER) $(MAYBE_THINGS) $(MAYBE_IMPULSE15) + +DIST_SUBDIRS = runapp background cairo-clock clock fx macro-recorder macros mounts notify-lcd im weather mpris menu panel mediaplayer g15daemon-server stopwatch \ + profiles sysmon volume rss processes screensaver cal cal-evolution cal-google lcdbiff indicator-messages sense lcdshot tweak tails display \ + voip voip-teamspeak3 google-analytics weather-noaa weather-yahoo debug trafficstats pommodoro \ + game-nexuiz backlight notify-lcd2 ppastats nm lens webkitbrowser things impulse15 + diff --git a/src/plugins/background/Makefile.am b/src/plugins/background/Makefile.am new file mode 100644 index 0000000..dec1fdf --- /dev/null +++ b/src/plugins/background/Makefile.am @@ -0,0 +1,8 @@ +plugindir = $(datadir)/gnome15/plugins/background +plugin_DATA = background.ui \ + background.py \ + background-160x43.png \ + background-320x240.png + +EXTRA_DIST = \ + $(plugin_DATA) \ No newline at end of file diff --git a/src/plugins/background/background-160x43.png b/src/plugins/background/background-160x43.png new file mode 100644 index 0000000..e965e79 Binary files /dev/null and b/src/plugins/background/background-160x43.png differ diff --git a/src/plugins/background/background-320x240.png b/src/plugins/background/background-320x240.png new file mode 100644 index 0000000..f079f2c Binary files /dev/null and b/src/plugins/background/background-320x240.png differ diff --git a/src/plugins/background/background.py b/src/plugins/background/background.py new file mode 100644 index 0000000..2ca0acb --- /dev/null +++ b/src/plugins/background/background.py @@ -0,0 +1,317 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2010 Brett Smith +# +# 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 . + +import gnome15.g15locale as g15locale +_ = g15locale.get_translation("background", modfile = __file__).ugettext + +import gnome15.util.g15convert as g15convert +import gnome15.util.g15uigconf as g15uigconf +import gnome15.util.g15gconf as g15gconf +import gnome15.util.g15cairo as g15cairo +import gnome15.g15driver as g15driver +import gnome15.g15screen as g15screen +import gnome15.g15profile as g15profile +import gnome15.g15desktop as g15desktop +import cairo +import gtk +import os +import logging +import gconf +from lxml import etree +logger = logging.getLogger(__name__) + +# Plugin details - All of these must be provided +id="background" +name=_("Wallpaper") +description=_("Use an image for the LCD background") +author=_("Brett Smith ") +copyright="Copyright (C)2010 Brett Smith" +site="http://www.russo79.com/gnome15" +has_preferences=True +unsupported_models = [ g15driver.MODEL_G110, g15driver.MODEL_G11, g15driver.MODEL_G930, g15driver.MODEL_G35 ] + +def create(gconf_key, gconf_client, screen): + return G15Background(gconf_key, gconf_client, screen) + +def show_preferences(parent, driver, gconf_client, gconf_key): + G15BackgroundPreferences(parent, driver, gconf_client, gconf_key) + +class G15BackgroundPreferences(): + + def __init__(self, parent, driver, gconf_client, gconf_key): + + widget_tree = gtk.Builder() + widget_tree.add_from_file(os.path.join(os.path.dirname(__file__), "background.ui")) + + self.gconf_client = gconf_client + self.gconf_key = gconf_key + + # Widgets + dialog = widget_tree.get_object("BackgroundDialog") + dialog.set_transient_for(parent) + g15uigconf.configure_radio_from_gconf(gconf_client, gconf_key + "/type", [ "UseDesktop", "UseFile" ], [ "desktop", "file" ], "desktop", widget_tree, True) + g15uigconf.configure_combo_from_gconf(gconf_client, gconf_key + "/style", "StyleCombo", "zoom", widget_tree) + widget_tree.get_object("UseDesktop").connect("toggled", self.set_available, widget_tree) + widget_tree.get_object("UseFile").connect("toggled", self.set_available, widget_tree) + g15uigconf.configure_checkbox_from_gconf(gconf_client, gconf_key + "/allow_profile_override", "AllowProfileOverride", True, widget_tree) + g15uigconf.configure_adjustment_from_gconf(gconf_client, gconf_key + "/brightness", "BrightnessAdjustment", 50, widget_tree) + + # Currently, only GNOME is supported for getting the desktop background + if g15desktop.get_desktop() in [ "gnome", "gnome-shell" ]: + widget_tree.get_object("UseFile").set_active(True) + + # The file chooser + chooser = gtk.FileChooserDialog("Open..", + None, + gtk.FILE_CHOOSER_ACTION_OPEN, + (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, + gtk.STOCK_OPEN, gtk.RESPONSE_OK)) + chooser.set_default_response(gtk.RESPONSE_OK) + + filter = gtk.FileFilter() + filter.set_name("Images") + filter.add_mime_type("image/png") + filter.add_mime_type("image/jpeg") + filter.add_mime_type("image/gif") + filter.add_pattern("*.png") + filter.add_pattern("*.jpg") + filter.add_pattern("*.jpeg") + filter.add_pattern("*.gif") + chooser.add_filter(filter) + + filter = gtk.FileFilter() + filter.set_name("All files") + filter.add_pattern("*") + chooser.add_filter(filter) + + chooser_button = widget_tree.get_object("FileChooserButton") + chooser_button.dialog = chooser + chooser_button.connect("file-set", self.file_set) + widget_tree.connect_signals(self) + bg_img = gconf_client.get_string(gconf_key + "/path") + if bg_img == None: + bg_img = "" + chooser_button.set_filename(bg_img) + self.set_available(None, widget_tree) + dialog.run() + dialog.hide() + + def set_available(self, widget, widget_tree): + widget_tree.get_object("FileChooserLabel").set_sensitive(widget_tree.get_object("UseFile").get_active()) + widget_tree.get_object("FileChooserButton").set_sensitive(widget_tree.get_object("UseFile").get_active()) + + def file_set(self, widget): + self.gconf_client.set_string(self.gconf_key + "/path", widget.get_filename()) + + +class G15BackgroundPainter(g15screen.Painter): + + def __init__(self, screen): + g15screen.Painter.__init__(self, g15screen.BACKGROUND_PAINTER, -9999) + self.background_image = None + self.brightness = 0 + self._screen = screen + + def paint(self, canvas): + if self.background_image != None: + canvas.set_source_surface(self.background_image, 0.0, 0.0) + canvas.paint() + if self.brightness > 0: + canvas.set_source_rgba(1.0, 1.0, 1.0, ( self.brightness / 100.0 )) + else: + canvas.set_source_rgba(0.0, 0.0, 0.0, ( abs(self.brightness) / 100.0 )) + size = self._screen.device.lcd_size + canvas.rectangle(0,0,size[0],size[1]) + canvas.fill() + +class G15Background(): + + def __init__(self, gconf_key, gconf_client, screen): + self.screen = screen + self.gconf_client = gconf_client + self.gconf_key = gconf_key + self.target_surface = None + self.target_context = None + self.gconf_client.add_dir('/desktop/gnome/background', gconf.CLIENT_PRELOAD_NONE) + + def activate(self): + self.bg_img = None + self.this_image = None + self.current_style = None + self.notify_handlers = [] + self.painter = G15BackgroundPainter(self.screen) + self.screen.painters.append(self.painter) + self.notify_handlers.append(self.gconf_client.notify_add(self.gconf_key + "/path", self.config_changed)) + self.notify_handlers.append(self.gconf_client.notify_add(self.gconf_key + "/type", self.config_changed)) + self.notify_handlers.append(self.gconf_client.notify_add(self.gconf_key + "/style", self.config_changed)) + self.notify_handlers.append(self.gconf_client.notify_add(self.gconf_key + "/brightness", self.config_changed)) + self.notify_handlers.append(self.gconf_client.notify_add("/apps/gnome15/%s/active_profile" % self.screen.device.uid, self._active_profile_changed)) + self.gnome_dconf_settings = None + self.gnome_dconf_handle = None + + # Monitor desktop specific configuration for wallpaper changes + if g15desktop.get_desktop() in [ "gnome", "gnome-shell" ]: + self.notify_handlers.append(self.gconf_client.notify_add("/desktop/gnome/background/picture_filename", self.config_changed)) + if os.path.exists("/usr/share/glib-2.0/schemas/org.gnome.desktop.background.gschema.xml"): + try: + from gi.repository import Gio + self.gnome_dconf_settings = Gio.Settings.new("org.gnome.desktop.background") + except Exception as e: + logger.debug("Could not get background with GI, falling back", exc_info = e) + # Work around on Ubuntu 12.10+ until Gnome15 is converted to GObject bindings + import gnome15.g15dconf as g15dconf + self.gnome_dconf_settings = g15dconf.GSettings("org.gnome.desktop.background") + + if self.gnome_dconf_settings is not None: + self.gnome_dconf_handle = self.gnome_dconf_settings.connect("changed::picture_uri", self._do_config_changed) + + # Listen for profile changes + g15profile.profile_listeners.append(self._profiles_changed) + + self._do_config_changed() + + def deactivate(self): + g15profile.profile_listeners.remove(self._profiles_changed) + self.screen.painters.remove(self.painter) + for h in self.notify_handlers: + self.gconf_client.notify_remove(h); + self.screen.redraw() + if self.gnome_dconf_handle is not None: + self.gnome_dconf_settings.disconnect(self.gnome_dconf_handle) + self.gnome_dconf_settings.__del__() + + def config_changed(self, client, connection_id, entry, args): + self._do_config_changed() + + def destroy(self): + pass + + ''' + Private + ''' + def _active_profile_changed(self, client, connection_id, entry, args): + self._do_config_changed() + + def _profiles_changed(self, profile_id, device_uid): + self._do_config_changed() + + def _do_config_changed(self): + # Get the configuration + screen_size = self.screen.size + self.bg_img = None + bg_type = self.gconf_client.get_string(self.gconf_key + "/type") + if bg_type == None: + bg_type = "desktop" + bg_style = self.gconf_client.get_string(self.gconf_key + "/style") + if bg_style == None: + bg_style = "zoom" + allow_profile_override = g15gconf.get_bool_or_default(self.gconf_client, self.gconf_key + "/allow_profile_override", True) + + # See if the current profile has a background + if allow_profile_override: + active_profile = g15profile.get_active_profile(self.screen.device) + if active_profile is not None and active_profile.background is not None and active_profile.background != "": + self.bg_img = active_profile.background + + if self.bg_img == None and bg_type == "desktop": + # Get the current background the desktop is using if possible + desktop_env = g15desktop.get_desktop() + if desktop_env in [ "gnome", "gnome-shell" ]: + if self.gnome_dconf_settings is not None: + self.bg_img = self.gnome_dconf_settings.get_string("picture-uri") + else: + self.bg_img = self.gconf_client.get_string("/desktop/gnome/background/picture_filename") + else: + logger.warning("User request wallpaper from the desktop, but the desktop environment is unknown. Please report this bug to the Gnome15 project") + + if self.bg_img == None: + # Use the file + self.bg_img = self.gconf_client.get_string(self.gconf_key + "/path") + + # Fallback to the default provided image + if self.bg_img == None: + self.bg_img = os.path.join(os.path.dirname(__file__), "background-%dx%d.png" % ( screen_size[0], screen_size[1] ) ) + + # Load the image + if self.bg_img != self.this_image or bg_style != self.current_style: + self.this_image = self.bg_img + self.current_style = bg_style + if g15cairo.is_url(self.bg_img) or os.path.exists(self.bg_img): + + """ + TODO handle background themes and transitions from XML files properly + + For now, just get the first static image + """ + if self.bg_img.endswith(".xml"): + filet = etree.parse(self.bg_img).getroot().findtext('.//file') + if filet: + self.bg_img = filet + + img_surface = g15cairo.load_surface_from_file(self.bg_img) + if img_surface is not None: + sx = float(screen_size[0]) / img_surface.get_width() + sy = float(screen_size[1]) / img_surface.get_height() + surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, screen_size[0], screen_size[1]) + context = cairo.Context(surface) + context.save() + if bg_style == "zoom": + scale = max(sx, sy) + context.scale(scale, scale) + context.set_source_surface(img_surface) + context.paint() + elif bg_style == "stretch": + context.scale(sx, sy) + context.set_source_surface(img_surface) + context.paint() + elif bg_style == "scale": + x = ( screen_size[0] - img_surface.get_width() * sy ) / 2 + context.translate(x, 0) + context.scale(sy, sy) + context.set_source_surface(img_surface) + context.paint() + elif bg_style == "center": + x = ( screen_size[0] - img_surface.get_width() ) / 2 + y = ( screen_size[1] - img_surface.get_height() ) / 2 + context.translate(x, y) + context.set_source_surface(img_surface) + context.paint() + elif bg_style == "tile": + context.set_source_surface(img_surface) + context.paint() + y = 0 + x = img_surface.get_width() + while y < screen_size[1] + img_surface.get_height(): + if x >= screen_size[1] + img_surface.get_width(): + x = 0 + y += img_surface.get_height() + context.restore() + context.save() + context.translate(x, y) + context.set_source_surface(img_surface) + context.paint() + x += img_surface.get_width() + + context.restore() + self.painter.background_image = surface + else: + self.painter.background_image = None + else: + self.painter.background_image = None + + self.painter.brightness = self.gconf_client.get_int(self.gconf_key + "/brightness") + + self.screen.redraw() diff --git a/src/plugins/background/background.ui b/src/plugins/background/background.ui new file mode 100644 index 0000000..2a23037 --- /dev/null +++ b/src/plugins/background/background.ui @@ -0,0 +1,268 @@ + + + + + + -100 + 100 + 1 + 10 + + + + + + + + + + + + zoom + Zoom + + + tile + Tile + + + center + Center + + + scale + Scale + + + stretch + Stretch + + + + + 460 + False + 5 + Wallpaper Preferences + False + True + center-on-parent + dialog + + + True + False + 2 + + + True + False + end + + + gtk-close + True + True + True + True + + + False + False + 0 + + + + + False + True + end + 0 + + + + + True + False + 4 + + + _Same as desktop background + True + True + False + True + True + True + + + True + True + 0 + + + + + Use _image file + True + True + False + True + True + True + UseDesktop + + + True + True + 1 + + + + + True + False + 4 + + + 160 + True + False + 0 + 32 + Background Image + + + False + False + 0 + + + + + True + False + Select A Background + + + True + True + 1 + + + + + False + False + 2 + + + + + True + False + 8 + + + True + False + Style + + + False + True + 0 + + + + + True + False + StyleModel + + + + 1 + + + + + True + True + 1 + + + + + True + True + 8 + 3 + + + + + Allow macro profiles to override background + True + True + False + True + + + False + True + 4 + + + + + True + False + + + 160 + True + False + Brightness + + + False + False + 0 + + + + + True + True + BrightnessAdjustment + 1 + 0 + + + True + True + 1 + + + + + True + True + 5 + + + + + False + False + 1 + + + + + + button9 + + + diff --git a/src/plugins/background/background2.py b/src/plugins/background/background2.py new file mode 100644 index 0000000..88cded6 --- /dev/null +++ b/src/plugins/background/background2.py @@ -0,0 +1,284 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2010 Brett Smith +# +# 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 . + +import gnome15.g15locale as g15locale +_ = g15locale.get_translation("background", modfile = __file__).ugettext + +import gnome15.util.g15convert as g15convert +import gnome15.util.g15uigconf as g15uigconf +import gnome15.util.g15gconf as g15gconf +import gnome15.util.g15cairo as g15cairo +import gnome15.g15driver as g15driver +import gnome15.g15screen as g15screen +import gnome15.g15profile as g15profile +import gnome15.g15desktop as g15desktop +import cairo +import gtk +import os +import logging +import gconf +from lxml import etree +logger = logging.getLogger(__name__) + +# Plugin details - All of these must be provided +id="background" +name=_("Wallpaper") +description=_("Use an image for the LCD background") +author=_("Brett Smith ") +copyright="Copyright (C)2010 Brett Smith" +site="http://www.russo79.com/gnome15" +has_preferences=True +unsupported_models = [ g15driver.MODEL_G110, g15driver.MODEL_G11, g15driver.MODEL_G930, g15driver.MODEL_G35 ] + +def create(gconf_key, gconf_client, screen): + return G15Background(gconf_key, gconf_client, screen) + +def show_preferences(parent, driver, gconf_client, gconf_key): + G15BackgroundPreferences(parent, driver, gconf_client, gconf_key) + +class G15BackgroundPreferences(): + + def __init__(self, parent, driver, gconf_client, gconf_key): + + widget_tree = gtk.Builder() + widget_tree.add_from_file(os.path.join(os.path.dirname(__file__), "background.ui")) + + self.gconf_client = gconf_client + self.gconf_key = gconf_key + + # Widgets + dialog = widget_tree.get_object("BackgroundDialog") + dialog.set_transient_for(parent) + g15uigconf.configure_radio_from_gconf(gconf_client, gconf_key + "/type", [ "UseDesktop", "UseFile" ], [ "desktop", "file" ], "desktop", widget_tree, True) + g15uigconf.configure_combo_from_gconf(gconf_client, gconf_key + "/style", "StyleCombo", "zoom", widget_tree) + widget_tree.get_object("UseDesktop").connect("toggled", self.set_available, widget_tree) + widget_tree.get_object("UseFile").connect("toggled", self.set_available, widget_tree) + g15uigconf.configure_checkbox_from_gconf(gconf_client, gconf_key + "/allow_profile_override", "AllowProfileOverride", True, widget_tree) + g15uigconf.configure_adjustment_from_gconf(gconf_client, gconf_key + "/brightness", "BrightnessAdjustment", 50, widget_tree) + + # Currently, only GNOME is supported for getting the desktop background + if g15desktop.get_desktop() in [ "gnome", "gnome-shell" ]: + widget_tree.get_object("UseFile").set_active(True) + + # The file chooser + chooser = gtk.FileChooserDialog("Open..", + None, + gtk.FILE_CHOOSER_ACTION_OPEN, + (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, + gtk.STOCK_OPEN, gtk.RESPONSE_OK)) + chooser.set_default_response(gtk.RESPONSE_OK) + + filter = gtk.FileFilter() + filter.set_name("Images") + filter.add_mime_type("image/png") + filter.add_mime_type("image/jpeg") + filter.add_mime_type("image/gif") + filter.add_pattern("*.png") + filter.add_pattern("*.jpg") + filter.add_pattern("*.jpeg") + filter.add_pattern("*.gif") + chooser.add_filter(filter) + + filter = gtk.FileFilter() + filter.set_name("All files") + filter.add_pattern("*") + chooser.add_filter(filter) + + chooser_button = widget_tree.get_object("FileChooserButton") + chooser_button.dialog = chooser + chooser_button.connect("file-set", self.file_set) + widget_tree.connect_signals(self) + bg_img = gconf_client.get_string(gconf_key + "/path") + if bg_img == None: + bg_img = "" + chooser_button.set_filename(bg_img) + self.set_available(None, widget_tree) + dialog.run() + dialog.hide() + + def set_available(self, widget, widget_tree): + widget_tree.get_object("FileChooserLabel").set_sensitive(widget_tree.get_object("UseFile").get_active()) + widget_tree.get_object("FileChooserButton").set_sensitive(widget_tree.get_object("UseFile").get_active()) + + def file_set(self, widget): + self.gconf_client.set_string(self.gconf_key + "/path", widget.get_filename()) + + +class G15BackgroundPainter(g15screen.Painter): + + def __init__(self, screen): + g15screen.Painter.__init__(self, g15screen.BACKGROUND_PAINTER, -9999) + self.background_image = None + self.brightness = 0 + self._screen = screen + + def paint(self, canvas): + if self.background_image != None: + canvas.set_source_surface(self.background_image, 0.0, 0.0) + canvas.paint() + if self.brightness > 0: + canvas.set_source_rgba(1.0, 1.0, 1.0, ( self.brightness / 100.0 )) + else: + canvas.set_source_rgba(0.0, 0.0, 0.0, ( abs(self.brightness) / 100.0 )) + size = self._screen.device.lcd_size + canvas.rectangle(0,0,size[0],size[1]) + canvas.fill() + +class G15Background(): + + def __init__(self, gconf_key, gconf_client, screen): + self.screen = screen + self.gconf_client = gconf_client + self.gconf_key = gconf_key + self.target_surface = None + self.target_context = None + self.gconf_client.add_dir('/desktop/gnome/background', gconf.CLIENT_PRELOAD_NONE) + + def activate(self): + self.bg_img = None + self.this_image = None + self.current_style = None + self.notify_handlers = [] + self.painter = G15BackgroundPainter(self.screen) + self.screen.painters.append(self.painter) + self.notify_handlers.append(self.gconf_client.notify_add(self.gconf_key + "/path", self.config_changed)) + self.notify_handlers.append(self.gconf_client.notify_add(self.gconf_key + "/type", self.config_changed)) + self.notify_handlers.append(self.gconf_client.notify_add(self.gconf_key + "/style", self.config_changed)) + self.notify_handlers.append(self.gconf_client.notify_add(self.gconf_key + "/brightness", self.config_changed)) + self.notify_handlers.append(self.gconf_client.notify_add("/apps/gnome15/%s/active_profile" % self.screen.device.uid, self._active_profile_changed)) + self.gnome_dconf_settings = None + self.gnome_dconf_handle = None + + # Monitor desktop specific configuration for wallpaper changes + if g15desktop.get_desktop() in [ "gnome", "gnome-shell" ]: + self.notify_handlers.append(self.gconf_client.notify_add("/desktop/gnome/background/picture_filename", self.config_changed)) + if os.path.exists("/usr/share/glib-2.0/schemas/org.gnome.desktop.background.gschema.xml"): + try: + from gi.repository import Gio + self.gnome_dconf_settings = Gio.Settings.new("org.gnome.desktop.background") + except Exception as e: + logger.debug("Could not get background with GI, falling back", exc_info = e) + # Work around on Ubuntu 12.10+ until Gnome15 is converted to GObject bindings + import gnome15.g15dconf as g15dconf + self.gnome_dconf_settings = g15dconf.GSettings("org.gnome.desktop.background") + + if self.gnome_dconf_settings is not None: + self.gnome_dconf_handle = self.gnome_dconf_settings.connect("changed::picture_uri", self._do_config_changed) + + # Listen for profile changes + g15profile.profile_listeners.append(self._profiles_changed) + + self._do_config_changed() + + def deactivate(self): + g15profile.profile_listeners.remove(self._profiles_changed) + self.screen.painters.remove(self.painter) + for h in self.notify_handlers: + self.gconf_client.notify_remove(h); + self.screen.redraw() + if self.gnome_dconf_handle is not None: + self.gnome_dconf_settings.disconnect(self.gnome_dconf_handle) + self.gnome_dconf_settings.__del__() + + def config_changed(self, client, connection_id, entry, args): + self._do_config_changed() + + def destroy(self): + pass + + ''' + Private + ''' + def _active_profile_changed(self, client, connection_id, entry, args): + self._do_config_changed() + + def _profiles_changed(self, profile_id, device_uid): + self._do_config_changed() + + def _do_config_changed(self): + # Get the configuration + screen_size = self.screen.size + self.bg_img = None + bg_type = self.gconf_client.get_string(self.gconf_key + "/type") + if bg_type == None: + bg_type = "desktop" + bg_style = self.gconf_client.get_string(self.gconf_key + "/style") + if bg_style == None: + bg_style = "zoom" + allow_profile_override = g15gconf.get_bool_or_default(self.gconf_client, self.gconf_key + "/allow_profile_override", True) + + # See if the current profile has a background + if allow_profile_override: + active_profile = g15profile.get_active_profile(self.screen.device) + if active_profile is not None and active_profile.background is not None and active_profile.background != "": + self.bg_img = active_profile.background + + if self.bg_img == None and bg_type == "desktop": + # Get the current background the desktop is using if possible + desktop_env = g15desktop.get_desktop() + if desktop_env in [ "gnome", "gnome-shell" ]: + if self.gnome_dconf_settings is not None: + self.bg_img = self.gnome_dconf_settings.get_string("picture-uri") + else: + self.bg_img = self.gconf_client.get_string("/desktop/gnome/background/picture_filename") + else: + logger.warning("User request wallpaper from the desktop, but the desktop environment is unknown. Please report this bug to the Gnome15 project") + + if self.bg_img == None: + # Use the file + self.bg_img = self.gconf_client.get_string(self.gconf_key + "/path") + + # Fallback to the default provided image + if self.bg_img == None: + self.bg_img = os.path.join(os.path.dirname(__file__), "background-%dx%d.png" % ( screen_size[0], screen_size[1] ) ) + + # Load the image + if self.bg_img != self.this_image or bg_style != self.current_style: + self.this_image = self.bg_img + self.current_style = bg_style + if g15cairo.is_url(self.bg_img) or os.path.exists(self.bg_img): + + """ + TODO handle background themes and transitions from XML files properly + + For now, just get the first static image + """ + + surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, screen_size[0], screen_size[1]) + context = cairo.Context(surface) + context.scale(screen_size[0], screen_size[1]) + context.set_line_width(0.04) + context.move_to(0.1, 0.5) + context.curve_to(0.4, 0.9, 0.6, 0.1, 0.9, 0.5) + context.stroke() + context.set_source_rgba(1, 0.2, 0.2, 0.6) + context.set_line_width(0.02) + context.move_to(0.1, 0.5) + context.line_to(0.4, 0.9) + context.move_to(0.6, 0.1) + context.line_to(0.9, 0.5) + context.stroke() + context.save() + context.paint() + context.restore() + self.painter.background_image = surface + + else: + self.painter.background_image = None + + self.painter.brightness = self.gconf_client.get_int(self.gconf_key + "/brightness") + + self.screen.redraw() diff --git a/src/plugins/background/i18n/background.en_GB.po b/src/plugins/background/i18n/background.en_GB.po new file mode 100644 index 0000000..5db1e79 --- /dev/null +++ b/src/plugins/background/i18n/background.en_GB.po @@ -0,0 +1,66 @@ +# English translations for gnome15-plugins package. +# Copyright (C) 2012 THE gnome15-plugins'S COPYRIGHT HOLDER +# This file is distributed under the same license as the gnome15-plugins package. +# Automatically generated, 2012. +# +msgid "" +msgstr "" +"Project-Id-Version: gnome15-plugins 0.8.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: 2012-01-08 11:23+0000\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: en_GB\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=ASCII\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: i18n/background.glade.h:1 +msgid "Allow macro profiles to override background" +msgstr "Allow macro profiles to override background" + +#: i18n/background.glade.h:2 +msgid "Background Image" +msgstr "Background Image" + +#: i18n/background.glade.h:3 +msgid "Center" +msgstr "Center" + +#: i18n/background.glade.h:4 +msgid "Scale" +msgstr "Scale" + +#: i18n/background.glade.h:5 +msgid "Select A Background" +msgstr "Select A Background" + +#: i18n/background.glade.h:6 +msgid "Stretch" +msgstr "Stretch" + +#: i18n/background.glade.h:7 +msgid "Style" +msgstr "Style" + +#: i18n/background.glade.h:8 +msgid "Tile" +msgstr "Tile" + +#: i18n/background.glade.h:9 +msgid "Use _image file" +msgstr "Use _image file" + +#: i18n/background.glade.h:10 +msgid "Wallpaper Preferences" +msgstr "Wallpaper Preferences" + +#: i18n/background.glade.h:11 +msgid "Zoom" +msgstr "Zoom" + +#: i18n/background.glade.h:12 +msgid "_Same as desktop background" +msgstr "_Same as desktop background" diff --git a/src/plugins/background/i18n/background.glade.h b/src/plugins/background/i18n/background.glade.h new file mode 100644 index 0000000..af34a46 --- /dev/null +++ b/src/plugins/background/i18n/background.glade.h @@ -0,0 +1,12 @@ +char *s = N_("Allow macro profiles to override background"); +char *s = N_("Background Image"); +char *s = N_("Center"); +char *s = N_("Scale"); +char *s = N_("Select A Background"); +char *s = N_("Stretch"); +char *s = N_("Style"); +char *s = N_("Tile"); +char *s = N_("Use _image file"); +char *s = N_("Wallpaper Preferences"); +char *s = N_("Zoom"); +char *s = N_("_Same as desktop background"); diff --git a/src/plugins/background/i18n/background.pot b/src/plugins/background/i18n/background.pot new file mode 100644 index 0000000..b945edf --- /dev/null +++ b/src/plugins/background/i18n/background.pot @@ -0,0 +1,66 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: i18n/background.glade.h:1 +msgid "Allow macro profiles to override background" +msgstr "" + +#: i18n/background.glade.h:2 +msgid "Background Image" +msgstr "" + +#: i18n/background.glade.h:3 +msgid "Center" +msgstr "" + +#: i18n/background.glade.h:4 +msgid "Scale" +msgstr "" + +#: i18n/background.glade.h:5 +msgid "Select A Background" +msgstr "" + +#: i18n/background.glade.h:6 +msgid "Stretch" +msgstr "" + +#: i18n/background.glade.h:7 +msgid "Style" +msgstr "" + +#: i18n/background.glade.h:8 +msgid "Tile" +msgstr "" + +#: i18n/background.glade.h:9 +msgid "Use _image file" +msgstr "" + +#: i18n/background.glade.h:10 +msgid "Wallpaper Preferences" +msgstr "" + +#: i18n/background.glade.h:11 +msgid "Zoom" +msgstr "" + +#: i18n/background.glade.h:12 +msgid "_Same as desktop background" +msgstr "" diff --git a/src/plugins/backlight/Makefile.am b/src/plugins/backlight/Makefile.am new file mode 100644 index 0000000..e1d220b --- /dev/null +++ b/src/plugins/backlight/Makefile.am @@ -0,0 +1,6 @@ +SUBDIRS = default +plugindir = $(datadir)/gnome15/plugins/backlight +plugin_DATA = backlight.py + +EXTRA_DIST = \ + $(plugin_DATA) diff --git a/src/plugins/backlight/backlight.py b/src/plugins/backlight/backlight.py new file mode 100644 index 0000000..d10bbf6 --- /dev/null +++ b/src/plugins/backlight/backlight.py @@ -0,0 +1,115 @@ +#!/usr/bin/env python + +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2010 Brett Smith +# +# 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 . + +import gnome15.g15theme as g15theme +import gnome15.g15screen as g15screen +import gnome15.g15driver as g15driver +import gnome15.util.g15icontools as g15icontools +import gnome15.g15gtk as g15gtk +import gtk +import gobject + +# Plugin details - All of these must be provided +id="backlight" +name="Backlight" +description="Set the keyboard backlight color using the LCD screen and menu keys. " + \ + "This plugin demonstrates the use of ordinary GTK widgets on the LCD." +author="Brett Smith " +copyright="Copyright (C)2010 Brett Smith" +site="http://www.gnome15.org/" +has_preferences=False +supported_models = [ g15driver.MODEL_G19 ] + +def create(gconf_key, gconf_client, screen): + return G15Backlight(gconf_client, gconf_key, screen) + +class G15Backlight(): + + def __init__(self, gconf_client, gconf_key, screen): + self.screen = screen + self.gconf_client = gconf_client + self.gconf_key = gconf_key + + def activate(self): + self.page = g15theme.G15Page(id, self.screen, theme_properties_callback = self._get_theme_properties, priority = g15screen.PRI_LOW, title = name, theme = g15theme.G15Theme(self), + originating_plugin = plugin) + self.window = g15gtk.G15OffscreenWindow("offscreenWindow") + self.page.add_child(self.window) + gobject.idle_add(self._create_offscreen_window) + + def deactivate(self): + if self.page != None: + self.screen.del_page(self.page) + self.page = None + + def destroy(self): + pass + + ''' + Private + ''' + + def _get_theme_properties(self): + backlight_control = self.screen.driver.get_control_for_hint(g15driver.HINT_DIMMABLE) + color = backlight_control.value + properties = { + "title" : "Set Backlight", + "icon" : g15icontools.get_icon_path("system-config-display"), + "r" : color[0], + "g" : color[1], + "b" : color[2] + } + return properties + + def _create_offscreen_window(self): + backlight_control = self.screen.driver.get_control_for_hint(g15driver.HINT_DIMMABLE) + color = backlight_control.value + + vbox = gtk.VBox() + adjustment = gtk.Adjustment(color[0], 0, 255, 1, 10, 10) + red = gtk.HScale(adjustment) + red.set_draw_value(False) + adjustment.connect("value-changed", self._value_changed, 0) + + vbox.add(red) + red.grab_focus() + adjustment = gtk.Adjustment(color[1], 0, 255, 1, 10, 10) + green = gtk.HScale(adjustment) + green.set_draw_value(False) + adjustment.connect("value-changed", self._value_changed, 1) + green.set_range(0, 255) + green.set_increments(1, 10) + vbox.add(green) + adjustment = gtk.Adjustment(color[2], 0, 255, 1, 10, 10) + blue = gtk.HScale(adjustment) + blue.set_draw_value(False) + adjustment.connect("value-changed", self._value_changed, 2) + blue.set_range(0, 255) + blue.set_increments(1, 10) + vbox.add(blue) + + self.window.set_content(vbox) + self.screen.add_page(self.page) + self.screen.redraw(self.page) + + def _value_changed(self, widget, octet): + backlight_control = self.screen.driver.get_control_for_hint(g15driver.HINT_DIMMABLE) + color = list(backlight_control.value) + color[octet] = int(widget.get_value()) + self.gconf_client.set_string("/apps/gnome15/" + backlight_control.id, "%d,%d,%d" % ( color[0],color[1],color[2])) + \ No newline at end of file diff --git a/src/plugins/backlight/default/Makefile.am b/src/plugins/backlight/default/Makefile.am new file mode 100644 index 0000000..65a6dcc --- /dev/null +++ b/src/plugins/backlight/default/Makefile.am @@ -0,0 +1,5 @@ +themedir = $(datadir)/gnome15/plugins/backlight/default +theme_DATA = g19.svg + +EXTRA_DIST = \ + $(theme_DATA) diff --git a/src/plugins/backlight/default/g19.svg b/src/plugins/backlight/default/g19.svg new file mode 100644 index 0000000..0203270 --- /dev/null +++ b/src/plugins/backlight/default/g19.svg @@ -0,0 +1,286 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + ${title} + + + + + ${r} + ${g} + ${b} + + diff --git a/src/plugins/cairo-clock/Makefile.am b/src/plugins/cairo-clock/Makefile.am new file mode 100644 index 0000000..c92155f --- /dev/null +++ b/src/plugins/cairo-clock/Makefile.am @@ -0,0 +1,8 @@ +SUBDIRS = g15 g19 mx5500 + +plugindir = $(datadir)/gnome15/plugins/cairo-clock +plugin_DATA = cairo-clock.ui \ + cairo-clock.py + +EXTRA_DIST = \ + $(plugin_DATA) \ No newline at end of file diff --git a/src/plugins/cairo-clock/cairo-clock.py b/src/plugins/cairo-clock/cairo-clock.py new file mode 100644 index 0000000..0b5da36 --- /dev/null +++ b/src/plugins/cairo-clock/cairo-clock.py @@ -0,0 +1,440 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2010 Brett Smith +# +# 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 . + +import gnome15.g15locale as g15locale +_ = g15locale.get_translation("cairo-clock", modfile = __file__).ugettext + +import gnome15.g15screen as g15screen +import gnome15.g15theme as g15theme +import gnome15.util.g15uigconf as g15uigconf +import gnome15.util.g15gconf as g15gconf +import gnome15.util.g15cairo as g15cairo +import gnome15.util.g15pythonlang as g15pythonlang +import gnome15.g15driver as g15driver +import gnome15.g15globals as g15globals +import gnome15.g15text as g15text +import gnome15.g15plugin as g15plugin +import datetime +from threading import Timer +import time +import gtk +import os +import sys +import cairo +import rsvg +import pango +import locale +import xdg.BaseDirectory + +# Plugin details - All of these must be provided +id="cairo-clock" +name=_("Cairo Clock") +description=_("Port of MacSlow's SVG clock to Gnome15. Standard cairo-clock \ +themes may be used on a G19, however, for all other models \ +you must use specially crafted themes (using GIF files instead of SVG). \ +One default theme for low resolution screens is provided.") +author="Brett Smith " +copyright=_("Copyright (C)2010 Brett Smith") +site="http://www.russo79.com/gnome15" +has_preferences=True +default_enabled=True +unsupported_models = [ g15driver.MODEL_G110, g15driver.MODEL_G11, g15driver.MODEL_G930, g15driver.MODEL_G35 ] + +def create(gconf_key, gconf_client, screen): + return G15CairoClock(gconf_key, gconf_client, screen) + +def changed(widget, key, gconf_client): + gconf_client.set_bool(key, widget.get_active()) + +def show_preferences(parent, driver, gconf_client, gconf_key): + G15CairoClockPreferences(parent, driver, gconf_key, gconf_client) + +def get_theme_dir(model_name, gconf_key, gconf_client, theme_name): + for dir in get_theme_dirs(model_name, gconf_key, gconf_client): + full_path = "%s/%s" % ( dir, theme_name) + if os.path.exists(full_path): + return full_path + +def get_theme_dirs(model_name, gconf_key, gconf_client): + dirs = [] + model_dir = "g15" + if model_name == g15driver.MODEL_G19: + model_dir = "g19" + elif model_name == g15driver.MODEL_MX5500: + model_dir = "mx5500" + dirs.append(os.path.join(os.path.dirname(__file__), model_dir)) + dirs.append(os.path.join(g15globals.user_data_dir, "cairo-clock", model_dir)) + theme_dir = gconf_client.get(gconf_key + "/theme_dir") + if theme_dir != None: + dirs.append(theme_dir.get_string()) + if model_name == g15driver.MODEL_G19: + dirs.append(os.path.join(xdg.BaseDirectory.xdg_data_home, "cairo-clock")) + dirs.append("/usr/share/cairo-clock/themes") + return dirs + +class G15CairoClockPreferences(): + + def __init__(self, parent, driver, gconf_key, gconf_client): + widget_tree = gtk.Builder() + widget_tree.add_from_file(os.path.join(os.path.dirname(__file__), "cairo-clock.ui")) + + dialog = widget_tree.get_object("ClockDialog") + dialog.set_transient_for(parent) + + g15uigconf.configure_checkbox_from_gconf(gconf_client, "%s/display_seconds" % gconf_key, "DisplaySecondsCheckbox", True, widget_tree) + g15uigconf.configure_checkbox_from_gconf(gconf_client, "%s/display_date" % gconf_key, "DisplayDateCheckbox", True, widget_tree) + g15uigconf.configure_checkbox_from_gconf(gconf_client, "%s/twenty_four_hour" % gconf_key, "TwentyFourHourCheckbox", True, widget_tree) + g15uigconf.configure_checkbox_from_gconf(gconf_client, "%s/display_digital_time" % gconf_key, "DisplayDigitalTimeCheckbox", True, widget_tree) + g15uigconf.configure_checkbox_from_gconf(gconf_client, "%s/display_year" % gconf_key, "DisplayYearCheckbox", True, widget_tree) + g15uigconf.configure_checkbox_from_gconf(gconf_client, "%s/second_sweep" % gconf_key, "SecondSweep", False, widget_tree) + g15uigconf.configure_checkbox_from_gconf(gconf_client, "%s/twenty_four_hour_digital" % gconf_key, "TwentyFourHourDigitalCheckbox", True, widget_tree) + + e = gconf_client.get(gconf_key + "/theme") + theme_name = "default" + if e != None: + theme_name = e.get_string() + theme_model = widget_tree.get_object("ThemeModel") + theme = widget_tree.get_object("ThemeCombo") + + def _theme_changed(widget, key): + gconf_client.set_string(key, theme_model[widget.get_active()][0]) + + theme.connect("changed", _theme_changed, gconf_key + "/theme") + + theme_dirs = get_theme_dirs(driver.get_model_name(), gconf_key, gconf_client) + themes = {} + for d in theme_dirs: + if os.path.exists(d): + for fname in os.listdir(d): + if os.path.isdir(os.path.join(d, fname)) and not fname in themes and ( driver.get_bpp() == 16 or fname == "default" ) : + theme_model.append([fname]) + themes[fname] = True + if fname == theme_name: + theme.set_active(len(theme_model) - 1) + + dialog.run() + dialog.hide() + + +class G15CairoClock(g15plugin.G15RefreshingPlugin): + + def __init__(self, gconf_key, gconf_client, screen): + g15plugin.G15RefreshingPlugin.__init__(self, gconf_client, gconf_key, screen, [ "cairo-clock", "clock", "gnome-panel-clock", "time", "xfce4-clock", "rclock" ], id, name) + self.revert_timer = None + self.display_date = False + self.display_seconds = False + self.only_refresh_when_visible = False + + def activate(self): + self._load_surfaces() + self.panel_text = g15text.new_text(self.screen) + self.time_text = g15text.new_text(self.screen) + g15plugin.G15RefreshingPlugin.activate(self) + self.watch(None, self.config_changed) + self.do_refresh() + + def create_theme(self): + # Painting is done using cairo and cairo-clock themes, no need for a theme + return None + + def config_changed(self, client, connection_id, entry, args): + self._load_surfaces() + self.screen.set_priority(self.page, g15screen.PRI_HIGH, revert_after = 3.0) + self.do_refresh() + + def refresh(self): + pass + + def get_next_tick(self): + now = datetime.datetime.now() + + if self.second_sweep: + next_tick = now + datetime.timedelta(0, 0.1) + elif self.display_seconds: + next_tick = now + datetime.timedelta(0, 1.0) + next_tick = datetime.datetime(next_tick.year,next_tick.month,next_tick.day,next_tick.hour, next_tick.minute, int(next_tick.second)) + else: + next_tick = now + datetime.timedelta(0, 60.0) + next_tick = datetime.datetime(next_tick.year,next_tick.month,next_tick.day,next_tick.hour, next_tick.minute, 0) + + return g15pythonlang.total_seconds( next_tick - now ) + + + ''' + Private + ''' + + def _load_surfaces(self): + self.display_date = g15gconf.get_bool_or_default(self.gconf_client, "%s/display_date" % self.gconf_key, True) + self.display_seconds = g15gconf.get_bool_or_default(self.gconf_client, "%s/display_seconds" % self.gconf_key, True) + self.display_date = g15gconf.get_bool_or_default(self.gconf_client, "%s/display_date" % self.gconf_key, True) + self.display_year = g15gconf.get_bool_or_default(self.gconf_client, "%s/display_year" % self.gconf_key, True) + self.display_digital_time = g15gconf.get_bool_or_default(self.gconf_client, "%s/display_digital_time" % self.gconf_key, True) + self.second_sweep = g15gconf.get_bool_or_default(self.gconf_client, "%s/second_sweep" % self.gconf_key, False) + self.twenty_four_hour = g15gconf.get_bool_or_default(self.gconf_client, "%s/twenty_four_hour" % self.gconf_key, False) + self.twenty_four_hour_digital = g15gconf.get_bool_or_default(self.gconf_client, "%s/twenty_four_hour_digital" % self.gconf_key, True) + + self.gconf_client.get_bool(self.gconf_key + "/twenty_four_hour") + + self.svg_size = None + self.width = self.screen.width + self.height = self.screen.height + + theme = self.gconf_client.get_string(self.gconf_key + "/theme") + if theme == None: + theme = "default" + + self.clock_theme_dir = get_theme_dir(self.screen.driver.get_model_name(), self.gconf_key, self.gconf_client, theme) + if not self.clock_theme_dir: + self.clock_theme_dir = get_theme_dir(self.screen.driver.get_model_name(), self.gconf_key, self.gconf_client, "default") + if not self.clock_theme_dir: + raise Exception("No themes could be found.") + self.behind_hands = self._load_surface_list(["clock-drop-shadow", "clock-face", "clock-marks"]) + self.hour_surfaces = self._load_surface_list(["clock-hour-hand-shadow", "clock-hour-hand"]) + self.minute_surfaces = self._load_surface_list(["clock-minute-hand-shadow", "clock-minute-hand"]) + self.second_surfaces = self._load_surface_list(["clock-secondhand-shadow", "clock-second-hand"]) + self.above_hands = self._load_surface_list([ "clock-face-shadow", "clock-glass", "clock-frame" ]) + + def _load_surface_list(self, names): + list = [] + for i in names: + path = self.clock_theme_dir + "/" + i + ".svg" + if os.path.exists(path): + svg = rsvg.Handle(path) + try: + if self.svg_size == None: + self.svg_size = svg.get_dimension_data()[2:4] + + svg_size = self.svg_size + + sx = self.width / svg_size[0] + sy = self.height / svg_size[1] + scale = min(sx, sy) + surface = cairo.SVGSurface(None, svg_size[0] * scale * 2,svg_size[1] * scale * 2) + context = cairo.Context(surface) + self.screen.configure_canvas(context) + context.scale(scale, scale) + context.translate(svg_size[0], svg_size[1]) + svg.render_cairo(context) + context.translate(-svg_size[0], -svg_size[1]) + list.append(((svg_size[0] * scale, svg_size[1] * scale), surface)) + finally: + svg.close() + + path = self.clock_theme_dir + "/" + i + ".gif" + if os.path.exists(path): + img_surface = g15cairo.load_surface_from_file(path, self.height) + surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, img_surface.get_width() * 2, img_surface.get_height() * 2) + context = cairo.Context(surface) + self.screen.configure_canvas(context) + context.translate(img_surface.get_width(), img_surface.get_height()) + context.set_source_surface(img_surface) + context.paint() + list.append(((img_surface.get_width(), img_surface.get_height()), surface)) + return list + + def _paint_thumbnail(self, canvas, allocated_size, horizontal): + scale = allocated_size / self.height + canvas.scale(scale, scale) + self._do_paint(canvas, self.width, self.height, False) + canvas.scale(1 / scale, 1 / scale) + return allocated_size + + def _paint_panel(self, canvas, allocated_size, horizontal): + if not self.screen.is_visible(self.page): + self.panel_text.set_canvas(canvas) + + # Don't display the date or seconds on mono displays, not enough room as it is + if self.screen.driver.get_bpp() == 1: + text = self._get_time_text(False) + font_size = 8 + factor = 2 + font_name = g15globals.fixed_size_font_name + x = 1 + gap = 1 + else: + factor = 1 if horizontal else 2 + font_name = "Sans" + if self.display_date: + text = "%s\n%s" % ( self._get_time_text(), self._get_date_text() ) + font_size = allocated_size / 3 + else: + text = self._get_time_text() + font_size = allocated_size / 2 + x = 4 + gap = 8 + + self.panel_text.set_attributes(text, align = pango.ALIGN_CENTER, font_desc = font_name, font_absolute_size = font_size * pango.SCALE / factor) + x, y, width, height = self.panel_text.measure() + if horizontal: + if self.screen.driver.get_bpp() == 1: + y = 0 + else: + y = (allocated_size / 2) - height / 2 + else: + x = (allocated_size / 2) - width / 2 + y = 0 + self.panel_text.draw(x, y) + if horizontal: + return width + gap + else: + return height + 4 + + def _paint(self, canvas, draw_date = True): + + width = float(self.screen.width) + height = float(self.screen.height) + + self._do_paint(canvas, width, height, self.display_date, self.display_digital_time) + + def _get_time_text(self, display_seconds = None): + if display_seconds == None: + display_seconds = self.display_seconds + if self.twenty_four_hour_digital: + return g15locale.format_time_24hour(datetime.datetime.now(), self.gconf_client, display_seconds) + else: + return g15locale.format_time(datetime.datetime.now(), self.gconf_client, display_seconds) + + def _get_date_text(self): + if self.display_year: + return datetime.datetime.now().strftime(locale.nl_langinfo(locale.D_FMT)) + else: + dformat = locale.nl_langinfo(locale.D_FMT) + for s in [ "/%y", "/%Y", ".%y", ".%Y", ".%y", ".%Y" ]: + dformat = dformat.replace(s, "") + return datetime.datetime.now().strftime(dformat) + + def _do_paint(self, canvas, width, height, draw_date = True, draw_time = True): + canvas.save() + self._do_paint_clock(canvas, width, height, draw_date and self.screen.driver.get_bpp() != 1, draw_time and self.screen.driver.get_bpp() != 1) + canvas.restore() + self.time_text.set_canvas(canvas) + + if self.screen.driver.get_bpp() == 1: + + rgb = self.screen.driver.get_color_as_ratios(g15driver.HINT_FOREGROUND, ( 0, 0, 0 )) + canvas.set_source_rgb(rgb[0],rgb[1],rgb[2]) + + if draw_date: + date_text = self._get_date_text() + self.time_text.set_attributes(pxwidth = 56, text = date_text, align = pango.ALIGN_CENTER, font_desc = "Fixed", font_absolute_size = 12 * pango.SCALE) + self.time_text.draw(0, 15) + + if draw_time: + time_text = self._get_time_text() + self.time_text.set_attributes(pxwidth = 56, text = time_text, align = pango.ALIGN_CENTER, font_desc = "Fixed", font_absolute_size = 12 * pango.SCALE) + self.time_text.draw(self.width - 56, 15) + + def _do_paint_clock(self, canvas, width, height, draw_date = True, draw_time = True): + + now = datetime.datetime.now() + properties = { } + + time = self._get_time_text() + + clock_width = min(width, height) + clock_height = min(width, height) + + drawing_surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, int(clock_width), int(clock_height)) + drawing_context = cairo.Context(drawing_surface) + self.screen.configure_canvas(drawing_context) + + # Below hands + for svg_size, surface in self.behind_hands: + drawing_context.save() + drawing_context.translate(-svg_size[0], -svg_size[1]) + drawing_context.set_source_surface(surface) + drawing_context.paint() + drawing_context.restore() + + # Date + t_offset = 0 + if draw_date: + drawing_context.save() + date_text = self._get_date_text() + drawing_context.select_font_face("Liberation Sans", + cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_BOLD) + drawing_context.set_font_size(27.0) + x_bearing, y_bearing, text_width, text_height = drawing_context.text_extents(date_text)[:4] + rgb = self.screen.driver.get_color_as_ratios(g15driver.HINT_FOREGROUND, ( 0, 0, 0 )) + drawing_context.set_source_rgb(rgb[0],rgb[1],rgb[2]) + tx = ( ( clock_width - text_width ) / 2 ) - x_bearing + ty = clock_height * 0.665 + t_offset += text_height + 4 + drawing_context.move_to( tx, ty ) + + drawing_context.show_text(date_text) + drawing_context.restore() + + # Date + if draw_time: + drawing_context.save() + time_text = self._get_time_text() + drawing_context.select_font_face("Liberation Sans", + cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_BOLD) + drawing_context.set_font_size(27.0) + x_bearing, y_bearing, text_width, text_height = drawing_context.text_extents(time_text)[:4] + rgb = self.screen.driver.get_color_as_ratios(g15driver.HINT_FOREGROUND, ( 0, 0, 0 )) + drawing_context.set_source_rgb(rgb[0],rgb[1],rgb[2]) + tx = ( ( clock_width - text_width ) / 2 ) - x_bearing + ty = ( clock_height * 0.665 ) + t_offset + drawing_context.move_to( tx, ty ) + + drawing_context.show_text(time_text) + drawing_context.restore() + + # The hand + if self.second_sweep: + ms_deg = ( ( float(now.microsecond) / 10000.0 ) * ( 6.0 / 100.0 ) ) + s_deg = ( now.second * 6 ) + ms_deg + else: + s_deg = now.second * 6 + m_deg = now.minute * 6 + ( now.second * ( 6.0 / 60.0 ) ) + + if self.twenty_four_hour: + h_deg = float(now.hour) * 15.0 + ( float ( now.minute * 0.25 ) ) + else: + h_deg = float( now.hour % 12 ) * 30.0 + ( float ( now.minute * 0.5 ) ) + + self._draw_hand(drawing_context, self.hour_surfaces, clock_width, clock_height, h_deg) + self._draw_hand(drawing_context, self.minute_surfaces, clock_width, clock_height, m_deg) + if self.display_seconds: + self._draw_hand(drawing_context, self.second_surfaces, clock_width, clock_height, s_deg) + + # Above hands + for svg_size, surface in self.above_hands: + drawing_context.save() + drawing_context.translate(-svg_size[0], -svg_size[1]) + drawing_context.set_source_surface(surface) + drawing_context.paint() + drawing_context.restore() + + # Paint to clock, centering it on the screen + canvas.translate( int(( width - height) / 2), 0) + canvas.set_source_surface(drawing_surface) + canvas.paint() + + + def _draw_hand(self, drawing_context, hand_surfaces, width, height, deg): + for svg_size, surface in hand_surfaces: + drawing_context.save() + drawing_context.translate(svg_size[0] / 2.0, svg_size[1] / 2.0) + g15cairo.rotate(drawing_context, -90) + g15cairo.rotate(drawing_context, deg) + drawing_context.translate(-svg_size[0], -svg_size[1]) + drawing_context.set_source_surface(surface) + drawing_context.paint() + drawing_context.restore() \ No newline at end of file diff --git a/src/plugins/cairo-clock/cairo-clock.ui b/src/plugins/cairo-clock/cairo-clock.ui new file mode 100644 index 0000000..20adbf3 --- /dev/null +++ b/src/plugins/cairo-clock/cairo-clock.ui @@ -0,0 +1,244 @@ + + + + + + 320 + False + 5 + Clock Preferences + False + True + center-on-parent + dialog + + + True + False + 2 + + + True + False + end + + + gtk-close + True + True + True + True + + + False + False + 0 + + + + + False + True + end + 0 + + + + + True + False + + + True + False + 0 + none + + + True + False + 12 + + + True + False + ThemeModel + + + + 0 + + + + + + + + + True + False + <b>Theme</b> + True + + + + + True + True + 0 + + + + + True + False + 0 + none + + + True + False + 12 + + + True + False + + + Sweeping second hand + True + True + False + 0.52999997138977051 + True + + + True + True + 0 + + + + + Show Seconds + True + True + False + True + + + True + True + 1 + + + + + Show Date + True + True + False + True + + + True + True + 2 + + + + + Display Digital Time + True + True + False + True + + + True + True + 3 + + + + + Show Year + True + True + False + True + + + True + True + 4 + + + + + 24hr Analogue Time (requires 24hr theme) + True + True + False + True + + + True + True + 5 + + + + + 24hr Digital Time + True + True + False + 0.54000002145767212 + True + + + True + True + 6 + + + + + + + + + True + False + <b>Display Options</b> + True + + + + + True + True + 1 + + + + + False + False + 1 + + + + + + button9 + + + + + + + + + diff --git a/src/plugins/cairo-clock/g15/Makefile.am b/src/plugins/cairo-clock/g15/Makefile.am new file mode 100644 index 0000000..4977d17 --- /dev/null +++ b/src/plugins/cairo-clock/g15/Makefile.am @@ -0,0 +1 @@ +SUBDIRS = default \ No newline at end of file diff --git a/src/plugins/cairo-clock/g15/default/Makefile.am b/src/plugins/cairo-clock/g15/default/Makefile.am new file mode 100644 index 0000000..51d4127 --- /dev/null +++ b/src/plugins/cairo-clock/g15/default/Makefile.am @@ -0,0 +1,10 @@ +cairoclockthemedir = $(datadir)/gnome15/plugins/cairo-clock/g15/default +cairoclocktheme_DATA = clock-frame.gif \ + clock-hour-hand.gif \ + clock-marks.gif \ + clock-glass.gif \ + clock-minute-hand.gif \ + clock-second-hand.gif + +EXTRA_DIST = \ + $(cairoclocktheme_DATA) diff --git a/src/plugins/cairo-clock/g15/default/clock-frame.gif b/src/plugins/cairo-clock/g15/default/clock-frame.gif new file mode 100644 index 0000000..58d13ac Binary files /dev/null and b/src/plugins/cairo-clock/g15/default/clock-frame.gif differ diff --git a/src/plugins/cairo-clock/g15/default/clock-glass.gif b/src/plugins/cairo-clock/g15/default/clock-glass.gif new file mode 100644 index 0000000..b846163 Binary files /dev/null and b/src/plugins/cairo-clock/g15/default/clock-glass.gif differ diff --git a/src/plugins/cairo-clock/g15/default/clock-hour-hand.gif b/src/plugins/cairo-clock/g15/default/clock-hour-hand.gif new file mode 100644 index 0000000..8f4b445 Binary files /dev/null and b/src/plugins/cairo-clock/g15/default/clock-hour-hand.gif differ diff --git a/src/plugins/cairo-clock/g15/default/clock-marks.gif b/src/plugins/cairo-clock/g15/default/clock-marks.gif new file mode 100644 index 0000000..6527b2a Binary files /dev/null and b/src/plugins/cairo-clock/g15/default/clock-marks.gif differ diff --git a/src/plugins/cairo-clock/g15/default/clock-minute-hand.gif b/src/plugins/cairo-clock/g15/default/clock-minute-hand.gif new file mode 100644 index 0000000..582f8a1 Binary files /dev/null and b/src/plugins/cairo-clock/g15/default/clock-minute-hand.gif differ diff --git a/src/plugins/cairo-clock/g15/default/clock-second-hand.gif b/src/plugins/cairo-clock/g15/default/clock-second-hand.gif new file mode 100644 index 0000000..221940e Binary files /dev/null and b/src/plugins/cairo-clock/g15/default/clock-second-hand.gif differ diff --git a/src/plugins/cairo-clock/g19/Makefile.am b/src/plugins/cairo-clock/g19/Makefile.am new file mode 100644 index 0000000..4977d17 --- /dev/null +++ b/src/plugins/cairo-clock/g19/Makefile.am @@ -0,0 +1 @@ +SUBDIRS = default \ No newline at end of file diff --git a/src/plugins/cairo-clock/g19/default/Makefile.am b/src/plugins/cairo-clock/g19/default/Makefile.am new file mode 100644 index 0000000..d6cc664 --- /dev/null +++ b/src/plugins/cairo-clock/g19/default/Makefile.am @@ -0,0 +1,16 @@ +cairoclockthemedir = $(datadir)/gnome15/plugins/cairo-clock/g19/default +cairoclocktheme_DATA = clock-drop-shadow.svg \ + clock-face.svg \ + clock-face-shadow.svg \ + clock-frame.svg \ + clock-glass.svg \ + clock-hour-hand.svg \ + clock-hour-hand-shadow.svg \ + clock-marks.svg \ + clock-minute-hand.svg \ + clock-minute-hand-shadow.svg \ + clock-second-hand.svg \ + clock-second-hand-shadow.svg + +EXTRA_DIST = \ + $(cairoclocktheme_DATA) diff --git a/src/plugins/cairo-clock/g19/default/clock-drop-shadow.svg b/src/plugins/cairo-clock/g19/default/clock-drop-shadow.svg new file mode 100644 index 0000000..8c18129 --- /dev/null +++ b/src/plugins/cairo-clock/g19/default/clock-drop-shadow.svg @@ -0,0 +1,144 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + diff --git a/src/plugins/cairo-clock/g19/default/clock-face-shadow.svg b/src/plugins/cairo-clock/g19/default/clock-face-shadow.svg new file mode 100644 index 0000000..12e2937 --- /dev/null +++ b/src/plugins/cairo-clock/g19/default/clock-face-shadow.svg @@ -0,0 +1,138 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + diff --git a/src/plugins/cairo-clock/g19/default/clock-face.svg b/src/plugins/cairo-clock/g19/default/clock-face.svg new file mode 100644 index 0000000..747321c --- /dev/null +++ b/src/plugins/cairo-clock/g19/default/clock-face.svg @@ -0,0 +1,128 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + diff --git a/src/plugins/cairo-clock/g19/default/clock-frame.svg b/src/plugins/cairo-clock/g19/default/clock-frame.svg new file mode 100644 index 0000000..b387b81 --- /dev/null +++ b/src/plugins/cairo-clock/g19/default/clock-frame.svg @@ -0,0 +1,221 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + diff --git a/src/plugins/cairo-clock/g19/default/clock-glass.svg b/src/plugins/cairo-clock/g19/default/clock-glass.svg new file mode 100644 index 0000000..2ebcb51 --- /dev/null +++ b/src/plugins/cairo-clock/g19/default/clock-glass.svg @@ -0,0 +1,112 @@ + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + diff --git a/src/plugins/cairo-clock/g19/default/clock-hour-hand-shadow.svg b/src/plugins/cairo-clock/g19/default/clock-hour-hand-shadow.svg new file mode 100644 index 0000000..4da746b --- /dev/null +++ b/src/plugins/cairo-clock/g19/default/clock-hour-hand-shadow.svg @@ -0,0 +1,125 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + diff --git a/src/plugins/cairo-clock/g19/default/clock-hour-hand.svg b/src/plugins/cairo-clock/g19/default/clock-hour-hand.svg new file mode 100644 index 0000000..e14dcb4 --- /dev/null +++ b/src/plugins/cairo-clock/g19/default/clock-hour-hand.svg @@ -0,0 +1,135 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + diff --git a/src/plugins/cairo-clock/g19/default/clock-marks.svg b/src/plugins/cairo-clock/g19/default/clock-marks.svg new file mode 100644 index 0000000..17781c3 --- /dev/null +++ b/src/plugins/cairo-clock/g19/default/clock-marks.svg @@ -0,0 +1,1163 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/plugins/cairo-clock/g19/default/clock-minute-hand-shadow.svg b/src/plugins/cairo-clock/g19/default/clock-minute-hand-shadow.svg new file mode 100644 index 0000000..d361f0b --- /dev/null +++ b/src/plugins/cairo-clock/g19/default/clock-minute-hand-shadow.svg @@ -0,0 +1,125 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + diff --git a/src/plugins/cairo-clock/g19/default/clock-minute-hand.svg b/src/plugins/cairo-clock/g19/default/clock-minute-hand.svg new file mode 100644 index 0000000..5d09dd4 --- /dev/null +++ b/src/plugins/cairo-clock/g19/default/clock-minute-hand.svg @@ -0,0 +1,125 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + diff --git a/src/plugins/cairo-clock/g19/default/clock-second-hand-shadow.svg b/src/plugins/cairo-clock/g19/default/clock-second-hand-shadow.svg new file mode 100644 index 0000000..f8b297c --- /dev/null +++ b/src/plugins/cairo-clock/g19/default/clock-second-hand-shadow.svg @@ -0,0 +1,140 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + diff --git a/src/plugins/cairo-clock/g19/default/clock-second-hand.svg b/src/plugins/cairo-clock/g19/default/clock-second-hand.svg new file mode 100644 index 0000000..3cc63e1 --- /dev/null +++ b/src/plugins/cairo-clock/g19/default/clock-second-hand.svg @@ -0,0 +1,158 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/src/plugins/cairo-clock/i18n/cairo-clock.en_GB.po b/src/plugins/cairo-clock/i18n/cairo-clock.en_GB.po new file mode 100644 index 0000000..a3a857e --- /dev/null +++ b/src/plugins/cairo-clock/i18n/cairo-clock.en_GB.po @@ -0,0 +1,54 @@ +# English translations for gnome15-plugins package. +# Copyright (C) 2012 THE gnome15-plugins'S COPYRIGHT HOLDER +# This file is distributed under the same license as the gnome15-plugins package. +# Automatically generated, 2012. +# +msgid "" +msgstr "" +"Project-Id-Version: gnome15-plugins 0.8.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: 2012-01-08 11:23+0000\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: en_GB\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=ASCII\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: i18n/cairo-clock.glade.h:1 +msgid "24hr Mode" +msgstr "24hr Mode" + +#: i18n/cairo-clock.glade.h:2 +msgid "Display Options" +msgstr "Display Options" + +#: i18n/cairo-clock.glade.h:3 +msgid "Theme" +msgstr "Theme" + +#: i18n/cairo-clock.glade.h:4 +msgid "Clock Preferences" +msgstr "Clock Preferences" + +#: i18n/cairo-clock.glade.h:5 +msgid "Display Digital Time" +msgstr "Display Digital Time" + +#: i18n/cairo-clock.glade.h:6 +msgid "Show Date" +msgstr "Show Date" + +#: i18n/cairo-clock.glade.h:7 +msgid "Show Seconds" +msgstr "Show Seconds" + +#: i18n/cairo-clock.glade.h:8 +msgid "Show Year" +msgstr "Show Year" + +#: i18n/cairo-clock.glade.h:9 +msgid "Sweeping second hand" +msgstr "Sweeping second hand" diff --git a/src/plugins/cairo-clock/i18n/cairo-clock.glade.h b/src/plugins/cairo-clock/i18n/cairo-clock.glade.h new file mode 100644 index 0000000..eed9f16 --- /dev/null +++ b/src/plugins/cairo-clock/i18n/cairo-clock.glade.h @@ -0,0 +1,9 @@ +char *s = N_("24hr Mode"); +char *s = N_("Display Options"); +char *s = N_("Theme"); +char *s = N_("Clock Preferences"); +char *s = N_("Display Digital Time"); +char *s = N_("Show Date"); +char *s = N_("Show Seconds"); +char *s = N_("Show Year"); +char *s = N_("Sweeping second hand"); diff --git a/src/plugins/cairo-clock/i18n/cairo-clock.pot b/src/plugins/cairo-clock/i18n/cairo-clock.pot new file mode 100644 index 0000000..4695fad --- /dev/null +++ b/src/plugins/cairo-clock/i18n/cairo-clock.pot @@ -0,0 +1,54 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: i18n/cairo-clock.glade.h:1 +msgid "24hr Mode" +msgstr "" + +#: i18n/cairo-clock.glade.h:2 +msgid "Display Options" +msgstr "" + +#: i18n/cairo-clock.glade.h:3 +msgid "Theme" +msgstr "" + +#: i18n/cairo-clock.glade.h:4 +msgid "Clock Preferences" +msgstr "" + +#: i18n/cairo-clock.glade.h:5 +msgid "Display Digital Time" +msgstr "" + +#: i18n/cairo-clock.glade.h:6 +msgid "Show Date" +msgstr "" + +#: i18n/cairo-clock.glade.h:7 +msgid "Show Seconds" +msgstr "" + +#: i18n/cairo-clock.glade.h:8 +msgid "Show Year" +msgstr "" + +#: i18n/cairo-clock.glade.h:9 +msgid "Sweeping second hand" +msgstr "" diff --git a/src/plugins/cairo-clock/mx5500/Makefile.am b/src/plugins/cairo-clock/mx5500/Makefile.am new file mode 100644 index 0000000..4977d17 --- /dev/null +++ b/src/plugins/cairo-clock/mx5500/Makefile.am @@ -0,0 +1 @@ +SUBDIRS = default \ No newline at end of file diff --git a/src/plugins/cairo-clock/mx5500/default/Makefile.am b/src/plugins/cairo-clock/mx5500/default/Makefile.am new file mode 100644 index 0000000..5dba142 --- /dev/null +++ b/src/plugins/cairo-clock/mx5500/default/Makefile.am @@ -0,0 +1,10 @@ +cairoclockthemedir = $(datadir)/gnome15/plugins/cairo-clock/mx5500/default +cairoclocktheme_DATA = clock-frame.gif \ + clock-hour-hand.gif \ + clock-marks.gif \ + clock-glass.gif \ + clock-minute-hand.gif \ + clock-second-hand.gif + +EXTRA_DIST = \ + $(cairoclocktheme_DATA) diff --git a/src/plugins/cairo-clock/mx5500/default/clock-frame.gif b/src/plugins/cairo-clock/mx5500/default/clock-frame.gif new file mode 100644 index 0000000..b27d9bf Binary files /dev/null and b/src/plugins/cairo-clock/mx5500/default/clock-frame.gif differ diff --git a/src/plugins/cairo-clock/mx5500/default/clock-glass.gif b/src/plugins/cairo-clock/mx5500/default/clock-glass.gif new file mode 100644 index 0000000..5d0a8f0 Binary files /dev/null and b/src/plugins/cairo-clock/mx5500/default/clock-glass.gif differ diff --git a/src/plugins/cairo-clock/mx5500/default/clock-hour-hand.gif b/src/plugins/cairo-clock/mx5500/default/clock-hour-hand.gif new file mode 100644 index 0000000..d43414e Binary files /dev/null and b/src/plugins/cairo-clock/mx5500/default/clock-hour-hand.gif differ diff --git a/src/plugins/cairo-clock/mx5500/default/clock-marks.gif b/src/plugins/cairo-clock/mx5500/default/clock-marks.gif new file mode 100644 index 0000000..132c28e Binary files /dev/null and b/src/plugins/cairo-clock/mx5500/default/clock-marks.gif differ diff --git a/src/plugins/cairo-clock/mx5500/default/clock-minute-hand.gif b/src/plugins/cairo-clock/mx5500/default/clock-minute-hand.gif new file mode 100644 index 0000000..e0ab5e2 Binary files /dev/null and b/src/plugins/cairo-clock/mx5500/default/clock-minute-hand.gif differ diff --git a/src/plugins/cairo-clock/mx5500/default/clock-second-hand.gif b/src/plugins/cairo-clock/mx5500/default/clock-second-hand.gif new file mode 100644 index 0000000..25fa89b Binary files /dev/null and b/src/plugins/cairo-clock/mx5500/default/clock-second-hand.gif differ diff --git a/src/plugins/cal-evolution/Makefile.am b/src/plugins/cal-evolution/Makefile.am new file mode 100644 index 0000000..854da95 --- /dev/null +++ b/src/plugins/cal-evolution/Makefile.am @@ -0,0 +1,7 @@ +plugindir = $(datadir)/gnome15/plugins/cal-evolution +plugin_DATA = cal-evolution.py \ + icon.png \ + cal-evolution.ui + +EXTRA_DIST = \ + $(plugin_DATA) \ No newline at end of file diff --git a/src/plugins/cal-evolution/cal-evolution.py b/src/plugins/cal-evolution/cal-evolution.py new file mode 100644 index 0000000..de6f320 --- /dev/null +++ b/src/plugins/cal-evolution/cal-evolution.py @@ -0,0 +1,131 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2012 Brett Smith +# +# 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 . + +""" +Calendar backend that retrieves event data from Evolution +""" + +import gnome15.g15locale as g15locale +import gnome15.g15accounts as g15accounts +_ = g15locale.get_translation("cal-evolution", modfile = __file__).ugettext +import gtk +import urllib +import vobject +import datetime +import dateutil +import sys, os, os.path +import re +import cal +import xdg.BaseDirectory +import logging +logger = logging.getLogger(__name__) + +""" +Plugin definition +""" +id="cal-evolution" +name=_("Calendar (Evolution support)") +description=_("Calendar for Evolution. Adds Evolution as a source for calendars \ +to the Calendar plugin") +author="Brett Smith " +copyright=_("Copyright (C)2012 Brett Smith") +site="http://www.russo79.com/gnome15" +has_preferences=False +global_plugin=True +passive=True +unsupported_models=cal.unsupported_models + +""" +Calendar Back-end module functions +""" +def create_options(account, account_ui): + return EvolutionCalendarOptions(account, account_ui) + +def create_backend(account, account_manager): + return EvolutionBackend() + +class EvolutionCalendarOptions(g15accounts.G15AccountOptions): + def __init__(self, account, account_ui): + g15accounts.G15AccountOptions.__init__(self, account, account_ui) + self.widget_tree = gtk.Builder() + self.widget_tree.add_from_file(os.path.join(os.path.dirname(__file__), "cal-evolution.ui")) + self.component = self.widget_tree.get_object("OptionPanel") + try : + self.event.valarm + self.alarm = True + except AttributeError as ae: + logger.debug("Could not set attribute", exc_info = ae) + pass + +class EvolutionEvent(cal.CalendarEvent): + + def __init__(self, parsed_event): + cal.CalendarEvent.__init__(self) + + self.start_date = parsed_event.dtstart.value + if parsed_event.dtend: + self.end_date = parsed_event.dtend.value + else: + self.end_date = datetime.datetime(self.start_date.year,self.start_date.month,self.start_date.day, 23, 59, 0) + + self.summary = parsed_event.summary.value + self.alt_icon = os.path.join(os.path.dirname(__file__), "icon.png") + +class EvolutionBackend(cal.CalendarBackend): + + def __init__(self): + cal.CalendarBackend.__init__(self) + + def get_events(self, now): + calendars = [] + event_days = {} + + # Find all the calendar files + cal_dir = os.path.join(xdg.BaseDirectory.xdg_data_home, "evolution", "calendar") + if not os.path.exists(cal_dir): + # Older versions of evolution store their data in ~/.evolution + cal_dir = os.path.expanduser("~/.evolution/calendar") + if os.path.exists(cal_dir): + for root, dirs, files in os.walk(cal_dir): + for _file in files: + if _file.endswith(".ics"): + calendars.append(os.path.join(root, _file)) + + for cal in calendars: + if not re.search("^webcal://", cal[1]): + f = open(cal) + calstring = ''.join(f.readlines()) + f.close() + try: + event_list = vobject.readOne(calstring).vevent_list + except AttributeError as ae: + logger.debug("Could not read attribute", exc_info = ae) + continue + else: # evolution library does not support webcal ics + webcal = urllib.urlopen('http://' + cal[1][9:]) + webcalstring = ''.join(webcal.readlines()) + webcal.close() + event_list = vobject.readOne(webcalstring).vevent_list + + for e in event_list: + if type(e) != vobject.icalendar.RecurringComponent: + parsed_event = vobject.readOne(e.get_as_string()) + else: + parsed_event = e + + self.check_and_add(EvolutionEvent(parsed_event), now, event_days) + + return event_days \ No newline at end of file diff --git a/src/plugins/cal-evolution/cal-evolution.ui b/src/plugins/cal-evolution/cal-evolution.ui new file mode 100644 index 0000000..b0b7e1c --- /dev/null +++ b/src/plugins/cal-evolution/cal-evolution.ui @@ -0,0 +1,26 @@ + + + + + + False + + + True + False + + + True + False + No configuration required + + + True + True + 0 + + + + + + diff --git a/src/plugins/cal-evolution/icon.png b/src/plugins/cal-evolution/icon.png new file mode 100644 index 0000000..a8ba4f1 Binary files /dev/null and b/src/plugins/cal-evolution/icon.png differ diff --git a/src/plugins/cal-google/Makefile.am b/src/plugins/cal-google/Makefile.am new file mode 100644 index 0000000..1089cfc --- /dev/null +++ b/src/plugins/cal-google/Makefile.am @@ -0,0 +1,8 @@ +plugindir = $(datadir)/gnome15/plugins/cal-google +plugin_DATA = cal-google.py \ + cal-google.ui \ + icon.png \ + iso8601.py + +EXTRA_DIST = \ + $(plugin_DATA) \ No newline at end of file diff --git a/src/plugins/cal-google/cal-google.py b/src/plugins/cal-google/cal-google.py new file mode 100644 index 0000000..84ba262 --- /dev/null +++ b/src/plugins/cal-google/cal-google.py @@ -0,0 +1,172 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2012 Brett Smith +# +# 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 . + +import gnome15.g15locale as g15locale +_ = g15locale.get_translation("cal-evolution", modfile = __file__).ugettext + +import gnome15.g15accounts as g15accounts +import gnome15.g15globals as g15globals +import cal +import gtk +import os +import datetime +import calendar +import gdata.calendar.data +import gdata.calendar.client +import gdata.acl.data +import gdata.service +import iso8601 +import subprocess +import socket + +# Logging +import logging +logger = logging.getLogger(__name__) + +""" +Plugin definition +""" +id="cal-google" +name=_("Calendar (Google support)") +description=_("Adds your Google Calendar as a source for the Gnome15 Calendar plugin") +author="Brett Smith " +copyright=_("Copyright (C)2012 Brett Smith") +site="http://www.russo79.com/gnome15" +has_preferences=False +passive=True +needs_network=True +global_plugin=True +requires="cal" +unsupported_models=cal.unsupported_models + +""" +Calendar Back-end module functions +""" +def create_options(account, account_ui): + return GoogleCalendarOptions(account, account_ui) + +def create_backend(account, account_manager): + return GoogleCalendarBackend(account, account_manager) + +# How often refresh from the evolution calendar. This can be a slow process, so not too often +REFRESH_INTERVAL = 15 * 60 + +class GoogleCalendarOptions(g15accounts.G15AccountOptions): + def __init__(self, account, account_ui): + g15accounts.G15AccountOptions.__init__(self, account, account_ui) + + self.widget_tree = gtk.Builder() + self.widget_tree.add_from_file(os.path.join(os.path.dirname(__file__), "cal-google.ui")) + self.component = self.widget_tree.get_object("OptionPanel") + + username = self.widget_tree.get_object("Username") + username.connect("changed", self._username_changed) + username.set_text(self.account.get_property("username", "")) + + calendar = self.widget_tree.get_object("Calendar") + calendar.connect("changed", self._calendar_changed) + calendar.set_text(self.account.get_property("calendar", "")) + + def _username_changed(self, widget): + self.account.properties["username"] = widget.get_text() + self.account_ui.save_accounts() + + def _calendar_changed(self, widget): + self.account.properties["calendar"] = widget.get_text() + self.account_ui.save_accounts() + +class GoogleEvent(cal.CalendarEvent): + + def __init__(self, when, parsed_event, color, url): + cal.CalendarEvent.__init__(self) + self.start_date = iso8601.parse_date(when.start) + if when.end: + # Cal plugin dates are inclusive, but googles seem to be exclusive + d = iso8601.parse_date(when.end) + if d.hour == 0 and d.minute == 0 and d.second == 0: + d = d - datetime.timedelta(0, 1) + + self.end_date = d + else: + self.end_date = datetime.datetime(self.start_date.year,self.start_date.month,self.start_date.day, 23, 59, 0) + + self.link = url + self.color = color + self.summary = parsed_event.title.text + self.alarm = len(when.reminder) > 0 + self.alt_icon = os.path.join(os.path.dirname(__file__), "icon.png") + + def activate(self): + logger.info("xdg-open '%s'", self.link) + subprocess.Popen(['xdg-open', self.link]) + + +class GoogleCalendarBackend(cal.CalendarBackend): + + def __init__(self, account, account_manager): + cal.CalendarBackend.__init__(self) + self.account = account + self.account_manager = account_manager + + def get_events(self, now): + self.cal_client = gdata.calendar.client.CalendarClient(source='%s-%s' % ( g15globals.name, g15globals.version ) ) + + # Reload the account + self.account = self.account_manager.by_name(self.account.name) + + for i in range(0, 3): + for j in range(0, 2): + password = self.account_manager.retrieve_password(self.account, "www.google.com", None, i > 0) + if password == None or password == "": + raise Exception(_("Authentication cancelled")) + + try : + return self._retrieve_events(now, password) + except gdata.client.BadAuthentication as e: + logger.debug("Error authenticating", exc_info = e) + pass + + raise Exception(_("Authentication attempted too many times")) + + + def _retrieve_events(self, now, password): + event_days = {} + self.cal_client.ClientLogin(self.account.get_property("username", ""), password, self.cal_client.source) + self.account_manager.store_password(self.account, password, "www.google.com", None) + start_date = datetime.date(now.year, now.month, 1) + end_date = datetime.date(now.year, now.month, calendar.monthrange(now.year, now.month)[1]) + feeds = self.cal_client.GetAllCalendarsFeed() + + for i, a_calendar in zip(xrange(len(feeds.entry)), feeds.entry): + query = gdata.calendar.client.CalendarEventQuery(start_min=start_date, start_max=end_date) + logger.info("Retrieving events from %s to %s", str(start_date), str(end_date)) + feed = self.cal_client.GetCalendarEventFeed(a_calendar.content.src, q = query) + + # TODO - Color doesn't seem to work + color = None + + for i, an_event in zip(xrange(len(feed.entry)), feed.entry): + logger.info('Adding event %s (%s)', an_event.title.text, str(an_event.when)) + + """ + An event may have multiple times. cal doesn't support multiple times, so we add multiple events instead + """ + for a_when in an_event.when: + self.check_and_add(GoogleEvent(a_when, an_event, color, a_calendar.content.src), now, event_days) + + return event_days + + diff --git a/src/plugins/cal-google/cal-google.ui b/src/plugins/cal-google/cal-google.ui new file mode 100644 index 0000000..4075098 --- /dev/null +++ b/src/plugins/cal-google/cal-google.ui @@ -0,0 +1,97 @@ + + + + + + False + + + True + False + + + True + False + 2 + 2 + 8 + 8 + + + True + False + 0 + Calendar: + + + GTK_FILL + + + + + + True + False + 0 + Username + + + 1 + 2 + GTK_FILL + + + + + + True + True + You may enter a hostname or IP address to use the +default port, or suffix the hostname with a colon and +the port number, e.g. mail.mycompany.com:110 + + False + False + True + True + + + 1 + 2 + GTK_FILL + + + + + + True + True + + False + False + True + True + + + 1 + 2 + 1 + 2 + + + + + + False + False + 8 + 0 + + + + + + + + + diff --git a/src/plugins/cal-google/icon.png b/src/plugins/cal-google/icon.png new file mode 100644 index 0000000..effd7a8 Binary files /dev/null and b/src/plugins/cal-google/icon.png differ diff --git a/src/plugins/cal-google/iso8601.py b/src/plugins/cal-google/iso8601.py new file mode 100644 index 0000000..5230e01 --- /dev/null +++ b/src/plugins/cal-google/iso8601.py @@ -0,0 +1,121 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2012 Brett Smith +# +# 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 . + +"""ISO 8601 date time string parsing + +Basic usage: +>>> import iso8601 +>>> iso8601.parse_date("2007-01-25T12:00:00Z") +datetime.datetime(2007, 1, 25, 12, 0, tzinfo=) +>>> +""" + +from datetime import datetime, timedelta, tzinfo +import re + +__all__ = ["parse_date", "ParseError"] + +# Adapted from http://delete.me.uk/2005/03/iso8601.html +ISO8601_REGEX = re.compile(r"(?P[0-9]{4})(-(?P[0-9]{1,2})(-(?P[0-9]{1,2})" + r"((?P.)(?P[0-9]{2}):(?P[0-9]{2})(:(?P[0-9]{2})(\.(?P[0-9]+))?)?" + r"(?PZ|(([-+])([0-9]{2}):([0-9]{2})))?)?)?)?" +) +TIMEZONE_REGEX = re.compile("(?P[+-])(?P[0-9]{2}).(?P[0-9]{2})") + +class ParseError(Exception): + """Raised when there is a problem parsing a date string""" + +# Yoinked from python docs +ZERO = timedelta(0) +class Utc(tzinfo): + """UTC + + """ + def utcoffset(self, dt): + return ZERO + + def tzname(self, dt): + return "UTC" + + def dst(self, dt): + return ZERO +UTC = Utc() + +class FixedOffset(tzinfo): + """Fixed offset in hours and minutes from UTC + + """ + def __init__(self, offset_hours, offset_minutes, name): + self.__offset = timedelta(hours=offset_hours, minutes=offset_minutes) + self.__name = name + + def utcoffset(self, dt): + return self.__offset + + def tzname(self, dt): + return self.__name + + def dst(self, dt): + return ZERO + + def __repr__(self): + return "" % self.__name + +def parse_timezone(tzstring, default_timezone=UTC): + """Parses ISO 8601 time zone specs into tzinfo offsets + + """ + if tzstring == "Z": + return default_timezone + # This isn't strictly correct, but it's common to encounter dates without + # timezones so I'll assume the default (which defaults to UTC). + # Addresses issue 4. + if tzstring is None: + return default_timezone + m = TIMEZONE_REGEX.match(tzstring) + prefix, hours, minutes = m.groups() + hours, minutes = int(hours), int(minutes) + if prefix == "-": + hours = -hours + minutes = -minutes + return FixedOffset(hours, minutes, tzstring) + +def parse_date(datestring, default_timezone=UTC): + """Parses ISO 8601 dates into datetime objects + + The timezone is parsed from the date string. However it is quite common to + have dates without a timezone (not strictly correct). In this case the + default timezone specified in default_timezone is used. This is UTC by + default. + """ + if not isinstance(datestring, basestring): + raise ParseError("Expecting a string %r" % datestring) + m = ISO8601_REGEX.match(datestring) + if not m: + raise ParseError("Unable to parse date string %r" % datestring) + groups = m.groupdict() + tz = parse_timezone(groups["timezone"], default_timezone=default_timezone) + if groups["fraction"] is None: + groups["fraction"] = 0 + else: + groups["fraction"] = int(float("0.%s" % groups["fraction"]) * 1e6) + return datetime(int(groups["year"] if "year" in groups else 0), + int(groups["month"] if "month" in groups else 1), + int(groups["day"] if "day" in groups else 1), + int(groups["hour"] if "hour" in groups and groups["hour"] is not None else 0), + int(groups["minute"] if "minute" in groups and groups["minute"] is not None else 0), + int(groups["second"] if "second" in groups and groups["second"] is not None else 0), + int(groups["fraction"] if "fraction" in groups and groups["fraction"] is not None else 0), tz) diff --git a/src/plugins/cal/Makefile.am b/src/plugins/cal/Makefile.am new file mode 100644 index 0000000..45d700f --- /dev/null +++ b/src/plugins/cal/Makefile.am @@ -0,0 +1,8 @@ +SUBDIRS = default +plugindir = $(datadir)/gnome15/plugins/cal +plugin_DATA = cal.py \ + cal.ui \ + bell.gif + +EXTRA_DIST = \ + $(plugin_DATA) \ No newline at end of file diff --git a/src/plugins/cal/bell.gif b/src/plugins/cal/bell.gif new file mode 100644 index 0000000..f32c902 Binary files /dev/null and b/src/plugins/cal/bell.gif differ diff --git a/src/plugins/cal/cal.py b/src/plugins/cal/cal.py new file mode 100644 index 0000000..ac3d9f1 --- /dev/null +++ b/src/plugins/cal/cal.py @@ -0,0 +1,516 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2010 Brett Smith +# +# 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 . + +import gnome15.g15locale as g15locale +_ = g15locale.get_translation("cal", modfile = __file__).ugettext + +import gnome15.g15theme as g15theme +import gnome15.g15driver as g15driver +import gnome15.util.g15convert as g15convert +import gnome15.util.g15scheduler as g15scheduler +import gnome15.util.g15uigconf as g15uigconf +import gnome15.util.g15gconf as g15gconf +import gnome15.util.g15cairo as g15cairo +import gnome15.util.g15icontools as g15icontools +import gnome15.g15screen as g15screen +import gnome15.g15accounts as g15accounts +import gnome15.g15plugin as g15plugin +import gnome15.g15globals as g15globals +import datetime +import time +import os, os.path +import gtk +import calendar + +# Logging +import logging +logger = logging.getLogger(__name__) + +# Plugin data +id="cal" +name=_("Calendar") +description=_("1Provides basic support for calendars. To make this\n\ +plugin work, you will also need a second plugin for your calendar\n\ +provider. Currently, Gnome15 supports Evolution and Google calendars.") +author="Brett Smith " +copyright=_("Copyright (C)2010 Brett Smith") +site="http://www.russo79.com/gnome15" +has_preferences=True +actions={ + g15driver.PREVIOUS_SELECTION : _("Previous day/Event"), + g15driver.NEXT_SELECTION : _("Next day/Event"), + g15driver.VIEW : _("Return to today"), + g15driver.CLEAR : _("Toggle calendar/events"), + g15driver.NEXT_PAGE : _("Next week"), + g15driver.PREVIOUS_PAGE : _("Previous week") +} +actions_g19={ + g15driver.PREVIOUS_PAGE : _("Previous day/Event"), + g15driver.NEXT_PAGE : _("Next day/Event"), + g15driver.VIEW : _("Return to today"), + g15driver.CLEAR : _("Toggle calendar/events"), + g15driver.NEXT_SELECTION : _("Next week"), + g15driver.PREVIOUS_SELECTION : _("Previous week") +} +unsupported_models = [ g15driver.MODEL_G110, g15driver.MODEL_G11, \ + g15driver.MODEL_MX5500, g15driver.MODEL_G930, \ + g15driver.MODEL_G35 ] + +# How often refresh from the evolution calendar. This can be a slow process, so not too often +REFRESH_INTERVAL = 15 * 60 + +# Configuration +CONFIG_PATH = os.path.join(g15globals.user_config_dir, "plugin-data", "cal", "calendars.xml") +CONFIG_ITEM_NAME = "calendar" + +""" +Functions +""" + +def create(gconf_key, gconf_client, screen): + return G15Cal(gconf_key, gconf_client, screen) + +def show_preferences(parent, driver, gconf_client, gconf_key): + G15CalendarPreferences(parent, gconf_client, gconf_key) + +def get_backend(account_type): + """ + Get the backend plugin module, given the account_type + + Keyword arguments: + account_type -- account type + """ + import gnome15.g15pluginmanager as g15pluginmanager + return g15pluginmanager.get_module_for_id("cal-%s" % account_type) + +def get_available_backends(): + """ + Get the "account type" names that are available by listing all of the + backend plugins that are installed + """ + l = [] + import gnome15.g15pluginmanager as g15pluginmanager + for p in g15pluginmanager.imported_plugins: + if p.id.startswith("cal-"): + l.append(p.id[4:]) + return l + +class CalendarEvent(): + + def __init__(self): + self.start_date = None + self.end_date = None + self.summary = None + self.color = None + self.alarm = False + self.alt_icon = None + + def activate(self): + raise Exception("Not implemented") + +class CalendarBackend(): + + def __init__(self): + self.start_date = None + self.end_date = None + + def check_and_add(self, ve, now, event_days): + if ve.start_date.month == now.month and ve.start_date.year == now.year: + day = ve.start_date.day + while day <= ve.end_date.day: + key = str(day) + day_event_list = event_days[key] if key is event_days else None + if day_event_list is None: + day_event_list = list() + event_days[key] = day_event_list + day_event_list.append(ve) + day += 1 + + def get_events(self, now): + raise Exception("Not implemented") + +class EventMenuItem(g15theme.MenuItem): + + def __init__(self, plugin, event, component_id): + g15theme.MenuItem.__init__(self, component_id) + self.event = event + self.plugin = plugin + + def get_default_theme_dir(self): + return os.path.join(os.path.dirname(__file__), "default") + + def get_theme_properties(self): + item_properties = g15theme.MenuItem.get_theme_properties(self) + item_properties["item_name"] = self.event.summary + + start_str = self.event.start_date.strftime("%H:%M") + end_str = self.event.end_date.strftime("%H:%M") + + if not self._same_day(self.event.start_date, self.event.end_date): + if not self._same_day(self.plugin._calendar_date, self.event.end_date): + end_str = _(self.event.end_date.strftime("%m/%d")) + if not self._same_day(self.plugin._calendar_date, self.event.start_date): + start_str = _(self.event.start_date.strftime("%m/%d")) + + + if self._same_day(self.event.start_date, self.event.end_date) and \ + self.event.start_date.hour == 0 and self.event.start_date.minute == 0 and \ + self.event.end_date.hour == 23 and self.event.end_date.minute == 59: + item_properties["item_alt"] = _("All Day") + else: + item_properties["item_alt"] = "%s-%s" % ( start_str, end_str) + + item_properties["item_alarm"] = self.event.alarm + if self.event.alarm: + if self.get_screen().device.bpp > 1: + item_properties["item_icon"] = g15icontools.get_icon_path([ "stock_alarm", "alarm-clock", "alarm-timer", "dialog-warning" ]) + else: + item_properties["item_icon"] = os.path.join(os.path.dirname(__file__), 'bell.gif') + if self.event.alt_icon: + item_properties["alt_icon"] = self.event.alt_icon + return item_properties + + def activate(self): + self.event.activate() + + def _same_day(self, date1, date2): + return date1.day == date2.day and date1.month == date2.month and date1.year == date2.year + +class Cell(g15theme.Component): + def __init__(self, day, now, event, component_id): + g15theme.Component.__init__(self, component_id) + self.day = day + self.now = now + self.event = event + + def on_configure(self): + self.set_theme(g15theme.G15Theme(os.path.join(os.path.dirname(__file__), "default"), "cell")) + + def get_theme_properties(self): + weekday = self.day.weekday() + properties = {} + properties["weekday"] = weekday + properties["day"] = self.day.day + properties["event"] = self.event.summary if self.event else "" + if self.now.day == self.day.day and self.now.month == self.day.month: + properties["today"] = True + return properties + + def get_item_attributes(self, selected): + return {} + +class Calendar(g15theme.Component): + + def __init__(self, component_id="calendar"): + g15theme.Component.__init__(self, component_id) + self.layout_manager = g15theme.GridLayoutManager(7) + self.focusable = True + + +class G15CalendarPreferences(g15accounts.G15AccountPreferences): + ''' + Configuration UI + ''' + + def __init__(self, parent, gconf_client, gconf_key): + g15accounts.G15AccountPreferences.__init__(self, parent, gconf_client, \ + gconf_key, \ + CONFIG_PATH, \ + CONFIG_ITEM_NAME) + + def get_account_types(self): + return get_available_backends() + + def get_account_type_name(self, account_type): + return _(account_type) + + def create_options_for_type(self, account, account_type): + backend = get_backend(account.type) + if backend is None: + logger.warning("No backend for account type %s", account_type) + return None + return backend.create_options(account, self) + + def create_general_options(self): + widget_tree = gtk.Builder() + widget_tree.add_from_file(os.path.join(os.path.dirname(__file__), "cal.ui")) + g15uigconf.configure_checkbox_from_gconf(self.gconf_client, "%s/twenty_four_hour_times" % self.gconf_key, "TwentyFourHourTimes", True, widget_tree) + return widget_tree.get_object("OptionPanel") + +class G15Cal(g15plugin.G15Plugin): + + def __init__(self, gconf_key, gconf_client, screen): + g15plugin.G15Plugin.__init__(self, gconf_client, gconf_key, screen) + self._timer = None + self._icon_path = g15icontools.get_icon_path(["calendar", "evolution-calendar", "office-calendar", "stock_calendar" ]) + self._thumb_icon = g15cairo.load_surface_from_file(self._icon_path) + + def activate(self): + g15plugin.G15Plugin.activate(self) + + self._active = True + self._event_days = None + self._calendar_date = None + self._page = None + self._theme = g15theme.G15Theme(os.path.join(os.path.dirname(__file__), "default"), auto_dirty = False) + self._loaded = 0 + + # Backend + self._account_manager = g15accounts.G15AccountManager(CONFIG_PATH, CONFIG_ITEM_NAME) + + # Calendar + self._calendar = Calendar() + + # Menu + self._menu = g15theme.Menu("menu") + self._menu.focusable = True + self._menu.focused_component = True + + # Page + self._page = g15theme.G15Page(name, self.screen, on_shown = self._on_shown, \ + on_hidden = self._on_hidden, theme_properties_callback = self._get_properties, + thumbnail_painter = self._paint_thumbnail, + originating_plugin = self) + self._page.set_title(_("Calendar")) + self._page.set_theme(self._theme) + self._page.focused_component = self._calendar + self._calendar.set_focused(True) + + # List for account changes + self._account_manager.add_change_listener(self._accounts_changed) + self.screen.key_handler.action_listeners.append(self) + + # Run first load in thread + self._page.add_child(self._menu) + self._page.add_child(self._calendar) + self._page.add_child(g15theme.MenuScrollbar("viewScrollbar", self._menu)) + self.screen.add_page(self._page) + g15scheduler.schedule("CalendarFirstLoad", 0, self._redraw) + + # Listen for changes in the network state + self.screen.service.network_manager.listeners.append(self._network_state_changed) + + # Config changes + self.watch("twenty_four_hour_times", self._config_changed) + + def deactivate(self): + g15plugin.G15Plugin.deactivate(self) + self.screen.service.network_manager.listeners.append(self._network_state_changed) + self._account_manager.remove_change_listener(self._accounts_changed) + self.screen.key_handler.action_listeners.remove(self) + if self._timer != None: + self._timer.cancel() + if self._page != None: + g15screen.run_on_redraw(self.screen.del_page, self._page) + + def destroy(self): + pass + + def action_performed(self, binding): + if self._page and self._page.is_visible(): + if self._calendar.is_focused(): + if ( binding.action == g15driver.PREVIOUS_PAGE and self.screen.device.model_id == g15driver.MODEL_G19 ) or \ + ( binding.action == g15driver.PREVIOUS_SELECTION and self.screen.device.model_id != g15driver.MODEL_G19 ): + self._adjust_calendar_date(-1) + return True + elif ( binding.action == g15driver.NEXT_PAGE and self.screen.device.model_id == g15driver.MODEL_G19 ) or \ + ( binding.action == g15driver.NEXT_SELECTION and self.screen.device.model_id != g15driver.MODEL_G19 ): + self._adjust_calendar_date(1) + return True + elif ( binding.action == g15driver.PREVIOUS_SELECTION and self.screen.device.model_id == g15driver.MODEL_G19 ) or \ + ( binding.action == g15driver.PREVIOUS_PAGE and self.screen.device.model_id != g15driver.MODEL_G19 ): + self._adjust_calendar_date(-7) + return True + elif ( binding.action == g15driver.NEXT_SELECTION and self.screen.device.model_id == g15driver.MODEL_G19 ) or \ + ( binding.action == g15driver.NEXT_PAGE and self.screen.device.model_id != g15driver.MODEL_G19 ): + self._adjust_calendar_date(7) + return True + elif binding.action == g15driver.VIEW: + self._calendar_date = None + self._adjust_calendar_date(0) + return True + if binding.action == g15driver.CLEAR: + self._page.next_focus() + return True + + """ + Private + """ + + def _config_changed(self, client, connection_id, entry, args): + self._loaded = 0 + self._redraw() + + def _network_state_changed(self, state): + self._loaded = 0 + self._redraw() + + def _accounts_changed(self, account_manager): + self._loaded = 0 + self._redraw() + + def _adjust_calendar_date(self, amount): + o_date = self._get_calendar_date() + self._calendar_date = o_date + datetime.timedelta(amount) + if amount == 0 or o_date.month != self._calendar_date.month or o_date.year != self._calendar_date.year: + self._load_month_events(self._calendar_date) + else: + g15screen.run_on_redraw(self._rebuild_components, self._calendar_date) + + def _get_calendar_date(self): + now = datetime.datetime.now() + return self._calendar_date if self._calendar_date is not None else now + + def _get_properties(self): + now = datetime.datetime.now() + calendar_date = self._get_calendar_date() + properties = {} + properties["icon"] = self._icon_path + properties["title"] = _('Calendar') + if g15gconf.get_bool_or_default(self.gconf_client, "%s/twenty_four_hour_times" % self.gconf_key, True): + properties["time"] = g15locale.format_time_24hour(now, self.gconf_client, False) + properties["full_time"] = g15locale.format_time_24hour(now, self.gconf_client, True) + else: + properties["full_time"] = g15locale.format_time(now, self.gconf_client, True) + properties["time"] = g15locale.format_time(now, self.gconf_client, False) + properties["time_24"] = now.strftime("%H:%M") + properties["full_time_24"] = now.strftime("%H:%M:%S") + properties["time_12"] = now.strftime("%I:%M %p") + properties["full_time_12"] = now.strftime("%I:%M:%S %p") + properties["short_date"] = now.strftime("%a %d %b") + properties["full_date"] = now.strftime("%A %d %B") + properties["date"] = g15locale.format_date(now, self.gconf_client) + properties["locale_date"] = now.strftime("%x") + properties["locale_time"] = now.strftime("%X") + properties["year"] = now.strftime("%Y") + properties["short_year"] = now.strftime("%y") + properties["week"] = now.strftime("%W") + properties["month"] = now.strftime("%m") + properties["month_name"] = now.strftime("%B") + properties["short_month_name"] = now.strftime("%b") + properties["day_name"] = now.strftime("%A") + properties["short_day_name"] = now.strftime("%a") + properties["day_of_year"] = now.strftime("%d") + properties["cal_year"] = calendar_date.strftime("%Y") + properties["cal_month"] = calendar_date.strftime("%m") + properties["cal_month_name"] = calendar_date.strftime("%B") + properties["cal_short_month_name"] = calendar_date.strftime("%b") + properties["cal_year"] = calendar_date.strftime("%Y") + properties["cal_short_year"] = calendar_date.strftime("%y") + properties["cal_locale_date"] = calendar_date.strftime("%x") + if self._event_days is None or not str(calendar_date.day) in self._event_days: + properties["message"] = "No events" + properties["events"] = False + else: + properties["events"] = True + properties["message"] = "" + return properties + + def _load_month_events(self, now): + self._event_days = {} + + for c in self._page.get_children(): + if isinstance(c, Cell): + pass + + # Get all the events for this month + for acc in self._account_manager.accounts: + try: + backend = get_backend(acc.type) + if backend is None: + logger.warning("Could not find a calendar backend for %s", acc.name) + else: + # Backends may specify if they need a network or not, so check the state + import gnome15.g15pluginmanager as g15pluginmanager + needs_net = g15pluginmanager.is_needs_network(backend) + if not needs_net or ( needs_net and self.screen.service.network_manager.is_network_available() ): + backend_events = backend.create_backend(acc, self._account_manager).get_events(now) + if backend_events is None: + logger.warning("Calendar returned no events, skipping") + else: + self._event_days = dict(self._event_days.items() + \ + backend_events.items()) + else: + logger.warning("Skipping backend %s because it requires the network, " \ + "and the network is not availabe", acc.type) + except Exception as e: + logger.warning("Failed to load events for account %s.", acc.name, exc_info = e) + + g15screen.run_on_redraw(self._rebuild_components, now) + self._page.mark_dirty() + + def _rebuild_components(self, now): + self._menu.remove_all_children() + if str(now.day) in self._event_days: + events = self._event_days[str(now.day)] + i = 0 + for event in events: + self._menu.add_child(EventMenuItem(self, event, "menuItem-%d" % i)) + i += 1 + + # Add the date cell components + self._calendar.remove_all_children() + cal = calendar.Calendar() + i = 0 + for day in cal.itermonthdates(now.year, now.month): + event = None + if str(day.day) in self._event_days: + event = self._event_days[str(day.day)][0] + self._calendar.add_child(Cell(day, now, event, "cell-%d" % i)) + i += 1 + + self._page.mark_dirty() + self._page.redraw() + + def _schedule_redraw(self): + if self.screen.is_visible(self._page): + if self._timer is not None: + self._timer.cancel() + + """ + Because the calendar page also displays a clock, we want to + redraw at second zero of every minute + """ + self._timer = g15scheduler.schedule("CalRedraw", 60 - time.gmtime().tm_sec, self._redraw) + + def _on_shown(self): + self._hidden = False + self._redraw() + + def _on_hidden(self): + if self._timer != None: + self._timer.cancel() + + def _redraw(self): + t = time.time() + if t > self._loaded + REFRESH_INTERVAL: + self._loaded = t + self._reload_events_now() + else: + self._page.mark_dirty() + self.screen.redraw(self._page) + self._schedule_redraw() + + def _reload_events_now(self): + self._load_month_events(self._get_calendar_date()) + self.screen.redraw(self._page) + self._schedule_redraw() + + def _paint_thumbnail(self, canvas, allocated_size, horizontal): + if self._page != None and self._thumb_icon != None and self.screen.driver.get_bpp() == 16: + return g15cairo.paint_thumbnail_image(allocated_size, self._thumb_icon, canvas) + + diff --git a/src/plugins/cal/cal.ui b/src/plugins/cal/cal.ui new file mode 100644 index 0000000..4dc12e8 --- /dev/null +++ b/src/plugins/cal/cal.ui @@ -0,0 +1,31 @@ + + + + + + False + + + True + False + + + Use 24hr format for times + True + True + False + True + + + False + True + 0 + + + + + + + + + diff --git a/src/plugins/cal/default/Makefile.am b/src/plugins/cal/default/Makefile.am new file mode 100644 index 0000000..7cf5275 --- /dev/null +++ b/src/plugins/cal/default/Makefile.am @@ -0,0 +1,29 @@ +themedir = $(datadir)/gnome15/plugins/cal/default +theme_DATA = default.svg \ + default-cell.svg \ + default-menu-entry.svg \ + g19.svg \ + g19-cell.svg \ + g19-menu-entry.svg + +EXTRA_DIST = \ + $(theme_DATA) + +all-local: + for M_LOCALE in @ENABLED_LOCALES@; do \ + mkdir -p i18n/$$M_LOCALE/LC_MESSAGES ; \ + if [ `ls i18n/*.po 2>/dev/null|wc -l` -gt 0 ]; then \ + for M_PO in i18n/*.po; do \ + BN=`basename $$M_PO .po`; \ + LL=`basename $$BN .$$M_LOCALE`.mo; \ + echo "$$M_PO -> $$LL"; \ + msgfmt $$M_PO --output-file i18n/$$M_LOCALE/LC_MESSAGES/$$LL; \ + done; \ + fi; \ + done + +install-exec-hook: + for M_LOCALE in @ENABLED_LOCALES@; do \ + mkdir -p $(DESTDIR)$(datadir)/gnome15/plugins/cal/default/i18n; \ + cp -pR i18n/$$M_LOCALE $(DESTDIR)$(datadir)/gnome15/plugins/cal/default/i18n; \ + done diff --git a/src/plugins/cal/default/default-cell.svg b/src/plugins/cal/default/default-cell.svg new file mode 100644 index 0000000..725094f --- /dev/null +++ b/src/plugins/cal/default/default-cell.svg @@ -0,0 +1,145 @@ + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + ${day} + ${day} + + diff --git a/src/plugins/cal/default/default-menu-entry.svg b/src/plugins/cal/default/default-menu-entry.svg new file mode 100644 index 0000000..870c7fd --- /dev/null +++ b/src/plugins/cal/default/default-menu-entry.svg @@ -0,0 +1,200 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + ${item_alt} + ${item_name} + + + + ${item_alt} + + ${item_name} + + diff --git a/src/plugins/cal/default/default.svg b/src/plugins/cal/default/default.svg new file mode 100644 index 0000000..f45f9eb --- /dev/null +++ b/src/plugins/cal/default/default.svg @@ -0,0 +1,313 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + ${cal_locale_date} + + + ${message} + + + M + + + + T + + + + W + + + + T + + + + F + + + + S + + + + S + + + + + + + + + diff --git a/src/plugins/cal/default/g19-cell.svg b/src/plugins/cal/default/g19-cell.svg new file mode 100644 index 0000000..7364818 --- /dev/null +++ b/src/plugins/cal/default/g19-cell.svg @@ -0,0 +1,133 @@ + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + ${day} + + + diff --git a/src/plugins/cal/default/g19-menu-entry.svg b/src/plugins/cal/default/g19-menu-entry.svg new file mode 100644 index 0000000..649418a --- /dev/null +++ b/src/plugins/cal/default/g19-menu-entry.svg @@ -0,0 +1,361 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + ${item_name} + ${item_alt} + + + + + + ${item_name} + ${item_alt} + + + + diff --git a/src/plugins/cal/default/g19.svg b/src/plugins/cal/default/g19.svg new file mode 100644 index 0000000..70bf768 --- /dev/null +++ b/src/plugins/cal/default/g19.svg @@ -0,0 +1,528 @@ + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + ${time} + ${date} + + + + Sa + + Su + Mo + + Tu + + We + + Th + + Fr + + _(Events) + + + + + + + + + + + + + ${title} + + ${cal_month_name} ${cal_year} + ${message} + + + + + + + + _(Today) + + + + + _(Events/Calendar) + + diff --git a/src/plugins/cal/i18n/cal.en_GB.po b/src/plugins/cal/i18n/cal.en_GB.po new file mode 100644 index 0000000..eff8634 --- /dev/null +++ b/src/plugins/cal/i18n/cal.en_GB.po @@ -0,0 +1,54 @@ +# English translations for gnome15-plugins package. +# Copyright (C) 2012 THE gnome15-plugins'S COPYRIGHT HOLDER +# This file is distributed under the same license as the gnome15-plugins package. +# Automatically generated, 2012. +# +msgid "" +msgstr "" +"Project-Id-Version: gnome15-plugins 0.8.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: 2012-01-08 11:23+0000\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: en_GB\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=ASCII\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: cal.py:37 +msgid "Calendar" +msgstr "Calendar" + +#: cal.py:38 +msgid "Calendar. Integrates with Evolution calendar." +msgstr "Calendar. Integrates with Evolution calendar." + +#: cal.py:40 +msgid "Copyright (C)2010 Brett Smith" +msgstr "Copyright (C)2010 Brett Smith" + +#: cal.py:44 +msgid "Previous day/Event" +msgstr "Previous day/Event" + +#: cal.py:45 +msgid "Next day/Event" +msgstr "Next day/Event" + +#: cal.py:46 +msgid "" +"Toggle between calendar\n" +"and events" +msgstr "" +"Toggle between calendar\n" +"and events" + +#: cal.py:47 +msgid "Next week" +msgstr "Next week" + +#: cal.py:48 +msgid "Previous week" +msgstr "Previous week" diff --git a/src/plugins/cal/i18n/cal.pot b/src/plugins/cal/i18n/cal.pot new file mode 100644 index 0000000..fa5b148 --- /dev/null +++ b/src/plugins/cal/i18n/cal.pot @@ -0,0 +1,52 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: cal.py:37 +msgid "Calendar" +msgstr "" + +#: cal.py:38 +msgid "Calendar. Integrates with Evolution calendar." +msgstr "" + +#: cal.py:40 +msgid "Copyright (C)2010 Brett Smith" +msgstr "" + +#: cal.py:44 +msgid "Previous day/Event" +msgstr "" + +#: cal.py:45 +msgid "Next day/Event" +msgstr "" + +#: cal.py:46 +msgid "" +"Toggle between calendar\n" +"and events" +msgstr "" + +#: cal.py:47 +msgid "Next week" +msgstr "" + +#: cal.py:48 +msgid "Previous week" +msgstr "" diff --git a/src/plugins/clock/Makefile.am b/src/plugins/clock/Makefile.am new file mode 100644 index 0000000..1bec669 --- /dev/null +++ b/src/plugins/clock/Makefile.am @@ -0,0 +1,8 @@ +SUBDIRS = default + +plugindir = $(datadir)/gnome15/plugins/clock +plugin_DATA = clock.ui \ + clock.py + +EXTRA_DIST = \ + $(plugin_DATA) \ No newline at end of file diff --git a/src/plugins/clock/clock.py b/src/plugins/clock/clock.py new file mode 100644 index 0000000..6ea893f --- /dev/null +++ b/src/plugins/clock/clock.py @@ -0,0 +1,366 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2010 Brett Smith +# +# 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 . + +import gnome15.g15locale as g15locale +_ = g15locale.get_translation("clock", modfile = __file__).ugettext + +import gnome15.g15screen as g15screen +import gnome15.g15theme as g15theme +import gnome15.util.g15scheduler as g15scheduler +import gnome15.util.g15pythonlang as g15pythonlang +import gnome15.g15driver as g15driver +import gnome15.g15globals as g15globals +import gnome15.g15text as g15text +import gnome15.g15plugin as g15plugin +import datetime +import gtk +import pango +import os +import locale + +# Plugin details - All of these must be provided +id="clock" +name=_("Clock") +description=_("Just displays a simple clock. This is the plugin used in \ +the tutorial at the Gnome15 site.") +author="Brett Smith " +copyright=_("Copyright (C)2010 Brett Smith") +site="http://www.russo79.com/gnome15" +has_preferences=True +unsupported_models = [ g15driver.MODEL_G110, g15driver.MODEL_G11, g15driver.MODEL_G930, g15driver.MODEL_G35 ] + +# +# This simple plugin displays a digital clock. It also demonstrates +# how to add a preferences dialog for your plugin +# + +''' +This function must create your plugin instance. You are provided with +a GConf client and a Key prefix to use if your plugin has preferences +''' +def create(gconf_key, gconf_client, screen): + return G15Clock(gconf_key, gconf_client, screen) + +''' +This function must be provided if you set has_preferences to True. You +should display a dialog for editing the plugins preferences +''' +def show_preferences(parent, driver, gconf_client, gconf_key): + widget_tree = gtk.Builder() + widget_tree.add_from_file(os.path.join(os.path.dirname(__file__), "clock.ui")) + + dialog = widget_tree.get_object("ClockDialog") + dialog.set_transient_for(parent) + + display_seconds = widget_tree.get_object("DisplaySecondsCheckbox") + display_seconds.set_active(gconf_client.get_bool(gconf_key + "/display_seconds")) + display_seconds.connect("toggled", _changed, gconf_key + "/display_seconds", gconf_client) + + display_date = widget_tree.get_object("DisplayDateCheckbox") + display_date.set_active(gconf_client.get_bool(gconf_key + "/display_date")) + display_date.connect("toggled", _changed, gconf_key + "/display_date", gconf_client) + + use_24hr_format = widget_tree.get_object("TwentFourHourCheckbox") + use_24hr_format.set_active(gconf_client.get_bool(gconf_key + "/use_24hr_format")) + use_24hr_format.connect("toggled", _changed, gconf_key + "/use_24hr_format", gconf_client) + + dialog.run() + dialog.hide() + +def _changed(widget, key, gconf_client): + ''' + gconf configuration has changed, redraw our canvas + ''' + gconf_client.set_bool(key, widget.get_active()) + +class G15Clock(g15plugin.G15Plugin): + ''' + You would normally want to extend at least g15plugin.G15Plugin as it + provides basic plugin functions. + + There are also further specialisations, such as g15plugin.G15PagePlugin + for plugins that have display a page, or g15plugin.G15MenuPlugin for + menu like plugins, or g15plugin.G15RefreshingPlugin for plugins that + refresh their view based on a timer. + + This example uses the most basic type to demonstrate how plugins are put + together, but it could easily use G15RefreshingPlugin and cut out a lot + of code. + + ''' + + + ''' + ****************************************************************** + * Lifecycle functions. You must provide activate and deactivate, * + * the constructor and destroy function are optional * + ****************************************************************** + ''' + + def __init__(self, gconf_key, gconf_client, screen): + g15plugin.G15Plugin.__init__(self, gconf_client, gconf_key, screen) + self.hidden = False + self.page = None + + def activate(self): + ''' + The activate function is invoked when gnome15 starts up, or the plugin is re-enabled + after it has been disabled. When extending any of the provided base plugin classes, + you nearly always want to call the function in the supoer class as well + ''' + g15plugin.G15Plugin.activate(self) + + + ''' + Load our configuration + ''' + self.timer = None + self._load_configuration() + + ''' + We will be drawing text manually in the thumbnail, so it is recommended you use the + G15Text class which simplifies drawing and measuring text in an efficient manner + ''' + self.text = g15text.new_text(self.screen) + + ''' + Most plugins will delegate their drawing to a 'Theme'. A theme usually consists of an SVG file, one + for each model that is supported, and optionally a fragment of Python for anything that can't + be done with SVG and the built in theme facilities + ''' + self._reload_theme() + + ''' + Most plugins will usually want to draw on the screen. To do so, a 'page' is created. We also supply a callback here to + perform the painting. You can also supply 'on_shown' and 'on_hidden' callbacks here to be notified when your + page actually gets shown and hidden. + + A thumbnail painter function is also provided. This is used by other plugins want a thumbnail representation + of the current screen. For example, this could be used in the 'panel', or the 'menu' plugins + ''' + self.page = g15theme.G15Page("Clock", self.screen, + theme_properties_callback = self._get_properties, + thumbnail_painter = self.paint_thumbnail, panel_painter = self.paint_thumbnail, + theme = self.theme, + originating_plugin = self) + self.page.title = "Simple Clock" + + ''' + Add the page to the screen + ''' + self.screen.add_page(self.page) + + ''' + Once created, we should always ask for the screen to be drawn (even if another higher + priority screen is actually active. If the canvas is not displayed immediately, + the on_shown function will be invoked when it finally is. + ''' + self.screen.redraw(self.page) + + ''' + As this is a Clock, we want to redraw at fixed intervals. So, schedule another redraw + if appropriate + ''' + self._schedule_redraw() + + ''' + We want to be notified when the plugin configuration changed, so watch for gconf events. + The watch function is used, as this will automatically track the monitor handles + and clean them up when the plugin is deactivated + ''' + self.watch(None, self._config_changed) + + def deactivate(self): + g15plugin.G15Plugin.deactivate(self) + + ''' + Stop updating + ''' + if self.timer != None: + self.timer.cancel() + self.timer = None + + ''' + Deactivation occurs when either the plugin is disabled, or the applet is stopped + On deactivate, we must remove our canvas. + ''' + self.screen.del_page(self.page) + + def destroy(self): + ''' + Invoked when the plugin is disabled or the applet is stopped + ''' + pass + + ''' + ************************************************************** + * Common callback functions. For example, your plugin is more* + * than likely to want to draw something on the LCD. Naming * + * the function paint() is the convention * + ************************************************************** + ''' + + ''' + Paint the thumbnail. You are given the MAXIMUM amount of space that is allocated for + the thumbnail, and you must return the amount of space actually take up. Thumbnails + can be used for example by the panel plugin, or the menu plugin. If you want to + support monochrome devices such as the G15, you will have to take into account + the amount of space you have (i.e. 6 pixels high maximum and limited width) + ''' + def paint_thumbnail(self, canvas, allocated_size, horizontal): + if self.page and not self.screen.is_visible(self.page): + properties = self._get_properties() + # Don't display the date or seconds on mono displays, not enough room as it is + if self.screen.driver.get_bpp() == 1: + text = properties["time"] + if self.display_seconds: + text = text[:-3] + font_size = 8 + factor = 2 + font_name = g15globals.fixed_size_font_name + x = 1 + gap = 1 + else: + factor = 1 if horizontal else 2 + font_name = "Sans" + if self.display_date: + text = "%s\n%s" % ( properties["time"],properties["date"] ) + font_size = allocated_size / 3 + else: + text = properties["time"] + font_size = allocated_size / 2 + x = 4 + gap = 8 + + self.text.set_canvas(canvas) + self.text.set_attributes(text, align = pango.ALIGN_CENTER, font_desc = font_name, \ + font_absolute_size = font_size * pango.SCALE / factor) + x, y, width, height = self.text.measure() + if horizontal: + if self.screen.driver.get_bpp() == 1: + y = 0 + else: + y = (allocated_size / 2) - height / 2 + else: + x = (allocated_size / 2) - width / 2 + y = 0 + self.text.draw(x, y) + if horizontal: + return width + gap + else: + return height + 4 + + ''' + *********************************************************** + * Functions specific to plugin * + *********************************************************** + ''' + + def _config_changed(self, client, connection_id, entry, args): + + ''' + Load the gconf configuration + ''' + self._load_configuration() + + ''' + This is called when the gconf configuration changes. See add_notify and remove_notify in + the plugin's activate and deactive functions. + ''' + + ''' + Reload the theme as the layout required may have changed (i.e. with the 'show date' + option has been change) + ''' + self._reload_theme() + self.page.set_theme(self.theme) + + ''' + In this case, we temporarily raise the priority of the page. This will force + the page to be painted (i.e. the paint function invoked). After the specified time, + the page will revert it's priority. Only one revert timer is active at any one time, + so it is safe to call this function in quick succession + ''' + self.screen.set_priority(self.page, g15screen.PRI_HIGH, revert_after = 3.0) + + + ''' + Schedule a redraw as well + ''' + if self.timer is not None: + self.timer.cancel() + self._redraw() + + def _load_configuration(self): + self.display_date = self.gconf_client.get_bool(self.gconf_key + "/display_date") + self.display_seconds = self.gconf_client.get_bool(self.gconf_key + "/display_seconds") + self.use_24hr_format = self.gconf_client.get_bool(self.gconf_key + "/use_24hr_format") + + def _redraw(self): + ''' + Invoked by the timer once a second to redraw the screen. If your page is currently activem + then the paint() functions will now get called. When done, we want to schedule the next + redraw + ''' + self.screen.redraw(self.page) + self._schedule_redraw() + + def _schedule_redraw(self): + if not self.active: + return + + ''' + Determine when to schedule the next redraw for. + ''' + now = datetime.datetime.now() + if self.display_seconds: + next_tick = now + datetime.timedelta(0, 1.0) + next_tick = datetime.datetime(next_tick.year,next_tick.month,next_tick.day,next_tick.hour, next_tick.minute, int(next_tick.second)) + else: + next_tick = now + datetime.timedelta(0, 60.0) + next_tick = datetime.datetime(next_tick.year,next_tick.month,next_tick.day,next_tick.hour, next_tick.minute, 0) + delay = g15pythonlang.total_seconds( next_tick - now ) + + ''' + Try not to create threads or timers if possible. Use g15scheduler.schedule) instead + ''' + self.timer = g15scheduler.schedule("ClockRedraw", delay, self._redraw) + + def _reload_theme(self): + variant = None + if self.display_date: + variant = "with-date" + self.theme = g15theme.G15Theme(os.path.join(os.path.dirname(__file__), "default"), variant) + + ''' + Get the properties dictionary + ''' + def _get_properties(self): + properties = { } + + ''' + Get the details to display and place them as properties which are passed to + the theme + ''' + now = datetime.datetime.now() + if self.use_24hr_format: + properties["time"] = g15locale.format_time_24hour(now, self.gconf_client, self.display_seconds) + else: + properties["time"] = g15locale.format_time(now, self.gconf_client, self.display_seconds) + if self.display_date: + properties["date"] = g15locale.format_date(now, self.gconf_client) + + return properties diff --git a/src/plugins/clock/clock.ui b/src/plugins/clock/clock.ui new file mode 100644 index 0000000..07a2b43 --- /dev/null +++ b/src/plugins/clock/clock.ui @@ -0,0 +1,106 @@ + + + + + + 320 + False + 5 + Clock Preferences + False + True + center-on-parent + dialog + + + True + False + 2 + + + True + False + end + + + gtk-close + True + True + True + True + + + False + False + 0 + + + + + False + True + end + 0 + + + + + True + False + + + Show Seconds + True + True + False + True + + + True + True + 0 + + + + + Show Date + True + True + False + True + + + True + True + 1 + + + + + Show Time in 24 hour format + True + True + False + 0.52999997138977051 + True + + + True + True + 2 + + + + + False + False + 1 + + + + + + button9 + + + diff --git a/src/plugins/clock/default/Makefile.am b/src/plugins/clock/default/Makefile.am new file mode 100644 index 0000000..1ca9a78 --- /dev/null +++ b/src/plugins/clock/default/Makefile.am @@ -0,0 +1,29 @@ +themedir = $(datadir)/gnome15/plugins/clock/default +theme_DATA = default.svg \ + default-with-date.svg \ + g19.svg \ + mx5500.svg \ + mx5500-with-date.svg \ + g19-with-date.svg + +EXTRA_DIST = \ + $(theme_DATA) + +all-local: + for M_LOCALE in @ENABLED_LOCALES@; do \ + mkdir -p i18n/$$M_LOCALE/LC_MESSAGES ; \ + if [ `ls i18n/*.po 2>/dev/null|wc -l` -gt 0 ]; then \ + for M_PO in i18n/*.po; do \ + BN=`basename $$M_PO .po`; \ + LL=`basename $$BN .$$M_LOCALE`.mo; \ + echo "$$M_PO -> $$LL"; \ + msgfmt $$M_PO --output-file i18n/$$M_LOCALE/LC_MESSAGES/$$LL; \ + done; \ + fi; \ + done + +install-exec-hook: + for M_LOCALE in @ENABLED_LOCALES@; do \ + mkdir -p $(DESTDIR)$(datadir)/gnome15/plugins/clock/default/i18n; \ + cp -pR i18n/$$M_LOCALE $(DESTDIR)$(datadir)/gnome15/plugins/clock/default/i18n; \ + done diff --git a/src/plugins/clock/default/default-with-date.svg b/src/plugins/clock/default/default-with-date.svg new file mode 100644 index 0000000..6ff4c4d --- /dev/null +++ b/src/plugins/clock/default/default-with-date.svg @@ -0,0 +1,88 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + ${time} + ${date} + + diff --git a/src/plugins/clock/default/default.svg b/src/plugins/clock/default/default.svg new file mode 100644 index 0000000..694767a --- /dev/null +++ b/src/plugins/clock/default/default.svg @@ -0,0 +1,87 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + ${time} + + diff --git a/src/plugins/clock/default/g19-with-date.svg b/src/plugins/clock/default/g19-with-date.svg new file mode 100644 index 0000000..b4387fb --- /dev/null +++ b/src/plugins/clock/default/g19-with-date.svg @@ -0,0 +1,109 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + ${date} + + ${time} + + diff --git a/src/plugins/clock/default/g19.svg b/src/plugins/clock/default/g19.svg new file mode 100644 index 0000000..3b70445 --- /dev/null +++ b/src/plugins/clock/default/g19.svg @@ -0,0 +1,87 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + ${time} + + diff --git a/src/plugins/clock/default/mx5500-with-date.svg b/src/plugins/clock/default/mx5500-with-date.svg new file mode 100644 index 0000000..48a615f --- /dev/null +++ b/src/plugins/clock/default/mx5500-with-date.svg @@ -0,0 +1,488 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + ${time} + ${date} + + + diff --git a/src/plugins/clock/default/mx5500.svg b/src/plugins/clock/default/mx5500.svg new file mode 100644 index 0000000..aa71e50 --- /dev/null +++ b/src/plugins/clock/default/mx5500.svg @@ -0,0 +1,467 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + ${time} + + diff --git a/src/plugins/clock/i18n/clock.en_GB.po b/src/plugins/clock/i18n/clock.en_GB.po new file mode 100644 index 0000000..259b8d7 --- /dev/null +++ b/src/plugins/clock/i18n/clock.en_GB.po @@ -0,0 +1,30 @@ +# English translations for gnome15-plugins package. +# Copyright (C) 2012 THE gnome15-plugins'S COPYRIGHT HOLDER +# This file is distributed under the same license as the gnome15-plugins package. +# Automatically generated, 2012. +# +msgid "" +msgstr "" +"Project-Id-Version: gnome15-plugins 0.8.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: 2012-01-08 11:23+0000\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: en_GB\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=ASCII\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: i18n/clock.glade.h:1 +msgid "Clock Preferences" +msgstr "Clock Preferences" + +#: i18n/clock.glade.h:2 +msgid "Show Date" +msgstr "Show Date" + +#: i18n/clock.glade.h:3 +msgid "Show Seconds" +msgstr "Show Seconds" diff --git a/src/plugins/clock/i18n/clock.glade.h b/src/plugins/clock/i18n/clock.glade.h new file mode 100644 index 0000000..3b37ec3 --- /dev/null +++ b/src/plugins/clock/i18n/clock.glade.h @@ -0,0 +1,3 @@ +char *s = N_("Clock Preferences"); +char *s = N_("Show Date"); +char *s = N_("Show Seconds"); diff --git a/src/plugins/clock/i18n/clock.pot b/src/plugins/clock/i18n/clock.pot new file mode 100644 index 0000000..7d6561c --- /dev/null +++ b/src/plugins/clock/i18n/clock.pot @@ -0,0 +1,30 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: i18n/clock.glade.h:1 +msgid "Clock Preferences" +msgstr "" + +#: i18n/clock.glade.h:2 +msgid "Show Date" +msgstr "" + +#: i18n/clock.glade.h:3 +msgid "Show Seconds" +msgstr "" diff --git a/src/plugins/debug/Makefile.am b/src/plugins/debug/Makefile.am new file mode 100644 index 0000000..b1b9e8d --- /dev/null +++ b/src/plugins/debug/Makefile.am @@ -0,0 +1,7 @@ +SUBDIRS = default + +plugindir = $(datadir)/gnome15/plugins/debug +plugin_DATA = debug.py + +EXTRA_DIST = \ + $(plugin_DATA) \ No newline at end of file diff --git a/src/plugins/debug/debug.py b/src/plugins/debug/debug.py new file mode 100644 index 0000000..f21867c --- /dev/null +++ b/src/plugins/debug/debug.py @@ -0,0 +1,473 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2012 Brett Smith +# +# 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 . + +import gnome15.g15locale as g15locale +_ = g15locale.get_translation("clock", modfile = __file__).ugettext + +import gnome15.g15text as g15text +import gnome15.g15driver as g15driver +import gnome15.g15globals as g15globals +import gnome15.g15plugin as g15plugin +import gnome15.g15theme as g15theme +import gnome15.util.g15scheduler as g15scheduler +import pango +import os +import sys +import traceback +import gc +import gnome15.objgraph as objgraph +import gnome15.g15logging as g15logging +import dbus.service + +# Logging +import logging +logger = logging.getLogger(__name__) + +id="debug" +name=_("Debug") +description=_("Displays some information useful for debugging Gnome15.\n \ +Also adds additional DBUS functions to inspect internals") +author="Brett Smith " +copyright=_("Copyright (C)2010 Brett Smith") +site="http://www.russo79.com/gnome15" +has_preferences=False +single_instance=True +unsupported_models = [ g15driver.MODEL_G110, g15driver.MODEL_G11, g15driver.MODEL_G930, g15driver.MODEL_G35 ] + +_proc_status = '/proc/%d/status' % os.getpid() + +_scale = {'kB': 1024.0, 'mB': 1024.0*1024.0, + 'KB': 1024.0, 'MB': 1024.0*1024.0} + +DEBUG_NAME="/org/gnome15/Debug" +DEBUG_IF_NAME="org.gnome15.Debug" +EXCLUDED = [ + "instancemethod", + "weakref", + "instance", + "method_descriptor", + "member_descriptor", + "frame", + "function", + "intdict", + "builtin_function_or_method", + "builtin_function_or_method", + ] + +class intdict(dict): + + def __init__(self): + dict.__init__(self) + +class Snapshot(): + + def __init__(self): + self.stats = intdict() + self.objects = intdict() + +def referents_count(typename): + print "%d instances of type %s. Referents :-" % ( objgraph.count(typename), typename) + done = {} + for r in objgraph.by_type(typename): + for o in gc.get_referents(r): + name = _get_key(o) + if name != "type" and name != typename and not name in EXCLUDED and not name in done: + done[name] = True + count = objgraph.count(name) + if count > 1: + print " %s (%d)" % ( name, count ) + +def referents(typename, max_depth = 1): + print "%d instances of type %s. Referents :-" % ( objgraph.count(typename), typename) + for r in objgraph.by_type(typename): + _do_referents(r, 1, max_depth) + +def _do_referents(r, depth, max_depth = 1): + dep = "" + for _ in range(0, depth): + dep += " " + for o in gc.get_referents(r): + if not _get_key(o) in EXCLUDED: + if isinstance(o, dict): + print "%s%s" % (dep, _max_len(o, 120)) + if depth < max_depth: + _do_referents(o, depth + 1) + +def referrers(typename, max_depth = 1): + print "%d instances of type %s. Referrers :-" % ( objgraph.count(typename), typename) + for r in objgraph.by_type(typename): + _do_referrers(r, 1, max_depth, []) + +def _do_referrers(r, depth, max_depth, done): + dep = "" + for _ in range(0, depth): + dep += " " + l = gc.get_referrers(r) + for o in l: + if not o == done and not o == l and not _get_key(o) in EXCLUDED and not o in done: + print "%s%s" % (dep, _max_len(o, 120)) + done.append(o) + if depth < max_depth: + _do_referrers(o, depth + 1, max_depth, done) + +def referrers_count(typename): + print "%d instances of type %s. Referrers :-" % ( objgraph.count(typename), typename) + done = {} + for r in objgraph.by_type(typename): + for o in gc.get_referrers(r): + name = _get_key(o) + if name != "type" and name != typename and not name in EXCLUDED and not name in done: + done[name] = True + count = objgraph.count(name) + if count > 1: + print " %s (%d)" % ( name, count ) + +def take_snapshot(snap_objects = True): + snapshot = Snapshot() + for o in gc.get_objects(): + k = _get_key(o) + if not k in EXCLUDED: + snapshot.stats.setdefault(k, 0) + snapshot.stats[k] += 1 + if snap_objects: + snapshot.objects.setdefault(k, []) + snapshot.objects[k].append(o) + return snapshot + +def compare_snapshots(snapshot1, snapshot2, show_removed = True): + new_types = [] + changed_types = [] + removed_types = [] + + # Find everything that has been removed or changed + for k, v in snapshot1.stats.iteritems(): + if not k in snapshot2.stats: + removed_types.append(k) + else: + if v != snapshot2.stats[k]: + changed_types.append(k) + + # Find everything that has been added + for k, v in snapshot2.stats.iteritems(): + if not k in snapshot1.stats: + new_types.append(k) + + # Print some stuff + print "New types" + _do_types(snapshot1, snapshot2, new_types) + + if show_removed: + print "Removed types" + for k in removed_types: + print " %-30s" % k + + # Find the actual objects that have been added for those that have changed + print "Changed types" + _do_types(snapshot1, snapshot2, changed_types, show_removed) + +def _get_key(o): + if isinstance(o, object): + try: + return o.__class__.__name__ + except: + return type(o).__name__ + else: + return type(o).__name__ + +def _do_types(snapshot1, snapshot2, types, show_removed = True): + for k in types: + print "%4s%-30s %10d (was %d)" % ("",k, snapshot2.stats[k], snapshot1.stats[k] if k in snapshot1.stats else 0) + old_objects = snapshot1.objects[k] if k in snapshot1.objects else [] + new_objects = snapshot2.objects[k] if k in snapshot2.objects else [] + + # Find any objects removed + removed = 0 + if show_removed: + for x in old_objects: + in_new = False + try: + in_new = x in new_objects + except: + pass + if not in_new: + removed += 1 + try : + _do_obj(x, "Removed") + except Exception as e: + print "%12sError! - %s" % ( "", _max_len(str(e), 80) ) + + # Find any objects added + added = 0 + for x in new_objects: + in_old = False + try: + in_old = x in old_objects + except: + pass + if not in_old: + added += 1 + try : + _do_obj(x, "Added") + except: + print "%12sError! - %s" % ( "", _max_len(str(e), 80) ) + + if added > 0 or removed > 0: + print "%4sAdded %d, Removed %d" % ("", added, removed ) + + + +def _do_obj(o, s): + if isinstance(o, list) and len(o) > 0: + # Ignore the list if it contains excluded items + if _get_key(o[0]) in EXCLUDED: + return + elif isinstance(o, dict): + for k, v in dict(o).iteritems(): + if _get_key(k) in EXCLUDED or _get_key(v) in EXCLUDED: + return + break + elif isinstance(o, tuple) and len(o) > 0: + # Ignore the list if it contains excluded items + for v in o: + if _get_key(v) in EXCLUDED: + return + + o_str = _max_len(o, 60) + print "%12s%8s : %-30s %-60s" % ("",s, _get_key(o), o_str) + +def _max_len(o, l): + o_str = str(o) + if len(o_str) > l: + o_str = o_str[:l] + return o_str + +class G15DBUSDebugService(dbus.service.Object): + + def __init__(self, dbus_service): + dbus.service.Object.__init__(self, dbus_service._bus_name, DEBUG_NAME) + self._service = dbus_service._service + self._snapshot1 = None + + @dbus.service.method(DEBUG_IF_NAME) + def Snapshot(self): + logger.info("Collecting garbage") + gc.collect() + logger.info("Collected garbage") + logger.info("Taking snapshot") + _snapshot2 = take_snapshot(False) + logger.info("Taken snapshot") + if self._snapshot1 is not None: + compare_snapshots(self._snapshot1, _snapshot2, show_removed = True) + else: + logger.info("FIRST snapshot taken, take another") + self._snapshot1 = _snapshot2 + + @dbus.service.method(DEBUG_IF_NAME) + def GC(self): + logger.info("Collecting garbage") + gc.collect() + logger.info("Collected garbage") + + @dbus.service.method(DEBUG_IF_NAME) + def ToggleDebugSVG(self): + g15theme.DEBUG_SVG = not g15theme.DEBUG_SVG + + @dbus.service.method(DEBUG_IF_NAME) + def MostCommonTypes(self): + print "Most used objects" + print "-----------------" + print + objgraph.show_most_common_types(limit=200) + print "Job Queues" + print "----------" + print + g15scheduler.scheduler.print_all_jobs() + print "Threads" + print "-------" + for threadId, stack in sys._current_frames().items(): + print "ThreadID: %s" % threadId + for filename, lineno, name, line in traceback.extract_stack(stack): + print ' File: "%s", line %d, in %s' % (filename, lineno, name) + + @dbus.service.method(DEBUG_IF_NAME) + def ShowGraph(self): + objgraph.show_refs(self._service) + + @dbus.service.method(DEBUG_IF_NAME, in_signature='s') + def PluginObject(self, m): + for scr in self._service.screens: + print "Screen %s" % scr.device.uid + for p in scr.plugins.plugin_map: + pmod = scr.plugins.plugin_map[p] + if m == '' or m == pmod.id: + print " %s" % pmod.id + objgraph.show_backrefs(p, filename='%s-%s' %(scr.device.uid, pmod.id)) + + @dbus.service.method(DEBUG_IF_NAME, in_signature='s') + def Objects(self, typename): + print "%d instances of type %s. Referrers :-" % ( objgraph.count(typename), typename) + done = {} + for r in objgraph.by_type(typename): + if isinstance(r, list): + print "%s" % str(r[:min(20, len(r))]) + else: + print "%s" % str(r) + + @dbus.service.method(DEBUG_IF_NAME, in_signature='s') + def SetDebugLevel(self, log_level): + logger = logging.getLogger() + logger.setLevel(g15logging.get_level(log_level)) + + @dbus.service.method(DEBUG_IF_NAME, in_signature='s') + def Referrers(self, typename): + referrers(typename, 1) + + @dbus.service.method(DEBUG_IF_NAME, in_signature='s') + def ReferrersCount(self, typename): + referrers_count(typename) + + @dbus.service.method(DEBUG_IF_NAME, in_signature='s') + def Referents(self, typename): + referents(typename) + + @dbus.service.method(DEBUG_IF_NAME, in_signature='s') + def ReferentsCount(self, typename): + referents_count(typename) + +def _VmB(VmKey): + '''Private. + ''' + global _proc_status, _scale + # get pseudo file /proc//status + try: + t = open(_proc_status) + v = t.read() + t.close() + except: + return 0.0 # non-Linux? + # get VmKey line e.g. 'VmRSS: 9999 kB\n ...' + i = v.index(VmKey) + v = v[i:].split(None, 3) # whitespace + if len(v) < 3: + return 0.0 # invalid format? + # convert Vm value to bytes + return float(v[1]) * _scale[v[2]] + + +def memory(since=0.0): + '''Return memory usage in bytes. + ''' + return _VmB('VmSize:') - since + + +def resident(since=0.0): + '''Return resident memory usage in bytes. + ''' + return _VmB('VmRSS:') - since + + +def stacksize(since=0.0): + '''Return stack size in bytes. + ''' + return _VmB('VmStk:') - since + +def create(gconf_key, gconf_client, screen): + return G15Debug(gconf_key, gconf_client, screen) + +class G15Debug(g15plugin.G15RefreshingPlugin): + + def __init__(self, gconf_key, gconf_client, screen): + g15plugin.G15RefreshingPlugin.__init__(self, gconf_client, gconf_key, screen, ["dialog-error"], id, name, refresh_interval = 1.0) + + def activate(self): + self._debug_service = G15DBUSDebugService(self.screen.service.dbus_service) + self.text = g15text.new_text(self.screen) + self.memory = 0 + self.resident = 0 + self.stack = 0 + self.only_refresh_when_visible = False + g15plugin.G15RefreshingPlugin.activate(self) + self.do_refresh() + + def deactivate(self): + self._silently_remove_from_connector(self._debug_service) + g15plugin.G15RefreshingPlugin.deactivate(self) + + def refresh(self): + self.memory = memory() + self.resident = resident() + self.stack = stacksize() + + def get_theme_properties(self): + properties = g15plugin.G15RefreshingPlugin.get_theme_properties(self) + properties["memory_b"] = "%f" % self.memory + properties["memory_k"] = "%f" % ( self.memory / 1024 ) + properties["memory_mb"] = "%.2f" % ( self.memory / 1024 / 1024 ) + properties["memory_gb"] = "%.2f" % ( self.memory / 1024 / 1024 / 1024 ) + properties["resident_b"] = "%f" % self.resident + properties["resident_k"] = "%f" % ( self.resident / 1024 ) + properties["resident_mb"] = "%.2f" % ( self.resident / 1024 / 1024 ) + properties["resident_gb"] = "%.2f" % ( self.memory / 1024 / 1024 / 1024 ) + properties["stack_b"] = "%f" % self.stack + properties["stack_k"] = "%f" % ( self.stack / 1024 ) + properties["stack_mb"] = "%.2f" % ( self.stack / 1024 / 1024 ) + properties["stack_gb"] = "%.2f" % ( self.stack / 1024 / 1024 / 1024 ) + return properties + + def _silently_remove_from_connector(self, obj): + try: + obj.remove_from_connection() + except Exception: + pass + + def _paint_panel(self, canvas, allocated_size, horizontal): + if self.page and not self.screen.is_visible(self.page): + # Don't display the date or seconds on mono displays, not enough room as it is + mem_mb = self.memory / 1024 / 1024 + res_mb = self.resident / 1024 / 1024 + if self.screen.driver.get_bpp() == 1: + text = "%.2f %.2f" % ( mem_mb, res_mb ) + font_size = 8 + factor = 2 + font_name = g15globals.fixed_size_font_name + x = 1 + gap = 1 + else: + factor = 1 if horizontal else 2 + font_name = "Sans" + text = "%.2f MiB\n%.2f MiB" % ( mem_mb, res_mb ) + font_size = allocated_size / 3 + x = 4 + gap = 8 + + self.text.set_canvas(canvas) + self.text.set_attributes(text, align = pango.ALIGN_CENTER, font_desc = font_name, \ + font_absolute_size = font_size * pango.SCALE / factor) + x, y, width, height = self.text.measure() + if horizontal: + if self.screen.driver.get_bpp() == 1: + y = 0 + else: + y = (allocated_size / 2) - height / 2 + else: + x = (allocated_size / 2) - width / 2 + y = 0 + self.text.draw(x, y) + if horizontal: + return width + gap + else: + return height + 4 + diff --git a/src/plugins/debug/default/Makefile.am b/src/plugins/debug/default/Makefile.am new file mode 100644 index 0000000..8f18692 --- /dev/null +++ b/src/plugins/debug/default/Makefile.am @@ -0,0 +1,25 @@ +themedir = $(datadir)/gnome15/plugins/debug/default +theme_DATA = default.svg \ + g19.svg + +EXTRA_DIST = \ + $(theme_DATA) + +all-local: + for M_LOCALE in @ENABLED_LOCALES@; do \ + mkdir -p i18n/$$M_LOCALE/LC_MESSAGES ; \ + if [ `ls i18n/*.po 2>/dev/null|wc -l` -gt 0 ]; then \ + for M_PO in i18n/*.po; do \ + BN=`basename $$M_PO .po`; \ + LL=`basename $$BN .$$M_LOCALE`.mo; \ + echo "$$M_PO -> $$LL"; \ + msgfmt $$M_PO --output-file i18n/$$M_LOCALE/LC_MESSAGES/$$LL; \ + done; \ + fi; \ + done + +install-exec-hook: + for M_LOCALE in @ENABLED_LOCALES@; do \ + mkdir -p $(DESTDIR)$(datadir)/gnome15/plugins/debug/default/i18n; \ + cp -pR i18n/$$M_LOCALE $(DESTDIR)$(datadir)/gnome15/plugins/debug/default/i18n; \ + done diff --git a/src/plugins/debug/default/default.svg b/src/plugins/debug/default/default.svg new file mode 100644 index 0000000..2685f16 --- /dev/null +++ b/src/plugins/debug/default/default.svg @@ -0,0 +1,132 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + Memory + Resident + Stack + ${memory_mb} MiB + ${resident_mb} MiB + ${stack_mb} MiB + + diff --git a/src/plugins/debug/default/g19.svg b/src/plugins/debug/default/g19.svg new file mode 100644 index 0000000..6843f8c --- /dev/null +++ b/src/plugins/debug/default/g19.svg @@ -0,0 +1,131 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + Memory: + ${memory_mb} MiB + Resident: + ${resident_mb} MiB + Stack: + ${stack_mb} MiB + + diff --git a/src/plugins/debug/i18n/clock.en_GB.po b/src/plugins/debug/i18n/clock.en_GB.po new file mode 100644 index 0000000..259b8d7 --- /dev/null +++ b/src/plugins/debug/i18n/clock.en_GB.po @@ -0,0 +1,30 @@ +# English translations for gnome15-plugins package. +# Copyright (C) 2012 THE gnome15-plugins'S COPYRIGHT HOLDER +# This file is distributed under the same license as the gnome15-plugins package. +# Automatically generated, 2012. +# +msgid "" +msgstr "" +"Project-Id-Version: gnome15-plugins 0.8.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: 2012-01-08 11:23+0000\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: en_GB\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=ASCII\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: i18n/clock.glade.h:1 +msgid "Clock Preferences" +msgstr "Clock Preferences" + +#: i18n/clock.glade.h:2 +msgid "Show Date" +msgstr "Show Date" + +#: i18n/clock.glade.h:3 +msgid "Show Seconds" +msgstr "Show Seconds" diff --git a/src/plugins/debug/i18n/clock.glade.h b/src/plugins/debug/i18n/clock.glade.h new file mode 100644 index 0000000..3b37ec3 --- /dev/null +++ b/src/plugins/debug/i18n/clock.glade.h @@ -0,0 +1,3 @@ +char *s = N_("Clock Preferences"); +char *s = N_("Show Date"); +char *s = N_("Show Seconds"); diff --git a/src/plugins/debug/i18n/clock.pot b/src/plugins/debug/i18n/clock.pot new file mode 100644 index 0000000..7d6561c --- /dev/null +++ b/src/plugins/debug/i18n/clock.pot @@ -0,0 +1,30 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: i18n/clock.glade.h:1 +msgid "Clock Preferences" +msgstr "" + +#: i18n/clock.glade.h:2 +msgid "Show Date" +msgstr "" + +#: i18n/clock.glade.h:3 +msgid "Show Seconds" +msgstr "" diff --git a/src/plugins/display/Makefile.am b/src/plugins/display/Makefile.am new file mode 100644 index 0000000..a02c839 --- /dev/null +++ b/src/plugins/display/Makefile.am @@ -0,0 +1,5 @@ +plugindir = $(datadir)/gnome15/plugins/display +plugin_DATA = display.py + +EXTRA_DIST = \ + $(plugin_DATA) \ No newline at end of file diff --git a/src/plugins/display/display.py b/src/plugins/display/display.py new file mode 100644 index 0000000..3f56389 --- /dev/null +++ b/src/plugins/display/display.py @@ -0,0 +1,186 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2012 Brett Smith +# +# 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 . + +import gnome15.g15locale as g15locale +_ = g15locale.get_translation("profiles", modfile = __file__).ugettext + +import gnome15.g15driver as g15driver +import gnome15.g15theme as g15theme +import gnome15.g15plugin as g15plugin +import gnome15.g15devices as g15devices +import gnome15.g15actions as g15actions +import gnome15.util.g15scheduler as g15scheduler +import gnome15.util.g15pythonlang as g15pythonlang +import gnome15.util.g15icontools as g15icontools +import logging +import os +import re +logger = logging.getLogger(__name__) + +ICONS = [ "display", "gnome-display-properties", "system-config-display", "video-display", "xfce4-display", "display-capplet" ] + +# Custom actions +SELECT_PROFILE = "select-profile" + +# Register the action with all supported models +g15devices.g15_action_keys[SELECT_PROFILE] = g15actions.ActionBinding(SELECT_PROFILE, [ g15driver.G_KEY_L1 ], g15driver.KEY_STATE_HELD) +g15devices.z10_action_keys[SELECT_PROFILE] = g15actions.ActionBinding(SELECT_PROFILE, [ g15driver.G_KEY_L1 ], g15driver.KEY_STATE_HELD) +g15devices.g19_action_keys[SELECT_PROFILE] = g15actions.ActionBinding(SELECT_PROFILE, [ g15driver.G_KEY_BACK ], g15driver.KEY_STATE_HELD) + +# Plugin details - All of these must be provided +id="display" +name=_("Display Resolution") +description=_("Allows selection of the resolution for your display.") +author="Brett Smith " +copyright=_("Copyright (C)2012 Brett Smith") +site="http://www.russo79.com/gnome15" +has_preferences=False +default_enabled=True +unsupported_models = [ g15driver.MODEL_G110, g15driver.MODEL_G11, g15driver.MODEL_MX5500, g15driver.MODEL_G930, g15driver.MODEL_G35 ] +actions={ + g15driver.PREVIOUS_SELECTION : _("Previous item"), + g15driver.NEXT_SELECTION : _("Next item"), + g15driver.NEXT_PAGE : _("Next page"), + g15driver.PREVIOUS_PAGE : _("Previous page"), + g15driver.SELECT : _("Select resolution") + } + +def create(gconf_key, gconf_client, screen): + return G15XRandR(gconf_client, gconf_key, screen) + +""" +Represents a resolution as a single item in a menu +""" +class ResolutionMenuItem(g15theme.MenuItem): + def __init__(self, index, size, refresh_rate, plugin, id, display): + g15theme.MenuItem.__init__(self, id, group = False) + self.current = False + self.size = size + self.index = index + self.refresh_rate = refresh_rate + self._plugin = plugin + self.display = display + + def get_theme_properties(self): + item_properties = g15theme.MenuItem.get_theme_properties(self) + item_properties["item_name"] = "%s x %s @ %s" % ( self.size[0], self.size[1], self.refresh_rate) + item_properties["item_radio"] = True + item_properties["item_radio_selected"] = self.current + item_properties["item_alt"] = "" + return item_properties + + def activate(self): + os.system("xrandr --auto --output %s --mode %sx%s -r %s" % (self.display, self.size[0], self.size[1], self.refresh_rate )) + self._plugin._reload_menu() + + +""" +XRANDR plugin class +""" +class G15XRandR(g15plugin.G15MenuPlugin): + + def __init__(self, gconf_client, gconf_key, screen): + g15plugin.G15MenuPlugin.__init__(self, gconf_client, gconf_key, screen, ICONS, id, _("Display")) + + def activate(self): + self._current_active = None + self._last_items = -1 + g15plugin.G15MenuPlugin.activate(self) + + def deactivate(self): + g15plugin.G15MenuPlugin.deactivate(self) + + def load_menu_items(self): + items = [] + display = "Default" + i = 0 + status, output = self._get_status_output("xrandr") + if status == 0: + old_active = self._current_active + new_active = [] + for line in output.split('\n'): + arr = re.findall(r'\S+', line) + if line.startswith(" "): + size = self._parse_size(arr[0]) + for a in range(1, len(arr)): + word = arr[a] + refresh_string = ''.join( c for c in word if c not in '*+' ) + if len(refresh_string) > 0: + refresh_rate = float(refresh_string) + i += 1 + item = ResolutionMenuItem(i, size, refresh_rate, self, "profile-%d-%s" % ( i, refresh_rate ), display ) + item.current = "*" in word + items.append(item) + if item.current: + new_active.append(item) + elif "connected" in line: + i += 1 + display = arr[0] + item = g15theme.MenuItem("display-%s" % i, True, arr[0], activatable=False, icon = g15icontools.get_icon_path(ICONS)) + items.append(item) + + + if len(items) != self._last_items or old_active is None or self._differs(new_active, old_active): + self.menu.set_children(items) + self._last_items = len(items) + + if new_active is not None: + self.menu.set_selected_item(new_active[0]) + self._current_active = new_active + + self._schedule_check() + else: + raise Exception("Failed to query XRandR. Is the xrandr command installed, and do you have the XRandR extension enabled in your X configuration?") + + ''' + Private + ''' + def _differs(self, old_active, new_active): + if ( old_active is None and new_active is not None ) or \ + ( old_active is not None and new_active is None ): + return True + if old_active is not None: + it = iter(old_active) + try: + for i in new_active: + if i.id != next(it).id: + return True + except StopIteration: + return True + + def _schedule_check(self): + if self.active == True: + g15scheduler.schedule("CheckResolution", 10.0, self.load_menu_items) + + def _parse_size(self, line): + arr = line.split("x") + return int(arr[0].strip()), int(arr[1].strip()) + + def _reload_menu(self): + self.load_menu_items() + self.screen.redraw(self.page) + + def _get_item_for_current_resolution(self): + return g15pythonlang.find(lambda m: m.current, self.menu.get_children()) + + def _get_status_output(self, cmd): + # TODO something like this is used in sense.py as well, make it a utility + pipe = os.popen('{ ' + cmd + '; } 2>/dev/null', 'r') + text = pipe.read() + sts = pipe.close() + if sts is None: sts = 0 + if text[-1:] == '\n': text = text[:-1] + return sts, text \ No newline at end of file diff --git a/src/plugins/fx/Makefile.am b/src/plugins/fx/Makefile.am new file mode 100644 index 0000000..6dd75c4 --- /dev/null +++ b/src/plugins/fx/Makefile.am @@ -0,0 +1,6 @@ +plugindir = $(datadir)/gnome15/plugins/fx +plugin_DATA = fx.ui \ + fx.py + +EXTRA_DIST = \ + $(plugin_DATA) \ No newline at end of file diff --git a/src/plugins/fx/fx.py b/src/plugins/fx/fx.py new file mode 100644 index 0000000..bf10c07 --- /dev/null +++ b/src/plugins/fx/fx.py @@ -0,0 +1,207 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2010 Brett Smith +# +# 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 . + +import gnome15.g15locale as g15locale +_ = g15locale.get_translation("fx", modfile = __file__).ugettext + +import gnome15.g15screen as g15screen +import gnome15.g15driver as g15driver +import gnome15.util.g15uigconf as g15uigconf +import gtk +import os +import time +import cairo +import random + + +# Plugin details - All of these must be provided +id="fx" +name=_("Special Effect") +description=_("This plugin introduces special effects when switching between screens. \ +Currently 3 main types of effect are provided, a sliding effect (in both directions) \ +a fading effect and a zoom effect . On a monochrome LCD such as the G15's, the fade appears as more \ +of a 'disolve' effect.") +author="Brett Smith " +copyright=_("Copyright (C)2010 Brett Smith") +site="http://www.russo79.com/gnome15" +has_preferences=True +unsupported_models = [ g15driver.MODEL_G110, g15driver.MODEL_G11, g15driver.MODEL_G930, g15driver.MODEL_G35 ] + +def create(gconf_key, gconf_client, screen): + return G15Fx(gconf_key, gconf_client, screen) + +def show_preferences(parent, driver, gconf_client, gconf_key): + widget_tree = gtk.Builder() + widget_tree.add_from_file(os.path.join(os.path.dirname(__file__), "fx.ui")) + dialog = widget_tree.get_object("FxDialog") + dialog.set_transient_for(parent) + g15uigconf.configure_combo_from_gconf(gconf_client, gconf_key + "/transition_effect", "TransitionCombo", "random", widget_tree) + g15uigconf.configure_adjustment_from_gconf(gconf_client, gconf_key + "/anim_speed", "AnimationSpeedAdjustment", 5.0, widget_tree) + dialog.run() + dialog.hide() + +effects = [ "vertical-scroll", "horizontal-scroll", "fade", "zoom" ] + +class G15Fx(): + + def __init__(self, gconf_key, gconf_client, screen): + self.screen = screen + self.gconf_client = gconf_client + self.gconf_key = gconf_key + + def activate(self): + self.chained_transition =self.screen.set_transition(self.transition) + self.notify_handler = self.gconf_client.notify_add(self.gconf_key, self.config_changed) + + def deactivate(self): + self.gconf_client.notify_remove(self.notify_handler) + self.screen.set_transition(self.chained_transition) + + def destroy(self): + pass + + ''' Callbacks + ''' + + def config_changed(self, client, connection_id, entry, args): + self.screen.redraw() + + + def transition(self, old_surface, new_surface, old_page, new_page, direction="up"): + # Determine effect to use + effect = self.gconf_client.get_string(self.gconf_key + "/transition_effect") + if effect == "": + effect = "random" + if effect == "random": + effect = effects[int(random.random() * len(effects))] + + # Animation speed + speed_entry = self.gconf_client.get(self.gconf_key + "/anim_speed") + speed = 5.0 if speed_entry == None else speed_entry.get_float() + + # Don't transition for high priority screens + if new_page == None or old_page == None or new_page.priority == g15screen.PRI_HIGH: + return + + width = self.screen.width + height = self.screen.height + + # Create a working surface + img_surface = cairo.ImageSurface (cairo.FORMAT_ARGB32, self.screen.width, self.screen.height) + img_context = cairo.Context(img_surface) + if effect == "vertical-scroll": + # Vertical scroll + step = max( int(speed), 1 ) + if direction == "down": + for i in range(0, self.screen.height, step): + img_context.save() + img_context.translate(0, -i) + img_context.set_source_surface(old_surface) + img_context.paint() + img_context.translate(0, self.screen.height) + img_context.set_source_surface(new_surface) + img_context.paint() + img_context.restore() + self.screen.driver.paint(img_surface) + self.anim_delay(speed) + else: + for i in range(0, self.screen.height, step): + img_context.save() + img_context.translate(0, -(self.screen.height - i)) + img_context.set_source_surface(new_surface) + img_context.paint() + img_context.translate(0, self.screen.height) + img_context.set_source_surface(old_surface) + img_context.paint() + img_context.restore() + self.screen.driver.paint(img_surface) + self.anim_delay(speed) + + elif effect == "horizontal-scroll": + # Horizontal scroll + step = max( ( width / height ) * speed, 1 ) + if direction == "down": + for i in range(0, self.screen.width, int(step)): + img_context.save() + img_context.translate(-i, 0) + img_context.set_source_surface(old_surface) + img_context.paint() + img_context.translate(self.screen.width, 0) + img_context.set_source_surface(new_surface) + img_context.paint() + img_context.restore() + self.screen.driver.paint(img_surface) + self.anim_delay(speed) + else: + for i in range(0, self.screen.width, int(step)): + img_context.save() + img_context.translate(-(self.screen.width - i), 0) + img_context.set_source_surface(new_surface) + img_context.paint() + img_context.translate(self.screen.width, 0) + img_context.set_source_surface(old_surface) + img_context.paint() + img_context.restore() + self.screen.driver.paint(img_surface) + self.anim_delay(speed) + elif effect == "fade": + step = max( int(speed), 1 ) + for i in range(0, 256, step): + img_context.set_source_surface(new_surface) + img_context.paint_with_alpha(float(i) / 256.0) + img_context.set_source_surface(old_surface) + img_context.paint_with_alpha(1.0 - ( float(i) / 256.0 ) ) + self.screen.driver.paint(img_surface) + self.anim_delay(speed) + elif effect == "zoom": + step = max( int(speed), 1 ) + if direction == "down": + for i in range(1, self.screen.width, step): + img_context.save() + img_context.set_source_surface(old_surface) + img_context.paint() + scale = i / float(self.screen.width) + scaled_width = self.screen.width * scale + scaled_height = self.screen.height * scale + img_context.translate( ( self.screen.width - scaled_width) / 2, ( self.screen.height - scaled_height) / 2) + img_context.scale(scale, scale) + img_context.set_source_surface(new_surface) + img_context.paint() + img_context.restore() + self.screen.driver.paint(img_surface) + self.anim_delay(speed) + else: + for i in range(self.screen.width, 0, step * -1): + img_context.save() + img_context.set_source_surface(new_surface) + img_context.paint() + scale = i / float(self.screen.width) + scaled_width = self.screen.width * scale + scaled_height = self.screen.height * scale + img_context.translate( ( self.screen.width - scaled_width) / 2, ( self.screen.height - scaled_height) / 2) + img_context.scale(scale, scale) + img_context.set_source_surface(old_surface) + img_context.paint() + img_context.restore() + self.screen.driver.paint(img_surface) + self.anim_delay(speed) + + if self.chained_transition != None: + self.chained_transition(old_surface, new_surface, old_page, new_page, direction) + + def anim_delay(self, speed): + if speed < 1.0: + time.sleep( ( 1.0 - speed ) / 50.0 ) \ No newline at end of file diff --git a/src/plugins/fx/fx.ui b/src/plugins/fx/fx.ui new file mode 100644 index 0000000..ed809bd --- /dev/null +++ b/src/plugins/fx/fx.ui @@ -0,0 +1,192 @@ + + + + + + 50 + 1 + 10 + 5 + + + + + + + + + + + horizontal-scroll + Horizontal Slide + + + vertical-scroll + Vertical Slide + + + fade + Fade + + + zoom + Zoom + + + random + Random + + + none + None + + + + + 320 + False + 5 + Special Effects Preferences + False + True + center-on-parent + dialog + + + True + False + 2 + + + True + False + end + + + gtk-close + True + True + True + True + + + False + False + 0 + + + + + False + True + end + 0 + + + + + True + False + + + True + False + 0 + none + + + True + False + 12 + + + True + False + + + True + False + TransitionModel + + + + 1 + + + + + True + True + 0 + + + + + True + False + 8 + + + True + False + Animation Speed + + + False + False + 0 + + + + + True + True + AnimationSpeedAdjustment + + + True + True + 1 + + + + + True + True + 4 + 1 + + + + + + + + + True + False + <b>Transitions</b> + True + + + + + True + True + 0 + + + + + False + False + 1 + + + + + + button9 + + + diff --git a/src/plugins/fx/i18n/fx.en_GB.po b/src/plugins/fx/i18n/fx.en_GB.po new file mode 100644 index 0000000..1152e79 --- /dev/null +++ b/src/plugins/fx/i18n/fx.en_GB.po @@ -0,0 +1,54 @@ +# English translations for gnome15-plugins package. +# Copyright (C) 2012 THE gnome15-plugins'S COPYRIGHT HOLDER +# This file is distributed under the same license as the gnome15-plugins package. +# Automatically generated, 2012. +# +msgid "" +msgstr "" +"Project-Id-Version: gnome15-plugins 0.8.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: 2012-01-08 11:23+0000\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: en_GB\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=ASCII\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: i18n/fx.glade.h:1 +msgid "Transitions" +msgstr "Transitions" + +#: i18n/fx.glade.h:2 +msgid "Animation Speed" +msgstr "Animation Speed" + +#: i18n/fx.glade.h:3 +msgid "Fade" +msgstr "Fade" + +#: i18n/fx.glade.h:4 +msgid "Horizontal Slide" +msgstr "Horizontal Slide" + +#: i18n/fx.glade.h:5 +msgid "None" +msgstr "None" + +#: i18n/fx.glade.h:6 +msgid "Random" +msgstr "Random" + +#: i18n/fx.glade.h:7 +msgid "Special Effects Preferences" +msgstr "Special Effects Preferences" + +#: i18n/fx.glade.h:8 +msgid "Vertical Slide" +msgstr "Vertical Slide" + +#: i18n/fx.glade.h:9 +msgid "Zoom" +msgstr "Zoom" diff --git a/src/plugins/fx/i18n/fx.glade.h b/src/plugins/fx/i18n/fx.glade.h new file mode 100644 index 0000000..ef9412c --- /dev/null +++ b/src/plugins/fx/i18n/fx.glade.h @@ -0,0 +1,9 @@ +char *s = N_("Transitions"); +char *s = N_("Animation Speed"); +char *s = N_("Fade"); +char *s = N_("Horizontal Slide"); +char *s = N_("None"); +char *s = N_("Random"); +char *s = N_("Special Effects Preferences"); +char *s = N_("Vertical Slide"); +char *s = N_("Zoom"); diff --git a/src/plugins/fx/i18n/fx.pot b/src/plugins/fx/i18n/fx.pot new file mode 100644 index 0000000..8b2e12c --- /dev/null +++ b/src/plugins/fx/i18n/fx.pot @@ -0,0 +1,54 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: i18n/fx.glade.h:1 +msgid "Transitions" +msgstr "" + +#: i18n/fx.glade.h:2 +msgid "Animation Speed" +msgstr "" + +#: i18n/fx.glade.h:3 +msgid "Fade" +msgstr "" + +#: i18n/fx.glade.h:4 +msgid "Horizontal Slide" +msgstr "" + +#: i18n/fx.glade.h:5 +msgid "None" +msgstr "" + +#: i18n/fx.glade.h:6 +msgid "Random" +msgstr "" + +#: i18n/fx.glade.h:7 +msgid "Special Effects Preferences" +msgstr "" + +#: i18n/fx.glade.h:8 +msgid "Vertical Slide" +msgstr "" + +#: i18n/fx.glade.h:9 +msgid "Zoom" +msgstr "" diff --git a/src/plugins/g15daemon-server/Makefile.am b/src/plugins/g15daemon-server/Makefile.am new file mode 100644 index 0000000..a97dc96 --- /dev/null +++ b/src/plugins/g15daemon-server/Makefile.am @@ -0,0 +1,6 @@ +plugindir = $(datadir)/gnome15/plugins/g15daemon-server +plugin_DATA = g15daemon-server.ui \ + g15daemon-server.py + +EXTRA_DIST = \ + $(plugin_DATA) \ No newline at end of file diff --git a/src/plugins/g15daemon-server/g15daemon-server.py b/src/plugins/g15daemon-server/g15daemon-server.py new file mode 100644 index 0000000..af9dedc --- /dev/null +++ b/src/plugins/g15daemon-server/g15daemon-server.py @@ -0,0 +1,493 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2011 Brett Smith +# +# 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 . + +from threading import Thread +from PIL import Image +from PIL import ImageMath +from PIL import ImageOps +import array +import asyncore +import cairo +import gnome15.g15driver as g15driver +import gnome15.g15locale as g15locale +import gnome15.g15screen as g15screen +import gnome15.g15theme as g15theme +import gnome15.util.g15convert as g15convert +import gnome15.util.g15uigconf as g15uigconf +import gnome15.util.g15gconf as g15gconf +import gnome15.util.g15cairo as g15cairo +import gobject +import gtk +import logging +import os +import socket +import struct +import sys +_ = g15locale.get_translation("g15daemon-server", modfile = __file__).ugettext + +logger = logging.getLogger(__name__) + +# Plugin details - All of these must be provided +id="g15daemon-server" +name=_("G15Daemon Compatibility") +description=_("Starts a network server compatible with the g15daemon network protocol. \ +This allows you to use g15daemon compatible scripts and applications on all \ +models supported by Gnome15, including the G19. Note, if you are using \ +a real g15daemon server, you will configure this plugin to use a different \ +port.") +author="Brett Smith " +copyright=_("Copyright (C)2010 Brett Smith") +site="http://www.russo79.com/gnome15" +has_preferences=True +needs_network=True +unsupported_models = [ g15driver.MODEL_G930, g15driver.MODEL_G35 ] + +# Client commands +CLIENT_CMD_GET_KEYSTATE=ord('k') +CLIENT_CMD_SWITCH_PRIORITIES=ord('p') +CLIENT_CMD_NEVER_SELECT=ord('n') +CLIENT_CMD_IS_FOREGROUND=ord('v') +CLIENT_CMD_IS_USER_SELECTED=ord('u') +CLIENT_CMD_KB_BACKLIGHT_COLOR=0x79 +CLIENT_CMD_BACKLIGHT=0x80 +CLIENT_CMD_KB_BACKLIGHT=0x8 +CLIENT_CMD_CONTRAST=0x40 +CLIENT_CMD_MKEY_LIGHTS=0x20 + + +KEY_MAP = { + g15driver.G_KEY_G1 : 1<<0, + g15driver.G_KEY_G2 : 1<<1, + g15driver.G_KEY_G3 : 1<<2, + g15driver.G_KEY_G4 : 1<<3, + g15driver.G_KEY_G5 : 1<<4, + g15driver.G_KEY_G6 : 1<<5, + g15driver.G_KEY_G7 : 1<<6, + g15driver.G_KEY_G8 : 1<<7, + g15driver.G_KEY_G9 : 1<<8, + g15driver.G_KEY_G10 : 1<<9, + g15driver.G_KEY_G11 : 1<<10, + g15driver.G_KEY_G12 : 1<<11, + + g15driver.G_KEY_M1 : 1<<18, + g15driver.G_KEY_M2 : 1<<19, + g15driver.G_KEY_M3 : 1<<20, + g15driver.G_KEY_MR : 1<<21, + + # L1-L5 + g15driver.G_KEY_L1 : 1<<22, + g15driver.G_KEY_L2 : 1<<23, + g15driver.G_KEY_L3 : 1<<24, + g15driver.G_KEY_L4 : 1<<25, + g15driver.G_KEY_L5 : 1<<26, + + # Map to L1-L5 + g15driver.G_KEY_UP : 1<<22, + g15driver.G_KEY_LEFT : 1<<23, + g15driver.G_KEY_OK : 1<<24, + g15driver.G_KEY_RIGHT : 1<<25, + g15driver.G_KEY_DOWN : 1<<26, + + g15driver.G_KEY_LIGHT : 1<<27 + } + +def create(gconf_key, gconf_client, screen): + return G15DaemonServer(gconf_key, gconf_client, screen) + +def show_preferences(parent, driver, gconf_client, gconf_key): + widget_tree = gtk.Builder() + widget_tree.add_from_file(os.path.join(os.path.dirname(__file__), "g15daemon-server.ui")) + + dialog = widget_tree.get_object("G15DaemonServerDialog") + dialog.set_transient_for(parent) + + g15uigconf.configure_adjustment_from_gconf(gconf_client, gconf_key + "/port", "PortAdjustment", 15550, widget_tree) + g15uigconf.configure_checkbox_from_gconf(gconf_client, gconf_key + "/keep_aspect_ratio", "KeepAspectRatio", False, widget_tree, True) + g15uigconf.configure_checkbox_from_gconf(gconf_client, gconf_key + "/take_over_macro_keys", "TakeOverMacroKeys", True, widget_tree, True) + + g15uigconf.configure_checkbox_from_gconf(gconf_client, gconf_key + "/use_custom_foreground", "UseCustomForeground", False, widget_tree) + g15uigconf.configure_colorchooser_from_gconf(gconf_client, gconf_key + "/custom_foreground", "CustomForeground", ( 255, 255, 255 ), widget_tree) + + dialog.run() + dialog.hide() + +class G15DaemonClient(asyncore.dispatcher): + def __init__(self, conn, plugin): + asyncore.dispatcher.__init__(self, sock=conn) + self.out_buffer = "" + self.img_buffer = None + self.buffer_type = None + self.buffer_len = 0 + self.surface = None + self.last_img_buffer = None + self.enable_keys = False + self.plugin = plugin + self.plugin.join(self) + self.handshake = False + self.keyboard_backlight_value = None + self.backlight_value = None + + self.page = g15theme.G15Page("G15Daemon%d" % self.plugin.screen_index, plugin.screen, painter = self._paint, on_shown = self._on_shown, on_hidden = self._on_hidden, originating_plugin = self) + self.page.set_title(_("G15Daemon Screen %d") % self.plugin.screen_index) + self.plugin.screen.add_page(self.page) + self.plugin.screen_index += 1 + self.plugin.screen.redraw(self.page) + self.backlight_acquire = None + self.keyboard_backlight_acquire = None + + self.out_buffer += "G15 daemon HELLO" + self.oob_buffer = "" + + def handle_close(self): + self.plugin.screen.del_page(self.page) + self.plugin.leave(self) + self.close() + + def handle_expt(self): + data = self.socket.recv(1, socket.MSG_OOB) + val = ord(data[0]) + + if val == CLIENT_CMD_SWITCH_PRIORITIES: + self.plugin.screen.raise_page(self.page) + elif val == CLIENT_CMD_NEVER_SELECT: + self.plugin.screen.set_priority(self, self.page, g15screen.PRI_LOW) + elif val == CLIENT_CMD_IS_FOREGROUND: + self.oob_buffer += "1" if self.plugin.screen.get_visible_page() == self.page else "0" + elif val == CLIENT_CMD_IS_USER_SELECTED: + self.oob_buffer += "1" if self.plugin.screen.get_visible_page() == self.page and self.page.priority == g15screen.PRI_NORMAL else "0" + elif val & CLIENT_CMD_MKEY_LIGHTS > 0: + self.screen.driver.set_value(val - CLIENT_CMD_MKEY_LIGHTS) + elif val & CLIENT_CMD_KEY_HANDLER > 0: + # TODO - the semantics are slightly different here. gnome15 is already grabbing the keyboard, always. + # So instead, we just only send keyboard events if the client requests this. + self.enable_keys = True + elif val & CLIENT_CMD_BACKLIGHT: + level = val - CLIENT_CMD_BACKLIGHT + if isinstance(self.plugin.default_backlight, int): + # Others + bl = level + else: + # G19 + if level == 0: + bl = 0 + elif level == 1: + bl = self.plugin.default_lcd_brightness / 2 + elif level == 2: + bl = self.plugin.default_lcd_brightness + if self.backlight_acquire: + self.backlight_value = bl + self.backlight_acquire.set_value(bl) + else: + logger.warning("g15daemon client requested backlight be changed, but there is no backlight to change") + elif val & CLIENT_CMD_KB_BACKLIGHT: + level = val - CLIENT_CMD_KB_BACKLIGHT + + if isinstance(self.plugin.default_backlight, int): + # Others + bl = level + else: + # G19 + if level == 0: + bl = (0, 0, 0) + elif level == 1: + bl = ( self.plugin.default_backlight[0] / 2, self.plugin.default_backlight[1] / 2, self.plugin.default_backlight[2] / 2 ) + elif level == 2: + bl = self.plugin.default_backlight + + if self.keyboard_backlight_acquire: + self.keyboard_backlight_value = bl + self.keyboard_backlight_acquire.set_value(bl) + else: + logger.warning("g15daemon client requested keyboard backlight be changed, but there is no backlight to change") + elif val & CLIENT_CMD_CONTRAST: + logger.warning("Ignoring contrast command") + + + def handle_read(self): + if not self.handshake: + buf_type = self.recv(4) + + self.buffer_type = buf_type[0] + self.handshake = True + self.img_buffer = "" + + if self.buffer_type == "G": + self.buffer_len = 6880 + elif self.buffer_type == "R": + self.buffer_len = 1048 +# TODO +# elif self.buffer_type == "W": +# self.buffer_len = 865 + else: + logger.warning("WARNING: Unsupported buffer type. Closing") + self.handle_close() + return + else: + recv = self.recv(self.buffer_len - len(self.img_buffer)) + self.img_buffer += recv + if len(self.img_buffer) == self.buffer_len: + if self.buffer_type == "G": + self.img_buffer = self.convert_gbuf(self.img_buffer) + self.draw_buffer(self.img_buffer) + self.last_img_buffer = self.img_buffer + self.img_buffer = "" + + elif len(self.img_buffer) > self.buffer_len: + logger.warning("Received bad frame (%d bytes), should be %d", + len(self.img_buffer), + self.buffer_len) + + def draw_buffer(self, img_buffer): + + pil_img = Image.fromstring("1", (160,43), img_buffer) + mask_img = pil_img.copy() + mask_img = mask_img.convert("L") + pil_img = pil_img.convert("P") + if self.plugin.palette is not None: + pil_img.putpalette(self.plugin.palette) + pil_img = pil_img.convert("RGBA") + pil_img.putalpha(mask_img) + + # TODO find a quicker way of converting + pixbuf = g15cairo.image_to_pixbuf(pil_img, "png") + self.surface = g15cairo.pixbuf_to_surface(pixbuf) + self.plugin.screen.redraw(self.page) + + def dump_buf(self, buf): + i = 0 + for y in range(43): + l = "" + for x in range(160): + if buf[ ( y * 160 ) + x ] == chr(0): + l += " " + else: + l += "*" + logger.info(l) + + def convert_gbuf(self, g_buffer): + r_buffer = array.array('c', chr(0)*1048) + new_buf = "" + for x in range(160): + for y in range(43): + pixel_offset = y * 160 + x + byte_offset = pixel_offset / 8 + bit_offset = 7 - (pixel_offset % 8) + val = ord(g_buffer[x + ( y * 160)]) + if val: + r_buffer[byte_offset] = chr(ord(r_buffer[byte_offset]) | 1 << bit_offset) + else: + r_buffer[byte_offset] = chr(ord(r_buffer[byte_offset]) & ~(1 << bit_offset)) + return r_buffer.tostring() + + def convert_rbuf(self, buffer): + new_buf = "" + for x in range(160): + for y in range(43): + pixel_offset = y * 143 + x + byte_offset = pixel_offset / 8 + bit_offset = 7 - (pixel_offset % 8) + ch = ord(buffer[byte_offset]) + new_buf += chr( ( ch >> bit_offset ) & 0xfe ) + + return new_buf + + def writable(self): + return len(self.out_buffer) > 0 + + def handle_write(self): + if len(self.out_buffer) > 0: + sent = self.send(self.out_buffer) + self.out_buffer = self.out_buffer[sent:] + + if len(self.oob_buffer) > 0: + sent = self.socket.send(self.oob_buffer, socket.MSG_OOB) + self.oob_buffer = self.oob_buffer[sent:] + + def handle_key(self, keys, state): + val = 0 + for key in keys: + if key in KEY_MAP: + val += KEY_MAP[key] + self.out_buffer += struct.pack(" G15 key") + + """ + Private + """ + + def _on_hidden(self): + if self.keyboard_backlight_acquire: + self.plugin.screen.driver.release_control(self.keyboard_backlight_acquire) + if self.backlight_acquire: + self.plugin.screen.driver.release_control(self.backlight_acquire) + + def _on_shown(self): + self.backlight_control = self.plugin.screen.driver.get_control_for_hint(g15driver.HINT_DIMMABLE) + if self.backlight_control: + if self.backlight_value is None: + self.backlight_value = self.backlight_control.value + self.backlight_acquire = self.plugin.screen.driver.acquire_control(self.backlight_control, val = self.backlight_value) + self.keyboard_backlight_control = self.plugin.screen.driver.get_control_for_hint(g15driver.HINT_SHADEABLE) + if self.keyboard_backlight_control: + if self.keyboard_backlight_value is None: + self.keyboard_backlight_value = self.keyboard_backlight_control.value + self.keyboard_backlight_acquire = self.plugin.screen.driver.acquire_control(self.keyboard_backlight_control, val = self.keyboard_backlight_value) + + def _paint(self, canvas): + if self.surface != None: + size = self.plugin.screen.driver.get_size() + + if self.plugin.gconf_client.get_bool(self.plugin.gconf_key + "/keep_aspect_ratio"): + canvas.translate(0.0, 77.0) + canvas.scale(2.0, 2.0) + else: + canvas.scale(float(size[0]) / 160, float(size[1]) / 43) + #canvas.scale(2.0, 3.0) + canvas.set_source_surface(self.surface) + canvas.paint() + +class G15Async(Thread): + def __init__(self): + Thread.__init__(self) + self.name = "G15Async" + self.setDaemon(True) + + def run(self): + try : + asyncore.loop(timeout=0.05) + except Exception as e: + logger.warning("Failed to connect to G15Daemon client", exc_info = e) + +class G15DaemonServer(): + + def __init__(self, gconf_key, gconf_client, screen): + self.screen = screen + self.gconf_client = gconf_client + self.gconf_key = gconf_key + self.daemon = None + + def activate(self): + self.screen_index = 0 + self.clients = [] + self.screen.driver.control_update_listeners.append(self) + self.load_configuration() + self.notify_handle = self.gconf_client.notify_add(self.gconf_key, self._config_changed); + self.daemon = G15Daemon(self._get_port(), self) + self.async = G15Async() + self.async.start() + + def deactivate(self): + self._stop_all_clients() + self.daemon.close() + if self in self.screen.driver.control_update_listeners: + self.screen.driver.control_update_listeners.remove(self) + self.gconf_client.notify_remove(self.notify_handle); + self.daemon = None + + def destroy(self): + pass + + def control_updated(self, control): + if control.id == "foreground": + self.load_configuration() + self.screen.redraw() + + def _get_port(self): + port_entry = self.gconf_client.get(self.gconf_key + "/port") + return 15550 if port_entry == None else port_entry.get_int() + + def _config_changed(self, client, connection_id, entry, args): + self.load_configuration() + self.screen.redraw() + port = self._get_port() + if self.daemon == None or self.daemon.port != port: + if self.daemon != None: + logger.warning("Port changed to %d (will restart daemon - clients may have to be reconnected manually", port) + self._stop_all_clients() + self.daemon.close() + self.daemon = G15Daemon(port, self) + self.async = G15Async() + self.async.start() + else: + for c in self.clients: + if c.last_img_buffer is not None: + c.draw_buffer(c.last_img_buffer) + + def _stop_all_clients(self): + for c in self.clients: + c.handle_close() + + def load_configuration(self): + self.take_over_macro_keys = g15gconf.get_bool_or_default(self.gconf_client, "%s/take_over_macro_keys" % self.gconf_key, True) + + if g15gconf.get_bool_or_default(self.gconf_client, "%s/use_custom_foreground" % self.gconf_key, False): + col = g15gconf.get_rgb_or_default(self.gconf_client, "%s/custom_foreground" % self.gconf_key, (255,255,255)) + self.palette = [0 for n in range(768)] + self.palette[765] = col[0] + self.palette[766] = col[1] + self.palette[767] = col[2] + else: + foreground_control = self.screen.driver.get_control("foreground") + if foreground_control is None: + self.palette = None + else: + self.palette = [0 for n in range(768)] + self.palette[765] = foreground_control.value[0] + self.palette[766] = foreground_control.value[1] + self.palette[767] = foreground_control.value[2] + + backlight_control = self.screen.driver.get_control_for_hint(g15driver.HINT_DIMMABLE) + self.default_backlight = backlight_control.value if backlight_control is not None else None + + lcd_brightness_control = self.screen.driver.get_control_for_hint(g15driver.HINT_SHADEABLE) + self.default_lcd_brightness = lcd_brightness_control.value if lcd_brightness_control is not None else None + + def leave(self, client): + if client in self.clients: + self.clients.remove(client) + + def join(self, client): + self.clients.append(client) + + def handle_key(self, keys, state, post): + if ( not post and self.take_over_macro_keys ) or ( post and not self.take_over_macro_keys ): + visible = self.screen.get_visible_page() + for client in self.clients: + if client.enable_keys and client.page == visible: + client.handle_key(keys, state) + return True + +class G15Daemon(asyncore.dispatcher): + def __init__(self, port, plugin): + asyncore.dispatcher.__init__(self) + self.create_socket(socket.AF_INET, socket.SOCK_STREAM) + self.set_reuse_addr() + logger.info('Binding to port %d', port) + self.bind(("127.0.0.1", port)) + logger.info('Bound to port %d', port) + self.listen(5) + self.plugin = plugin + self.port = port + + def handle_accept(self): + sock, addr = self.accept() + logger.debug('Got client') + client = G15DaemonClient(sock, self.plugin) + + def handle_sig_term(self, arg0, arg1): + self.close() \ No newline at end of file diff --git a/src/plugins/g15daemon-server/g15daemon-server.ui b/src/plugins/g15daemon-server/g15daemon-server.ui new file mode 100644 index 0000000..5473549 --- /dev/null +++ b/src/plugins/g15daemon-server/g15daemon-server.ui @@ -0,0 +1,176 @@ + + + + + + 320 + False + 5 + G15Daemon Compatibility Preferences + False + True + center-on-parent + dialog + + + True + False + 2 + + + True + False + end + + + gtk-close + True + True + True + True + + + False + False + 0 + + + + + False + True + end + 0 + + + + + True + False + 4 + + + True + False + 4 + + + True + False + Port + + + False + False + 0 + + + + + True + True + + False + False + True + True + PortAdjustment + + + True + True + 1 + + + + + True + True + 0 + + + + + Keep Aspect Ratio + True + True + False + True + + + True + True + 1 + + + + + Take over all macro keys when active + True + True + False + True + + + True + True + 2 + + + + + True + False + + + Use custom foreground colour + True + True + False + True + + + True + True + 0 + + + + + True + True + True + #000000000000 + + + True + True + 1 + + + + + True + True + 3 + + + + + False + False + 1 + + + + + + button9 + + + + 65535 + 1 + 10 + + diff --git a/src/plugins/g15daemon-server/i18n/g15daemon-server.en_GB.po b/src/plugins/g15daemon-server/i18n/g15daemon-server.en_GB.po new file mode 100644 index 0000000..7f97ca7 --- /dev/null +++ b/src/plugins/g15daemon-server/i18n/g15daemon-server.en_GB.po @@ -0,0 +1,30 @@ +# English translations for g package. +# Copyright (C) 2012 THE g'S COPYRIGHT HOLDER +# This file is distributed under the same license as the g package. +# Automatically generated, 2012. +# +msgid "" +msgstr "" +"Project-Id-Version: g 15daemon-server\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: 2012-01-08 11:23+0000\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: en_GB\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=ASCII\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: i18n/g15daemon-server.glade.h:1 +msgid "G15Daemon Compatibility Preferences" +msgstr "G15Daemon Compatibility Preferences" + +#: i18n/g15daemon-server.glade.h:2 +msgid "Keep Aspect Ratio" +msgstr "Keep Aspect Ratio" + +#: i18n/g15daemon-server.glade.h:3 +msgid "Port" +msgstr "Port" diff --git a/src/plugins/g15daemon-server/i18n/g15daemon-server.glade.h b/src/plugins/g15daemon-server/i18n/g15daemon-server.glade.h new file mode 100644 index 0000000..ccd64fb --- /dev/null +++ b/src/plugins/g15daemon-server/i18n/g15daemon-server.glade.h @@ -0,0 +1,3 @@ +char *s = N_("G15Daemon Compatibility Preferences"); +char *s = N_("Keep Aspect Ratio"); +char *s = N_("Port"); diff --git a/src/plugins/g15daemon-server/i18n/g15daemon-server.pot b/src/plugins/g15daemon-server/i18n/g15daemon-server.pot new file mode 100644 index 0000000..5d2366f --- /dev/null +++ b/src/plugins/g15daemon-server/i18n/g15daemon-server.pot @@ -0,0 +1,30 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: i18n/g15daemon-server.glade.h:1 +msgid "G15Daemon Compatibility Preferences" +msgstr "" + +#: i18n/g15daemon-server.glade.h:2 +msgid "Keep Aspect Ratio" +msgstr "" + +#: i18n/g15daemon-server.glade.h:3 +msgid "Port" +msgstr "" diff --git a/src/plugins/game-nexuiz/Makefile.am b/src/plugins/game-nexuiz/Makefile.am new file mode 100644 index 0000000..fd449aa --- /dev/null +++ b/src/plugins/game-nexuiz/Makefile.am @@ -0,0 +1,28 @@ +SUBDIRS = default resources + +plugindir = $(datadir)/gnome15/plugins/game-nexuiz +plugin_DATA = game-nexuiz.py \ + game-nexuiz.g13.macros \ + game-nexuiz.g19.macros + +EXTRA_DIST = \ + $(plugin_DATA) + +all-local: + for M_LOCALE in @ENABLED_LOCALES@; do \ + mkdir -p i18n/$$M_LOCALE/LC_MESSAGES ; \ + if [ `ls i18n/*.po 2>/dev/null|wc -l` -gt 0 ]; then \ + for M_PO in i18n/*.po; do \ + BN=`basename $$M_PO .po`; \ + LL=`basename $$BN .$$M_LOCALE`.mo; \ + echo "$$M_PO -> $$LL"; \ + msgfmt $$M_PO --output-file i18n/$$M_LOCALE/LC_MESSAGES/$$LL; \ + done; \ + fi; \ + done + +install-exec-hook: + for M_LOCALE in @ENABLED_LOCALES@; do \ + mkdir -p $(DESTDIR)$(datadir)/gnome15/plugins/clock/i18n; \ + cp -pR i18n/$$M_LOCALE $(DESTDIR)$(datadir)/gnome15/plugins/clock/i18n; \ + done diff --git a/src/plugins/game-nexuiz/default/Makefile.am b/src/plugins/game-nexuiz/default/Makefile.am new file mode 100644 index 0000000..6632b53 --- /dev/null +++ b/src/plugins/game-nexuiz/default/Makefile.am @@ -0,0 +1,25 @@ +themedir = $(datadir)/gnome15/plugins/game-nexuiz/default +theme_DATA = default.svg \ + g19.svg + +EXTRA_DIST = \ + $(theme_DATA) + +all-local: + for M_LOCALE in @ENABLED_LOCALES@; do \ + mkdir -p i18n/$$M_LOCALE/LC_MESSAGES ; \ + if [ `ls i18n/*.po 2>/dev/null|wc -l` -gt 0 ]; then \ + for M_PO in i18n/*.po; do \ + BN=`basename $$M_PO .po`; \ + LL=`basename $$BN .$$M_LOCALE`.mo; \ + echo "$$M_PO -> $$LL"; \ + msgfmt $$M_PO --output-file i18n/$$M_LOCALE/LC_MESSAGES/$$LL; \ + done; \ + fi; \ + done + +install-exec-hook: + for M_LOCALE in @ENABLED_LOCALES@; do \ + mkdir -p $(DESTDIR)$(datadir)/gnome15/plugins/clock/default/i18n; \ + cp -pR i18n/$$M_LOCALE $(DESTDIR)$(datadir)/gnome15/plugins/clock/default/i18n; \ + done diff --git a/src/plugins/game-nexuiz/default/default.svg b/src/plugins/game-nexuiz/default/default.svg new file mode 100644 index 0000000..24e158f --- /dev/null +++ b/src/plugins/game-nexuiz/default/default.svg @@ -0,0 +1,78 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + ${time} + + diff --git a/src/plugins/game-nexuiz/default/g19.svg b/src/plugins/game-nexuiz/default/g19.svg new file mode 100644 index 0000000..abc16d1 --- /dev/null +++ b/src/plugins/game-nexuiz/default/g19.svg @@ -0,0 +1,156 @@ + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + 0 + + diff --git a/src/plugins/game-nexuiz/game-nexuiz.g13.macros b/src/plugins/game-nexuiz/game-nexuiz.g13.macros new file mode 100644 index 0000000..71c8b02 --- /dev/null +++ b/src/plugins/game-nexuiz/game-nexuiz.g13.macros @@ -0,0 +1,122 @@ +[DEFAULT] +name = Nexuiz +icon = resources/icon.png +window_name = +base_profile = 0 +activate_on_focus = False +send_delays = False +fixed_delays = False +press_delay = 50 +release_delay = 50 +background = +author = Brett Smith +version = 1.0 +models = g13 + +[LAUNCH] +activate_on_launch = True +pattern = 'nexuiz'|'/.*/nexuiz' +monitor = 'stdout' + +[m1] +keys_g4_name = W +keys_g4_mappedkey = KEY_W +keys_g4_maptype = keyboard +keys_g4_type = mapped-to-key +keys_g10_name = A +keys_g10_mappedkey = KEY_A +keys_g10_maptype = keyboard +keys_g10_type = mapped-to-key +keys_g11_name = S +keys_g11_mappedkey = KEY_S +keys_g11_maptype = keyboard +keys_g11_type = mapped-to-key +keys_g12_name = D +keys_g12_mappedkey = KEY_D +keys_g12_maptype = keyboard +keys_g12_type = mapped-to-key +keys_g22_name = Jump +keys_g22_mappedkey = KEY_SPACE +keys_g22_maptype = keyboard +keys_g22_type = mapped-to-key +keys_right_name = Right +keys_right_mappedkey = KEY_RIGHT +keys_right_maptype = keyboard +keys_right_type = mapped-to-key +keys_up_name = Up +keys_up_mappedkey = KEY_UP +keys_up_maptype = keyboard +keys_up_type = mapped-to-key +keys_down_name = Down +keys_down_mappedkey = KEY_DOWN +keys_down_maptype = keyboard +keys_down_type = mapped-to-key +keys_g15_name = Crouch +keys_g15_mappedkey = KEY_LEFTCTRL +keys_g15_maptype = keyboard +keys_g15_type = mapped-to-key +keys_jd_name = Tab +keys_jd_mappedkey = KEY_SLASH +keys_jd_maptype = keyboard +keys_jd_type = mapped-to-key +keys_jl_name = Space +keys_jl_mappedkey = KEY_SPACE +keys_jl_maptype = keyboard +keys_jl_type = mapped-to-key +keys_g20_name = Land +keys_g20_mappedkey = KEY_LEFTSHIFT +keys_g20_maptype = keyboard +keys_g20_type = mapped-to-key +keys_g7_name = Escape +keys_g7_type = mapped-to-key +keys_left_name = Left +keys_left_mappedkey = KEY_LEFT +keys_left_maptype = keyboard +keys_left_type = mapped-to-key +keys_jc_name = Enter +keys_jc_mappedkey = KEY_ENTER +keys_jc_maptype = keyboard +keys_jc_type = mapped-to-key +keys_g7_maptype = keyboard +keys_g7_mappedkey = KEY_1 +keys_g5_name = Use +keys_g5_type = mapped-to-key +keys_g5_maptype = keyboard +keys_g5_mappedkey = KEY_E +keys_g5_repeatdelay = -1.0 +keys_g5_repeatmode = held +keys_g4_repeatdelay = -1.0 +keys_g4_repeatmode = held +keys_g7_repeatdelay = -1.0 +keys_g7_repeatmode = held +keys_g10_repeatdelay = -1.0 +keys_g10_repeatmode = held +keys_g11_repeatdelay = -1.0 +keys_g11_repeatmode = held +keys_g12_repeatdelay = -1.0 +keys_g12_repeatmode = held +keys_g15_repeatdelay = -1.0 +keys_g15_repeatmode = held +keys_g20_repeatdelay = -1.0 +keys_g20_repeatmode = held +keys_g22_repeatdelay = -1.0 +keys_g22_repeatmode = held +keys_right_repeatdelay = -1.0 +keys_right_repeatmode = held +keys_up_repeatdelay = -1.0 +keys_up_repeatmode = held +keys_down_repeatdelay = -1.0 +keys_down_repeatmode = held +keys_jd_repeatdelay = -1.0 +keys_jd_repeatmode = held +keys_jl_repeatdelay = -1.0 +keys_jl_repeatmode = held +keys_left_repeatdelay = -1.0 +keys_left_repeatmode = held +keys_jc_repeatdelay = -1.0 +keys_jc_repeatmode = held + +[m2] + +[m3] + diff --git a/src/plugins/game-nexuiz/game-nexuiz.g19.macros b/src/plugins/game-nexuiz/game-nexuiz.g19.macros new file mode 100644 index 0000000..9662295 --- /dev/null +++ b/src/plugins/game-nexuiz/game-nexuiz.g19.macros @@ -0,0 +1,122 @@ +[DEFAULT] +name = Nexuiz +icon = resources/icon.png +window_name = +base_profile = 0 +activate_on_focus = False +send_delays = False +fixed_delays = False +press_delay = 50 +release_delay = 50 +background = resources/g19-background.jpg +author = Brett Smith +version = 1.0 +models = g19 + +[LAUNCH] +activate_on_launch = True +pattern = 'nexuiz'|'/.*/nexuiz' +monitor = 'stdout' + +[m1] +keys_g4_name = W +keys_g4_mappedkey = KEY_W +keys_g4_maptype = keyboard +keys_g4_type = mapped-to-key +keys_g10_name = A +keys_g10_mappedkey = KEY_A +keys_g10_maptype = keyboard +keys_g10_type = mapped-to-key +keys_g11_name = S +keys_g11_mappedkey = KEY_S +keys_g11_maptype = keyboard +keys_g11_type = mapped-to-key +keys_g12_name = D +keys_g12_mappedkey = KEY_D +keys_g12_maptype = keyboard +keys_g12_type = mapped-to-key +keys_g22_name = Jump +keys_g22_mappedkey = KEY_SPACE +keys_g22_maptype = keyboard +keys_g22_type = mapped-to-key +keys_right_name = Right +keys_right_mappedkey = KEY_RIGHT +keys_right_maptype = keyboard +keys_right_type = mapped-to-key +keys_up_name = Up +keys_up_mappedkey = KEY_UP +keys_up_maptype = keyboard +keys_up_type = mapped-to-key +keys_down_name = Down +keys_down_mappedkey = KEY_DOWN +keys_down_maptype = keyboard +keys_down_type = mapped-to-key +keys_g15_name = Crouch +keys_g15_mappedkey = KEY_LEFTCTRL +keys_g15_maptype = keyboard +keys_g15_type = mapped-to-key +keys_jd_name = Tab +keys_jd_mappedkey = KEY_SLASH +keys_jd_maptype = keyboard +keys_jd_type = mapped-to-key +keys_jl_name = Space +keys_jl_mappedkey = KEY_SPACE +keys_jl_maptype = keyboard +keys_jl_type = mapped-to-key +keys_g20_name = Land +keys_g20_mappedkey = KEY_LEFTSHIFT +keys_g20_maptype = keyboard +keys_g20_type = mapped-to-key +keys_g7_name = Escape +keys_g7_type = mapped-to-key +keys_left_name = Left +keys_left_mappedkey = KEY_LEFT +keys_left_maptype = keyboard +keys_left_type = mapped-to-key +keys_jc_name = Enter +keys_jc_mappedkey = KEY_ENTER +keys_jc_maptype = keyboard +keys_jc_type = mapped-to-key +keys_g7_maptype = keyboard +keys_g7_mappedkey = KEY_1 +keys_g5_name = Use +keys_g5_type = mapped-to-key +keys_g5_maptype = keyboard +keys_g5_mappedkey = KEY_E +keys_g5_repeatdelay = -1.0 +keys_g5_repeatmode = held +keys_g4_repeatdelay = -1.0 +keys_g4_repeatmode = held +keys_g7_repeatdelay = -1.0 +keys_g7_repeatmode = held +keys_g10_repeatdelay = -1.0 +keys_g10_repeatmode = held +keys_g11_repeatdelay = -1.0 +keys_g11_repeatmode = held +keys_g12_repeatdelay = -1.0 +keys_g12_repeatmode = held +keys_g15_repeatdelay = -1.0 +keys_g15_repeatmode = held +keys_g20_repeatdelay = -1.0 +keys_g20_repeatmode = held +keys_g22_repeatdelay = -1.0 +keys_g22_repeatmode = held +keys_right_repeatdelay = -1.0 +keys_right_repeatmode = held +keys_up_repeatdelay = -1.0 +keys_up_repeatmode = held +keys_down_repeatdelay = -1.0 +keys_down_repeatmode = held +keys_jd_repeatdelay = -1.0 +keys_jd_repeatmode = held +keys_jl_repeatdelay = -1.0 +keys_jl_repeatmode = held +keys_left_repeatdelay = -1.0 +keys_left_repeatmode = held +keys_jc_repeatdelay = -1.0 +keys_jc_repeatmode = held + +[m2] + +[m3] + diff --git a/src/plugins/game-nexuiz/game-nexuiz.py b/src/plugins/game-nexuiz/game-nexuiz.py new file mode 100644 index 0000000..a16112a --- /dev/null +++ b/src/plugins/game-nexuiz/game-nexuiz.py @@ -0,0 +1,79 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2011 Brett Smith +# +# 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 . + +import gnome15.g15theme as g15theme +import gnome15.g15driver as g15driver +import gnome15.g15profile as g15profile +import os +import locale + +# Plugin details - All of these must be provided +id="game-nexuiz" +name=_("Nexuiz") +description=_("Gaming plugin for Nexuiz") +author="Brett Smith " +copyright=_("Copyright (C)2011 Brett Smith") +site="http://www.gnome15.org/" +has_preferences=True +unsupported_models = [ g15driver.MODEL_G110, g15driver.MODEL_G11, g15driver.MODEL_G930, g15driver.MODEL_G35 ] + +""" +Register this as a location for profiles +""" +g15profile.add_profile_dir(os.path.dirname(__file__)) + +def create(gconf_key, gconf_client, screen): + return GameNexuiz(gconf_key, gconf_client, screen) + +class GameNexuiz(): + + def __init__(self, gconf_key, gconf_client, screen): + self._screen = screen + self._gconf_client = gconf_client + self._gconf_key = gconf_key + self._page = None + + def activate(self): + self._reload_theme() + self._page = g15theme.G15Page("Nexuiz", self._screen, + theme_properties_callback = self._get_properties, + theme = self._theme, + originating_plugin = self) + self._page.title = "Nexuiz" + self._screen.add_page(self._page) + self._redraw() + + # Add the right profile for the model + macro_file = os.path.join(os.path.dirname(__file__), "game-nexuiz.%s.macros") + if os.path.exists(macro_file): + profile = g15profile.G15Profile(self._screen.device, file_path = macro_file) + g15profiles.add_profile(profile) + + def deactivate(self): + self._screen.del_page(self._page) + + def destroy(self): + pass + + def _redraw(self): + self._screen.redraw(self._page) + + def _reload_theme(self): + self._theme = g15theme.G15Theme(os.path.join(os.path.dirname(__file__), "default"), None) + + def _get_properties(self): + properties = { } + return properties diff --git a/src/plugins/game-nexuiz/resources/Makefile.am b/src/plugins/game-nexuiz/resources/Makefile.am new file mode 100644 index 0000000..28739dc --- /dev/null +++ b/src/plugins/game-nexuiz/resources/Makefile.am @@ -0,0 +1,8 @@ +resourcesdir = $(datadir)/gnome15/plugins/game-nexuiz/resources +resources_DATA = \ + g19-background.jpg \ + icon.png + +EXTRA_DIST = \ + $(resources_DATA) + diff --git a/src/plugins/game-nexuiz/resources/g19-background.jpg b/src/plugins/game-nexuiz/resources/g19-background.jpg new file mode 100644 index 0000000..26a2eb7 Binary files /dev/null and b/src/plugins/game-nexuiz/resources/g19-background.jpg differ diff --git a/src/plugins/game-nexuiz/resources/icon.png b/src/plugins/game-nexuiz/resources/icon.png new file mode 100644 index 0000000..be28a6f Binary files /dev/null and b/src/plugins/game-nexuiz/resources/icon.png differ diff --git a/src/plugins/google-analytics/Makefile.am b/src/plugins/google-analytics/Makefile.am new file mode 100644 index 0000000..8a49b0d --- /dev/null +++ b/src/plugins/google-analytics/Makefile.am @@ -0,0 +1,7 @@ +SUBDIRS = default +plugindir = $(datadir)/gnome15/plugins/google-analytics +plugin_DATA = google-analytics.py \ + google-analytics.ui + +EXTRA_DIST = \ + $(plugin_DATA) \ No newline at end of file diff --git a/src/plugins/google-analytics/default/Makefile.am b/src/plugins/google-analytics/default/Makefile.am new file mode 100644 index 0000000..3a0148f --- /dev/null +++ b/src/plugins/google-analytics/default/Makefile.am @@ -0,0 +1,25 @@ +themedir = $(datadir)/gnome15/plugins/google-analytics/default +theme_DATA = default.svg \ + g19.svg + +EXTRA_DIST = \ + $(theme_DATA) + +all-local: + for M_LOCALE in @ENABLED_LOCALES@; do \ + mkdir -p i18n/$$M_LOCALE/LC_MESSAGES ; \ + if [ `ls i18n/*.po 2>/dev/null|wc -l` -gt 0 ]; then \ + for M_PO in i18n/*.po; do \ + BN=`basename $$M_PO .po`; \ + LL=`basename $$BN .$$M_LOCALE`.mo; \ + echo "$$M_PO -> $$LL"; \ + msgfmt $$M_PO --output-file i18n/$$M_LOCALE/LC_MESSAGES/$$LL; \ + done; \ + fi; \ + done + +install-exec-hook: + for M_LOCALE in @ENABLED_LOCALES@; do \ + mkdir -p $(DESTDIR)$(datadir)/gnome15/plugins/cal/default/i18n; \ + cp -pR i18n/$$M_LOCALE $(DESTDIR)$(datadir)/gnome15/plugins/cal/default/i18n; \ + done diff --git a/src/plugins/google-analytics/default/default.svg b/src/plugins/google-analytics/default/default.svg new file mode 100644 index 0000000..f661645 --- /dev/null +++ b/src/plugins/google-analytics/default/default.svg @@ -0,0 +1,287 @@ + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + ${title} + + + + + + + ${alt_title} + + Visit: + ${visits} + Views: + ${views} + Uniq : + ${unique} + Pg/Vst: + ${pagesVisit} + Av.Dur: + ${avgDuration} + Bounce: + ${bounce}% + + diff --git a/src/plugins/google-analytics/default/g19.svg b/src/plugins/google-analytics/default/g19.svg new file mode 100644 index 0000000..048ee4d --- /dev/null +++ b/src/plugins/google-analytics/default/g19.svg @@ -0,0 +1,351 @@ + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + ${visits} + _(Sites) + + Visits: + Unique: + ${unique} + Views: + ${views} + Pg/Visit: + ${pagesVisit} + Avg. Dur.: + ${avgDuration} + Bounce%: + ${bounce} + + + + ${title} + ${alt_title} + + + + + + + + + ${message} + + diff --git a/src/plugins/google-analytics/google-analytics.py b/src/plugins/google-analytics/google-analytics.py new file mode 100644 index 0000000..f6dd14f --- /dev/null +++ b/src/plugins/google-analytics/google-analytics.py @@ -0,0 +1,376 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2012 Brett Smith +# +# 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 . + +import gnome15.g15locale as g15locale +_ = g15locale.get_translation("cal", modfile = __file__).ugettext + +import gnome15.g15theme as g15theme +import gnome15.g15driver as g15driver +import gnome15.util.g15convert as g15convert +import gnome15.util.g15gconf as g15gconf +import gnome15.util.g15scheduler as g15scheduler +import gnome15.util.g15cairo as g15cairo +import gnome15.util.g15icontools as g15icontools +import gnome15.g15screen as g15screen +import gnome15.g15accounts as g15accounts +import gnome15.g15globals as g15globals +import datetime +import time +import os, os.path +import gobject +import calendar +import gtk +import gdata.analytics.client +import cairoplot +import cairo + +# Logging +import logging +logger = logging.getLogger(__name__) + + +id="google-analytics" +name=_("Google Analytics") +description=_("Displays some summary information about sites being monitored\n\ +by Google Analytics. You will require a Google Account, and the ID of the sites\n\ +you wish to monitor.") +author="Brett Smith " +copyright=_("Copyright (C)2010 Brett Smith") +site="http://www.russo79.com/gnome15" +has_preferences=True +needs_network=True +actions={ + g15driver.PREVIOUS_SELECTION : _("Previous site"), + g15driver.NEXT_SELECTION : _("Next site"), + } +unsupported_models = [ g15driver.MODEL_G110, g15driver.MODEL_G11, g15driver.MODEL_MX5500, g15driver.MODEL_G930, g15driver.MODEL_G35 ] + +SOURCE_APP_NAME = '%s-%s' % ( g15globals.name, g15globals.version ) +CONFIG_PATH = os.path.join(g15globals.user_config_dir, + "plugin-data", + "google-analytics", + "accounts.xml") +CONFIG_ITEM_NAME = "accounts" +ACC_MGR_HOSTNAME = "www.google.com" + +""" +Functions +""" + +def create(gconf_key, gconf_client, screen): + return G15GoogleAnalytics(gconf_key, gconf_client, screen) + +def show_preferences(parent, driver, gconf_client, gconf_key): + G15GoogleAnalyticsPreferences(parent, gconf_client, gconf_key) + +def get_update_time(gconf_client, gconf_key): + val = gconf_client.get_int(gconf_key + "/update_time") + if val == 0: + val = 10 + return val + +class Site(): + + def __init__(self): + self.name = "Unknown" + +class SiteMenuItem(g15theme.MenuItem): + + def __init__(self, entry, account): + g15theme.MenuItem.__init__(self, entry.GetProperty('ga:webPropertyId').value) + self._entry = entry + self._account = account + self.aggregates = {} + + def get_default_theme_dir(self): + return os.path.join(os.path.dirname(__file__), "default") + + def get_theme_properties(self): + + item_properties = g15theme.MenuItem.get_theme_properties(self) + item_properties["item_name"] = self._entry.GetProperty('ga:accountName').value + item_properties["item_alt"] = self._account.name + + return item_properties + + def activate(self): + self.event.activate() + + +class GoogleAnalyticsOptions(g15accounts.G15AccountOptions): + def __init__(self, account, account_ui): + g15accounts.G15AccountOptions.__init__(self, account, account_ui) + + self.widget_tree = gtk.Builder() + self.widget_tree.add_from_file(os.path.join(os.path.dirname(__file__), "google-analytics.ui")) + self.component = self.widget_tree.get_object("OptionPanel") + + username = self.widget_tree.get_object("Username") + username.connect("changed", self._username_changed) + username.set_text(self.account.get_property("username", "")) + + def _username_changed(self, widget): + self.account.properties["username"] = widget.get_text() + self.account_ui.save_accounts() + + +class G15VisitsGraph(g15theme.Component): + + def __init__(self, component_id, plugin): + g15theme.Component.__init__(self, component_id) + self.plugin = plugin + + def get_colors(self): + series_colors = None + fill_colors = None + if self.plugin._screen.driver.get_control_for_hint(g15driver.HINT_HIGHLIGHT): + highlight_color = self.plugin._screen.driver.get_color_as_ratios(g15driver.HINT_HIGHLIGHT, (255, 0, 0 )) + series_colors = (highlight_color[0],highlight_color[1],highlight_color[2], 1.0) + fill_colors = (highlight_color[0],highlight_color[1],highlight_color[2], 0.50) + return series_colors, fill_colors + + def create_plot(self, graph_surface): + series_color, fill_color = self.get_colors() + alt_series_color = g15convert.get_alt_color(series_color) + alt_fill_color = g15convert.get_alt_color(fill_color) + + selected = self.plugin._menu.selected + pie_data = {} + if selected: + new_visits = float(selected.aggregates["ga:percentNewVisits"]) + returning = 100.0 - new_visits + pie_data[_("New %0.2f%%" % new_visits)] = new_visits + pie_data[_("Returning %0.2f%%" % returning)] = returning + + plot = cairoplot.PiePlot(graph_surface, pie_data, + self.view_bounds[2], + self.view_bounds[3], + background = None, + colors = [ series_color, alt_series_color ]) + plot.font_size = 18 + return plot + + def paint(self, canvas): + g15theme.Component.paint(self, canvas) + if self.view_bounds: + graph_surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, + int(self.view_bounds[2]), + int(self.view_bounds[3])) + plot = self.create_plot(graph_surface) + plot.line_width = 2.0 + plot.line_color = self.plugin._screen.driver.get_color_as_ratios(g15driver.HINT_FOREGROUND, (255, 255, 255)) + plot.label_color = self.plugin._screen.driver.get_color_as_ratios(g15driver.HINT_FOREGROUND, (255, 255, 255)) + plot.shadow = True + plot.bounding_box = False + plot.render() + plot.commit() + + canvas.save() + canvas.translate(self.view_bounds[0], self.view_bounds[1]) + canvas.set_source_surface(graph_surface, 0.0, 0.0) + canvas.paint() + canvas.restore() + + +class G15GoogleAnalyticsPreferences(g15accounts.G15AccountPreferences): + ''' + Configuration UI + ''' + + def __init__(self, parent, gconf_client, gconf_key): + g15accounts.G15AccountPreferences.__init__(self, parent, gconf_client, \ + gconf_key, \ + CONFIG_PATH, \ + CONFIG_ITEM_NAME) + + def get_account_types(self): + return [ "google-analytics" ] + + def get_account_type_name(self, account_type): + return _(account_type) + + def create_options_for_type(self, account, account_type): + return GoogleAnalyticsOptions(account, self) + +class G15GoogleAnalytics(): + + def __init__(self, gconf_key, gconf_client, screen): + + self._screen = screen + self._gconf_client = gconf_client + self._gconf_key = gconf_key + self._timer = None + self._icon_path = g15icontools.get_icon_path([ "redhat-office", "package_office", "gnome-applications", "xfce-office", "baobab" ]) + self._thumb_icon = g15cairo.load_surface_from_file(self._icon_path) + self._timer = None + + def activate(self): + self._active = True + self._page = None + self._theme = g15theme.G15Theme(os.path.join(os.path.dirname(__file__), "default"), auto_dirty = False) + self._loaded = 0 + + self.pie_data = [] + + # Backend + self._account_manager = g15accounts.G15AccountManager(CONFIG_PATH, CONFIG_ITEM_NAME) + self._account_manager.add_change_listener(self._accounts_changed) + + # Menu + self._menu = g15theme.Menu("menu") + self._menu.focusable = True + self._menu.on_selected = self._on_menu_selected + + # Page + self._page = g15theme.G15Page(name, self._screen, theme_properties_callback = self._get_properties, + thumbnail_painter = self._paint_thumbnail, + originating_plugin = self) + self._page.set_title(_("Google Analytics")) + self._page.set_theme(self._theme) + self._screen.key_handler.action_listeners.append(self) + self._page.add_child(G15VisitsGraph("visitsGraph", self)) + self._page.add_child(self._menu) + self._page.add_child(g15theme.MenuScrollbar("viewScrollbar", self._menu)) + self._screen.add_page(self._page) + self._schedule_refresh(0) + + def deactivate(self): + self._account_manager.remove_change_listener(self._accounts_changed) + self._screen.key_handler.action_listeners.remove(self) + self._cancel_refresh() + self._page.delete() + + def destroy(self): + pass + + def action_performed(self, binding): + if self._page and self._page.is_visible(): + pass + + """ + Private + """ + def _on_menu_selected(self): + self._gconf_client.set_string("%s/selected_site" % self._gconf_key, self._menu.selected.id) + + def _cancel_refresh(self): + if self._timer != None: + self._timer.cancel() + + def _do_refresh(self): + if self._page: + self._load_site_data() + self._page.redraw() + self._schedule_refresh(get_update_time(self._gconf_client, self._gconf_key) * 60.0) + selected = g15gconf.get_string_or_default(self._gconf_client, "%s/selected_site" % self._gconf_key, None) + if selected: + for m in self._menu.get_children(): + if m.id == selected: + self._menu.set_selected_item(m) + + def _schedule_refresh(self, time): + self._timer = g15scheduler.schedule("AnalyticsRedraw", time, self._do_refresh) + + def _accounts_changed(self, account_manager): + self._cancel_refresh() + self._do_refresh() + + def _get_properties(self): + properties = {} + properties["icon"] = self._icon_path + properties["title"] = self._page.title + properties["alt_title"] = "" + properties["message"] = _("No sites configured") if len(self._menu.get_children()) == 0 else "" + sel = self._menu.selected + if sel: + properties["visits"] = sel.aggregates["ga:visits"] + properties["unique"] = sel.aggregates["ga:newVisits"] + properties["views"] = sel.aggregates["ga:pageviews"] + properties["pagesVisit"] = "%0.2f" % float(sel.aggregates["ga:pageviewsPerVisit"]) + properties["avgDuration"] = str(datetime.timedelta(seconds=int(float(sel.aggregates["ga:avgTimeOnSite"])))) + properties["bounce"] = "%0.2f" % float(sel.aggregates["ga:visitBounceRate"]) + properties["uniquePercent"] = "%0.2f" % float(sel.aggregates["ga:percentNewVisits"]) + else: + properties["visits"] = "" + properties["unique"] = "" + properties["views"] = "" + properties["pagesVisit"] = "" + properties["avgDuration"] = "" + properties["bounce"] = "" + properties["uniquePercent"] = "" + + return properties + + def _load_site_data(self): + items = [] + for acc in self._account_manager.accounts: + self._load_account_site_data(items, acc) + self._menu.set_children(items) + self._page.mark_dirty() + + def _load_account_site_data(self, items, account): + self._client = gdata.analytics.client.AnalyticsClient(source=SOURCE_APP_NAME) + ex = None + for i in range(0, 3): + for j in range(0, 2): + password = self._account_manager.retrieve_password(account, ACC_MGR_HOSTNAME, None, i > 0) + if password == None or password == "": + raise Exception(_("Authentication cancelled")) + + try : + return self._retrieve_site_data(items, account, password) + except gdata.client.BadAuthentication as e: + logger.debug("Error authenticating", exc_info = e) + ex = e + + if ex is not None: + raise ex + + def _retrieve_site_data(self, items, account, password): + username = account.get_property("username", "") + logger.info("Logging in as %s / %s for %s on %s", + username, + password, + account, + self._client.source) + self._client.ClientLogin(username, password, self._client.source) + account_query = gdata.analytics.client.AccountFeedQuery() + self._account_manager.store_password(account, password, ACC_MGR_HOSTNAME, None) + self.feed = self._client.GetAccountFeed(account_query) + + for entry in self.feed.entry: + item = SiteMenuItem(entry, account) + items.append(item) + end_date = datetime.date.today() + start_date = datetime.date(2005,01,01) + data_query = gdata.analytics.client.DataFeedQuery({ + 'ids': entry.table_id.text, + 'start-date': start_date.isoformat(), + 'end-date': end_date.isoformat(), + 'max-results': 0, + 'dimensions': 'ga:date', + 'metrics': 'ga:visits,ga:newVisits,ga:pageviews,ga:pageviewsPerVisit,ga:avgTimeOnSite,ga:visitBounceRate,ga:percentNewVisits'}) + + feed = self._client.GetDataFeed(data_query) + aggregates = feed.aggregates + for m in aggregates.metric: + item.aggregates[m.name] = m.value + + def _paint_thumbnail(self, canvas, allocated_size, horizontal): + if self._page != None and self._thumb_icon != None and self._screen.driver.get_bpp() == 16: + return g15cairo.paint_thumbnail_image(allocated_size, self._thumb_icon, canvas) + + diff --git a/src/plugins/google-analytics/google-analytics.ui b/src/plugins/google-analytics/google-analytics.ui new file mode 100644 index 0000000..e898ad5 --- /dev/null +++ b/src/plugins/google-analytics/google-analytics.ui @@ -0,0 +1,60 @@ + + + + + + False + + + True + False + + + True + False + 2 + 8 + 8 + + + True + False + 0 + Username + + + GTK_FILL + + + + + + True + True + + False + False + True + True + + + 1 + 2 + + + + + + False + False + 8 + 0 + + + + + + + + + diff --git a/src/plugins/im/Makefile.am b/src/plugins/im/Makefile.am new file mode 100644 index 0000000..860558b --- /dev/null +++ b/src/plugins/im/Makefile.am @@ -0,0 +1,5 @@ +plugindir = $(datadir)/gnome15/plugins/im +plugin_DATA = im.py + +EXTRA_DIST = \ + $(plugin_DATA) \ No newline at end of file diff --git a/src/plugins/im/i18n/im.en_GB.po b/src/plugins/im/i18n/im.en_GB.po new file mode 100644 index 0000000..472d9b9 --- /dev/null +++ b/src/plugins/im/i18n/im.en_GB.po @@ -0,0 +1,92 @@ +# English translations for gnome15-plugins package. +# Copyright (C) 2012 THE gnome15-plugins'S COPYRIGHT HOLDER +# This file is distributed under the same license as the gnome15-plugins package. +# Automatically generated, 2012. +# +msgid "" +msgstr "" +"Project-Id-Version: gnome15-plugins 0.8.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: 2012-01-08 11:23+0000\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: en_GB\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=ASCII\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: im.py:61 +msgid "Instant Messenger" +msgstr "Instant Messenger" + +#: im.py:62 +msgid "" +"Integrates with a number of instant messengers, showing buddy " +"lists and messages on your LCD. Currently supports all clients " +"that use the Telepathy framework." +msgstr "" +"Integrates with a number of instant messengers, showing buddy " +"lists and messages on your LCD. Currently supports all clients " +"that use the Telepathy framework." + +#: im.py:66 +msgid "Copyright (C)2011 Brett Smith" +msgstr "Copyright (C)2011 Brett Smith" + +#: im.py:71 +msgid "Previous contact" +msgstr "Previous contact" + +#: im.py:72 +msgid "Next contact" +msgstr "Next contact" + +#: im.py:73 +msgid "Toggle mode" +msgstr "Toggle mode" + +#: im.py:74 +msgid "Next page" +msgstr "Next page" + +#: im.py:75 +msgid "Previous page" +msgstr "Previous page" + +#: im.py:84 +msgid "Offline" +msgstr "Offline" + +#: im.py:85 +msgid "Available" +msgstr "Available" + +#: im.py:86 +msgid "Chatty" +msgstr "Chatty" + +#: im.py:87 +msgid "Idle" +msgstr "Idle" + +#: im.py:88 +msgid "Busy" +msgstr "Busy" + +#: im.py:89 +msgid "Away" +msgstr "Away" + +#: im.py:97 +msgid "All Contacts" +msgstr "All Contacts" + +#: im.py:98 +msgid "Online Contacts" +msgstr "Online Contacts" + +#: im.py:99 +msgid "Available Contacts" +msgstr "Available Contacts" diff --git a/src/plugins/im/i18n/im.pot b/src/plugins/im/i18n/im.pot new file mode 100644 index 0000000..0e07810 --- /dev/null +++ b/src/plugins/im/i18n/im.pot @@ -0,0 +1,89 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: im.py:61 +msgid "Instant Messenger" +msgstr "" + +#: im.py:62 +msgid "" +"Integrates with a number of instant messengers, showing buddy " +"lists and messages on your LCD. Currently supports all clients " +"that use the Telepathy framework." +msgstr "" + +#: im.py:66 +msgid "Copyright (C)2011 Brett Smith" +msgstr "" + +#: im.py:71 +msgid "Previous contact" +msgstr "" + +#: im.py:72 +msgid "Next contact" +msgstr "" + +#: im.py:73 +msgid "Toggle mode" +msgstr "" + +#: im.py:74 +msgid "Next page" +msgstr "" + +#: im.py:75 +msgid "Previous page" +msgstr "" + +#: im.py:84 +msgid "Offline" +msgstr "" + +#: im.py:85 +msgid "Available" +msgstr "" + +#: im.py:86 +msgid "Chatty" +msgstr "" + +#: im.py:87 +msgid "Idle" +msgstr "" + +#: im.py:88 +msgid "Busy" +msgstr "" + +#: im.py:89 +msgid "Away" +msgstr "" + +#: im.py:97 +msgid "All Contacts" +msgstr "" + +#: im.py:98 +msgid "Online Contacts" +msgstr "" + +#: im.py:99 +msgid "Available Contacts" +msgstr "" diff --git a/src/plugins/im/im.py b/src/plugins/im/im.py new file mode 100644 index 0000000..8585815 --- /dev/null +++ b/src/plugins/im/im.py @@ -0,0 +1,544 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2011 Brett Smith +# +# 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 . +# +# Notes +# ===== +# +# The program "contact-selector" was a big help in getting this working. The ContactList +# class is very loosely based on this, with many modifications. These are licensed under +# LGPL. See http://telepathy.freedesktop.org/wiki/Contact%20selector + + +import gnome15.g15locale as g15locale +_ = g15locale.get_translation("im", modfile = __file__).ugettext + +import gnome15.util.g15scheduler as g15scheduler +import gnome15.util.g15icontools as g15icontools +import gnome15.g15theme as g15theme +import gnome15.g15driver as g15driver +import gnome15.g15plugin as g15plugin +import dbus +import telepathy +from telepathy.interfaces import ( + CHANNEL, + CHANNEL_INTERFACE_GROUP, + CHANNEL_TYPE_CONTACT_LIST, + CONNECTION, + CONNECTION_INTERFACE_ALIASING, + CONNECTION_INTERFACE_CONTACTS, + CONNECTION_INTERFACE_REQUESTS, + CONNECTION_INTERFACE_SIMPLE_PRESENCE) + +from telepathy.constants import ( + CONNECTION_PRESENCE_TYPE_AVAILABLE, + CONNECTION_PRESENCE_TYPE_AWAY, + CONNECTION_PRESENCE_TYPE_BUSY, + CONNECTION_PRESENCE_TYPE_EXTENDED_AWAY, + HANDLE_TYPE_LIST) + +# Logging +import logging +logger = logging.getLogger(__name__) + +# Plugin details - All of these must be provided +id="im" +name=_("Instant Messenger") +description=_("Integrates with a number of instant messengers, showing \n\ +buddy lists and messages on your LCD. Currently supports all \n\ +clients that use the Telepathy framework.") +author="Brett Smith " +copyright=_("Copyright (C)2011 Brett Smith") +site="http://www.russo79.com/gnome15" +has_preferences=False +unsupported_models = [ g15driver.MODEL_G110, g15driver.MODEL_G11, g15driver.MODEL_G930, g15driver.MODEL_G35 ] +actions={ + g15driver.PREVIOUS_SELECTION : _("Previous contact"), + g15driver.NEXT_SELECTION : _("Next contact"), + g15driver.VIEW : _("Toggle mode"), + g15driver.NEXT_PAGE : _("Next page"), + g15driver.PREVIOUS_PAGE : _("Previous page") + } + +# Other constants +POSSIBLE_ICON_NAMES = [ "im-user", "empathy", "pidgin", "emesene", "system-config-users", "im-message-new" ] +CONNECTION_PRESENCE_TYPE_OFFLINE = 1 + +IMAGE_DIR = 'images' +STATUS_MAP = { + ( CONNECTION_PRESENCE_TYPE_OFFLINE, None ): [ [ "offline", "user-offline-panel" ] , _("Offline")], + ( CONNECTION_PRESENCE_TYPE_AVAILABLE, None ): [ "user-available", _("Available") ], + ( CONNECTION_PRESENCE_TYPE_AVAILABLE, "chat" ): [ "im-message-new", _("Chatty") ], + ( CONNECTION_PRESENCE_TYPE_AWAY, None ): [ "user-idle", _("Idle") ], + ( CONNECTION_PRESENCE_TYPE_BUSY, None ): [ "user-busy", _("Busy") ], + ( CONNECTION_PRESENCE_TYPE_EXTENDED_AWAY, None ): [ "user-away", _("Away") ] + } + +MODE_ALL = "all" +MODE_ONLINE = "online" +MODE_AVAILABLE = "available" +MODE_LIST= [ MODE_ONLINE, MODE_AVAILABLE, MODE_ALL ] +MODES = { + MODE_ALL : [ "All", _("All Contacts") ], + MODE_ONLINE : [ "Online", _("Online Contacts") ], + MODE_AVAILABLE : [ "Available", _("Available Contacts") ] + } + +def create(gconf_key, gconf_client, screen): + """ + Create the plugin instance + + gconf_key -- GConf key that may be used for plugin preferences + gconf_client -- GConf client instance + """ + return G15Im(gconf_client, gconf_key, screen) + +""" +Holds list of contacts for a single connection +""" +class ContactList: + + def __init__(self, list_store, conn, screen): + self.menu = list_store + self._conn = conn + self.screen = screen + self._contact_list = {} + self._conn.call_when_ready(self._connection_ready_cb) + + def deactivate(self): + pass + + def _connection_ready_cb(self, conn): + if CONNECTION_INTERFACE_SIMPLE_PRESENCE not in conn: + logger.warning("SIMPLE_PRESENCE interface not available on %s", conn.service_name) + return + if CONNECTION_INTERFACE_REQUESTS not in conn: + logger.warning("REQUESTS interface not available on %s", conn.service_name) + return + + conn[CONNECTION_INTERFACE_SIMPLE_PRESENCE].connect_to_signal( + "PresencesChanged", self._contact_presence_changed_cb) + self._ensure_channel() + + def _ensure_channel(self): + groups = ["subscribe", "publish"] + for group in groups: + requests = { + CHANNEL + ".ChannelType": CHANNEL_TYPE_CONTACT_LIST, + CHANNEL + ".TargetHandleType": HANDLE_TYPE_LIST, + CHANNEL + ".TargetID": group} + self._conn[CONNECTION_INTERFACE_REQUESTS].EnsureChannel( + requests, + reply_handler = self._ensure_channel_cb, + error_handler = self._error_cb) + + def _ensure_channel_cb(self, is_yours, channel, properties): + channel = telepathy.client.Channel( + service_name = self._conn.service_name, + object_path = channel) + DBUS_PROPERTIES = 'org.freedesktop.DBus.Properties' + channel[DBUS_PROPERTIES].Get( + CHANNEL_INTERFACE_GROUP, + 'Members', + reply_handler = self._request_contact_info, + error_handler = self._error_cb) + + def _request_contact_info(self, handles): + logger.debug("Requesting contact info for %s", str(handles)) + interfaces = [CONNECTION, + CONNECTION_INTERFACE_ALIASING, + CONNECTION_INTERFACE_SIMPLE_PRESENCE] + self._conn[CONNECTION_INTERFACE_CONTACTS].GetContactAttributes( + handles, + interfaces, + False, + reply_handler = self._get_contact_attributes_cb, + error_handler = self._error_cb) + + def _get_contact_attributes_cb(self, attributes): + logger.debug("Received contact attributes for %s", str(attributes)) + for handle, member in attributes.iteritems(): + contact_info = self._parse_member_attributes(member) + contact, alias, presence = contact_info + if handle not in self._contact_list: + self._add_contact(handle, contact, presence, str(alias)) + + def _parse_member_attributes(self, member): + contact_id, alias, presence = None, None, None + for key, value in member.iteritems(): + if key == CONNECTION + '/contact-id': + contact_id = value + elif key == CONNECTION_INTERFACE_ALIASING + '/alias': + alias = value + elif key == CONNECTION_INTERFACE_SIMPLE_PRESENCE + '/presence': + presence = value + + return (contact_id, alias, presence) + + def _add_contact(self, handle, contact, presence, alias): + logger.debug("Add contact %s (%s)", str(contact), str(handle)) + self._contact_list[handle] = contact + self.menu.add_contact(self._conn, handle, contact, presence, alias) + + def _contact_presence_changed_cb(self, presences): + logger.debug("Contact presence changed %s", str(presences)) + for handle, presence in presences.iteritems(): + if handle in self._contact_list: + self._update_contact_presence(handle, presence) + else: + self._request_contact_info([handle]) + + def _update_contact_presence(self, handle, presence): + logger.debug("Updating contact presence for %s", str(handle)) + self.menu.update_contact_presence(self._conn, handle, presence) + + def _error_cb(self, *args): + logger.error("Error happens: %s", args) + +""" +Represents a contact as a single item in a menu +""" +class ContactMenuItem(g15theme.MenuItem): + def __init__(self, conn, handle, contact, presence, alias): + g15theme.MenuItem.__init__(self, "contact-%s-%s" % ( str(conn), str(handle) ) ) + self.conn = conn + self.handle = handle + self.contact= contact + self.presence = presence + self.alias = alias + + def get_theme_properties(self): + """ + Render a single menu item + + Keyword arguments: + item -- item object + selected -- selected item object + canvas -- canvas to draw on + properties -- properties to pass to theme + attribtes -- attributes to pass to theme + + """ + item_properties = g15theme.MenuItem.get_theme_properties(self) + item_properties["item_name"] = self.alias + item_properties["item_alt"] = self._get_status_text(self.presence) + item_properties["item_type"] = "" + item_properties["item_icon"] = g15icontools.get_icon_path(self._get_status_icon_name(self.presence)) + return item_properties + + def set_presence(self, presence): + logger.debug("Setting presence of %s to %s", str(self.contact), str(presence)) + self.presence = presence + + ''' + Private + ''' + + def _get_status_text(self, presence): + key = ( presence[0], presence[1] ) + if key in STATUS_MAP: + return STATUS_MAP[key][1] + key = ( presence[0], None ) + if key in STATUS_MAP: + return STATUS_MAP[key][1] + logger.warning("Unknown presence %d = %s", presence[0], presence[1]) + return "Unknown" + + def _get_status_icon_name(self, presence): + key = ( presence[0], presence[1] ) + if key in STATUS_MAP: + return STATUS_MAP[key][0] + key = ( presence[0], None ) + if key in STATUS_MAP: + return STATUS_MAP[key][0] + logger.warning("Unknown presence %d = %s", presence[0], presence[1]) + return "dialog-warning" + +""" +Compare a single contact based on it's alias and presence +""" +def compare_contacts(a, b): + if ( a is None and b is not None ): + val = 1 + elif ( b is None and a is not None ): + val = -1 + elif ( b is None and a is None ): + val = 0 + else: + val = cmp(a.presence[0], b.presence[0]) + if val == 0: + val = cmp(a.alias, b.alias) + + return val + +""" +Theme menu for displaying all contacts across all monitored +connections. +""" +class ContactMenu(g15theme.Menu): + + def __init__(self, mode): + """ + Create the menu instance + + Keyword arguments: + screen -- screen instance + page -- page object + mode -- display mode + """ + g15theme.Menu.__init__(self, "menu") + self.mode = mode + self.on_update = None + if not self.mode: + self.mode = MODE_ONLINE + self._contacts = [] + self._contact_lists = {} + self._connections = [] + for connection in telepathy.client.Connection.get_connections(): + self._connect(connection) + + def deactivate(self): + for c in self._connections: + if c in self._contact_lists: + self._contact_lists[c].deactivate() + if c._status_changed_connection: + c._status_changed_connection.remove() + c._status_changed_connection = None + self._connections = [] + self._contact_lists = {} + self._contacts = [] + + def new_connection(self, bus_name, bus): + """ + Add a new connection to those monitored for contacts. + + Keyword arguments: + bus_name -- connection bus name + bus -- dbus instance + """ + connection = telepathy.client.Connection(bus_name, "/%s" % bus_name.replace(".", "/"), bus) + self._connect(connection) + + + def remove_connection(self, bus_name): + """ + Remove a connection given its name. All contacts attached to this connection + will be removed, and the menu reloaded + + Keyword arguments: + bus_name -- bus name + """ + for connection in list(self._connections): + if connection.service_name == bus_name: + del self._contact_lists[connection] + self._connections.remove(connection) + for item in list(self._contacts): + if item.conn == connection: + self._contacts.remove(item) + self.reload() + if self.on_update: + self.on_update() + return + + def is_connected(self, bus_name): + """ + Determine if the given connection name exists in the list of + connections currently being maintained + + Keyword arguments: + bus_name -- bus name + """ + for connection in self._connections: + if connection.service_name == bus_name: + return True + return False + + def reload(self): + """ + Build up the filter menu item list from the stored contacts. Only + contacts that are appropriate for the current mode will be added + """ + logger.debug("Reloading contacts") + c = [] + for item in self._contacts: + if self._is_presence_included(item.presence): + c.append(item) + self.sort_items(c) + self.select_first() + self.mark_dirty() + + def sort_items(self, children): + """ + Sort items based on their alias and presence + """ + self.set_children(sorted(children, cmp=compare_contacts)) + + def add_contact(self, conn, handle, contact, presence, alias): + """ + Add a new contact to the menu + + Keyword arguments: + conn -- connection + handle -- contact handle + contact -- contact id + alias - alias or real name + """ + item = ContactMenuItem(conn, handle, contact, presence, alias) + self._contacts.append(item) + self.reload() + if self.on_update: + self.on_update() + + def update_contact_presence(self, conn, handle, presence): + """ + Update a contact's presence in the list and reload + + Keyword arguments: + conn -- connection + handle -- contact handle + prescence -- presence object + """ + for row in self._contacts: + if row.handle == handle and row.conn == conn: + logger.debug("Updating presence of %s to %s", str(row.contact), str(presence)) + row.set_presence(presence) + self.selected = row + self.reload() + if self.on_update: + self.on_update() + return + logger.warning("Got presence update for unknown contact %s", str(presence)) + + + ''' + Private + ''' + + + def _connect(self, connection): + """ + Connect to the given path. Events will then be received to add new contacts + + Keyword arguments: + connection -- connection object + """ + self._contact_lists[connection] = ContactList(self, connection, self.screen) + self._connections.append(connection) + + def _is_presence_included(self, presence): + """ + Determine if presence is appropriate for the current mode + + Keyword arguments: + presence -- presence + """ + return ( self.mode == MODE_ONLINE and presence[0] != 1 ) or \ + ( self.mode == MODE_AVAILABLE and presence[0] == CONNECTION_PRESENCE_TYPE_AVAILABLE ) or \ + self.mode == MODE_ALL + +""" +Instant Messenger plugin class +""" + +class G15Im(g15plugin.G15MenuPlugin): + + def __init__(self, gconf_client, gconf_key, screen): + """ + Constructor + + Keyword arguments: + gconf_client -- GConf client instance + gconf_key -- gconf_key for storing plugin preferences + screen -- screen manager + """ + g15plugin.G15MenuPlugin.__init__(self, gconf_client, gconf_key, screen, POSSIBLE_ICON_NAMES, id, name) + + self.hidden = False + self._session_bus = dbus.SessionBus() + self._signal_handle = None + + def activate(self): + """ + Activate the plugin + """ + g15plugin.G15MenuPlugin.activate(self) + self.screen.key_handler.action_listeners.append(self) + self._signal_handle = self._session_bus.add_signal_receiver(self._name_owner_changed, + dbus_interface='org.freedesktop.DBus', + signal_name='NameOwnerChanged') + + def create_menu(self): + mode = self.gconf_client.get_string(self.gconf_key + "/mode") + return ContactMenu(mode) + + def deactivate(self): + self.screen.key_handler.action_listeners.remove(self) + g15plugin.G15MenuPlugin.deactivate(self) + if self._signal_handle: + self._session_bus.remove_signal_receiver(self._signal_handle) + + def action_performed(self, binding): + """ + Handle actions. Most actions will be handle by the abstract menu plugin class, + but we want to switch the mode when the "View" action is selected. + + Keyword arguments: + binding -- binding + """ + if binding.action == g15driver.VIEW and self.page != None and self.page.is_visible(): + mode_index = MODE_LIST.index(self.menu.mode) + 1 + if mode_index >= len(MODE_LIST): + mode_index = 0 + self.menu.mode = MODE_LIST[mode_index] + logger.info("Mode is now %s", self.menu.mode) + self.gconf_client.set_string(self.gconf_key + "/mode", self.menu.mode) + self.menu.reload() + self.screen.redraw(self.page) + return True + + def get_theme_properties(self): + props = g15plugin.G15MenuPlugin.get_theme_properties(self) + props["title"] = MODES[self.menu.mode][1] + + # Get what mode to switch to + mode_index = MODE_LIST.index(self.menu.mode) + 1 + if mode_index >= len(MODE_LIST): + mode_index = 0 + props["list"] = MODES[MODE_LIST[mode_index]][0] + return props + + """ + DBUS callbacks functions + """ + + def _name_owner_changed(self, name, old_owner, new_owner): + """ + If the change is a telepathy connection, determine if it is + a connection that is to be removed, or a new connection to + be added + """ + if name.startswith("org.freedesktop.Telepathy.Connection"): + logger.info("Telepathy Name owner changed for %s from %s to %s", + name, + old_owner, + new_owner) + connected = self.menu.is_connected(name) + if new_owner == "" and connected: + logger.info("Removing %s", name) + g15scheduler.schedule("RemoveConnection", 5.0, self.menu.remove_connection, name) + elif old_owner == "" and not connected: + logger.info("Adding %s", name) + g15scheduler.schedule("NewConnection", 5.0, self.menu.new_connection, name, self._session_bus) + diff --git a/src/plugins/impulse15/Makefile.am b/src/plugins/impulse15/Makefile.am new file mode 100644 index 0000000..57e08a4 --- /dev/null +++ b/src/plugins/impulse15/Makefile.am @@ -0,0 +1,29 @@ +SUBDIRS = themes + +plugindir = $(datadir)/gnome15/plugins/impulse15 +plugin_DATA = impulse15.ui \ + impulse15.py + +EXTRA_DIST = \ + $(plugin_DATA) + +all-local: + for M_LOCALE in @ENABLED_LOCALES@; do \ + mkdir -p i18n/$$M_LOCALE/LC_MESSAGES ; \ + if [ `ls i18n/*.po 2>/dev/null|wc -l` -gt 0 ]; then \ + for M_PO in i18n/*.po; do \ + BN=`basename $$M_PO .po`; \ + LL=`basename $$BN .$$M_LOCALE`.mo; \ + echo "$$M_PO -> $$LL"; \ + msgfmt $$M_PO --output-file i18n/$$M_LOCALE/LC_MESSAGES/$$LL; \ + done; \ + fi; \ + done + +install-exec-hook: + for M_LOCALE in @ENABLED_LOCALES@; do \ + mkdir -p $(DESTDIR)$(datadir)/gnome15/plugins/impulse15/i18n; \ + cp -pR i18n/$$M_LOCALE $(DESTDIR)$(datadir)/gnome15/plugins/impulse15/i18n; \ + done + + \ No newline at end of file diff --git a/src/plugins/impulse15/i18n/impulse15.en_GB.po b/src/plugins/impulse15/i18n/impulse15.en_GB.po new file mode 100644 index 0000000..48cac91 --- /dev/null +++ b/src/plugins/impulse15/i18n/impulse15.en_GB.po @@ -0,0 +1,106 @@ +# English translations for impulse package. +# Copyright (C) 2011 THE impulse'S COPYRIGHT HOLDER +# This file is distributed under the same license as the impulse package. +# Brett Smith , 2011. +# +msgid "" +msgstr "" +"Project-Id-Version: impulse 15\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2011-10-09 19:36+0100\n" +"PO-Revision-Date: 2011-10-09 19:39+0100\n" +"Last-Translator: Brett Smith \n" +"Language-Team: English (British)\n" +"Language: en_GB\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=ASCII\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: i18n/impulse15.glade.h:1 +msgid "Animate M-Key lights" +msgstr "Animate M-Key lights" + +#: i18n/impulse15.glade.h:2 +msgid "Audio Source" +msgstr "Audio Source" + +#: i18n/impulse15.glade.h:3 +msgid "Background" +msgstr "Background" + +#: i18n/impulse15.glade.h:4 +msgid "Bar Height" +msgstr "Bar Height" + +#: i18n/impulse15.glade.h:5 +msgid "Bar Width" +msgstr "Bar Width" + +#: i18n/impulse15.glade.h:6 +msgid "Bars" +msgstr "Bars" + +#: i18n/impulse15.glade.h:7 +msgid "Circle LCD" +msgstr "Circle LCD" + +#: i18n/impulse15.glade.h:8 +msgid "Circle Line" +msgstr "Circle Line" + +#: i18n/impulse15.glade.h:9 +msgid "Color 1" +msgstr "Color 1" + +#: i18n/impulse15.glade.h:10 +msgid "Color 2" +msgstr "Color 2" + +#: i18n/impulse15.glade.h:11 +msgid "Default" +msgstr "Default" + +#: i18n/impulse15.glade.h:12 +msgid "Disco mode" +msgstr "Disco mode" + +#: i18n/impulse15.glade.h:13 +msgid "Display" +msgstr "Display" + +#: i18n/impulse15.glade.h:14 +msgid "Foreground" +msgstr "Foreground" + +#: i18n/impulse15.glade.h:15 +msgid "Frame rate" +msgstr "Frame rate" + +#: i18n/impulse15.glade.h:16 +msgid "Impulse Preferences" +msgstr "Impulse Preferences" + +#: i18n/impulse15.glade.h:17 +msgid "Mode" +msgstr "Mode" + +#: i18n/impulse15.glade.h:18 +msgid "Original" +msgstr "Original" + +#: i18n/impulse15.glade.h:19 +msgid "Rows" +msgstr "Rows" + +#: i18n/impulse15.glade.h:20 +msgid "Screen" +msgstr "Screen" + +#: i18n/impulse15.glade.h:21 +msgid "Spacing" +msgstr "Spacing" + +#: i18n/impulse15.glade.h:22 +msgid "screen" +msgstr "screen" diff --git a/src/plugins/impulse15/i18n/impulse15.glade.h b/src/plugins/impulse15/i18n/impulse15.glade.h new file mode 100644 index 0000000..4e4ab68 --- /dev/null +++ b/src/plugins/impulse15/i18n/impulse15.glade.h @@ -0,0 +1,22 @@ +char *s = N_("Animate M-Key lights"); +char *s = N_("Audio Source"); +char *s = N_("Background"); +char *s = N_("Bar Height"); +char *s = N_("Bar Width"); +char *s = N_("Bars"); +char *s = N_("Circle LCD"); +char *s = N_("Circle Line"); +char *s = N_("Color 1"); +char *s = N_("Color 2"); +char *s = N_("Default"); +char *s = N_("Disco mode"); +char *s = N_("Display"); +char *s = N_("Foreground"); +char *s = N_("Frame rate"); +char *s = N_("Impulse Preferences"); +char *s = N_("Mode"); +char *s = N_("Original"); +char *s = N_("Rows"); +char *s = N_("Screen"); +char *s = N_("Spacing"); +char *s = N_("screen"); diff --git a/src/plugins/impulse15/i18n/impulse15.pot b/src/plugins/impulse15/i18n/impulse15.pot new file mode 100644 index 0000000..75cb130 --- /dev/null +++ b/src/plugins/impulse15/i18n/impulse15.pot @@ -0,0 +1,106 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2011-10-09 19:36+0100\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: i18n/impulse15.glade.h:1 +msgid "Animate M-Key lights" +msgstr "" + +#: i18n/impulse15.glade.h:2 +msgid "Audio Source" +msgstr "" + +#: i18n/impulse15.glade.h:3 +msgid "Background" +msgstr "" + +#: i18n/impulse15.glade.h:4 +msgid "Bar Height" +msgstr "" + +#: i18n/impulse15.glade.h:5 +msgid "Bar Width" +msgstr "" + +#: i18n/impulse15.glade.h:6 +msgid "Bars" +msgstr "" + +#: i18n/impulse15.glade.h:7 +msgid "Circle LCD" +msgstr "" + +#: i18n/impulse15.glade.h:8 +msgid "Circle Line" +msgstr "" + +#: i18n/impulse15.glade.h:9 +msgid "Color 1" +msgstr "" + +#: i18n/impulse15.glade.h:10 +msgid "Color 2" +msgstr "" + +#: i18n/impulse15.glade.h:11 +msgid "Default" +msgstr "" + +#: i18n/impulse15.glade.h:12 +msgid "Disco mode" +msgstr "" + +#: i18n/impulse15.glade.h:13 +msgid "Display" +msgstr "" + +#: i18n/impulse15.glade.h:14 +msgid "Foreground" +msgstr "" + +#: i18n/impulse15.glade.h:15 +msgid "Frame rate" +msgstr "" + +#: i18n/impulse15.glade.h:16 +msgid "Impulse Preferences" +msgstr "" + +#: i18n/impulse15.glade.h:17 +msgid "Mode" +msgstr "" + +#: i18n/impulse15.glade.h:18 +msgid "Original" +msgstr "" + +#: i18n/impulse15.glade.h:19 +msgid "Rows" +msgstr "" + +#: i18n/impulse15.glade.h:20 +msgid "Screen" +msgstr "" + +#: i18n/impulse15.glade.h:21 +msgid "Spacing" +msgstr "" + +#: i18n/impulse15.glade.h:22 +msgid "screen" +msgstr "" diff --git a/src/plugins/impulse15/impulse15.py b/src/plugins/impulse15/impulse15.py new file mode 100644 index 0000000..ead265b --- /dev/null +++ b/src/plugins/impulse15/impulse15.py @@ -0,0 +1,398 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2010 Brett Smith +# +# 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 . + +import gnome15.g15screen as g15screen +import gnome15.util.g15scheduler as g15scheduler +import gnome15.util.g15uigconf as g15uigconf +import gnome15.util.g15gconf as g15gconf +import gnome15.util.g15os as g15os +import gnome15.g15driver as g15driver +import gnome15.g15theme as g15theme +import gobject +import gtk +import os +import sys +import datetime + +# Logging +import logging +logger = logging.getLogger(__name__) + +id="impulse15" +name="Impulse15" +description="Spectrum analyser. Based on the Impulse screenlet and desktop widget" +author="Brett Smith " +copyright="Copyright (C)2010 Brett Smith, Ian Halpern" +site="https://launchpad.net/impulse.bzr" +unsupported_models = [ g15driver.MODEL_G930, g15driver.MODEL_G35 ] +has_preferences=True + +def get_source_index(source_name): + status, output = g15os.get_command_output("pacmd list-sources") + if status == 0 and len(output) > 0: + i = 0 + for line in output.split("\n"): + line = line.strip() + if line.startswith("index: "): + i = int(line[7:]) + elif line.startswith("name: <%s" % source_name): + return i + logger.warning("Audio source %s not found, default to first source", source_name) + return 0 + +def create(gconf_key, gconf_client, screen): + return G15Impulse(gconf_key, gconf_client, screen) + +def show_preferences(parent, driver, gconf_client, gconf_key): + widget_tree = gtk.Builder() + widget_tree.add_from_file(os.path.join(os.path.dirname(__file__), "impulse15.ui")) + + dialog = widget_tree.get_object("ImpulseDialog") + dialog.set_transient_for(parent) + + # Set up the audio source model + audio_source_model = widget_tree.get_object("AudioSourceModel") + status, output = g15os.get_command_output("pacmd list-sources") + source_name = "0" + if status == 0 and len(output) > 0: + i = 0 + for line in output.split("\n"): + line = line.strip() + if line.startswith("index: "): + i = int(line[7:]) + source_name = str(i) + elif line.startswith("name: "): + source_name = line[7:-1] + elif line.startswith("device.description = "): + audio_source_model.append((source_name, line[22:-1])) + else: + for i in range(0, 9): + audio_source_model.append((str(i), "Source %d" % i)) + + g15uigconf.configure_checkbox_from_gconf(gconf_client, gconf_key + "/disco", "Disco", False, widget_tree) + g15uigconf.configure_checkbox_from_gconf(gconf_client, gconf_key + "/animate_mkeys", "AnimateMKeys", False, widget_tree) + g15uigconf.configure_combo_from_gconf(gconf_client, gconf_key + "/mode", "ModeCombo", "spectrum", widget_tree) + g15uigconf.configure_combo_from_gconf(gconf_client, gconf_key + "/paint", "PaintCombo", "screen", widget_tree) + g15uigconf.configure_spinner_from_gconf(gconf_client, gconf_key + "/bars", "BarsSpinner", 16, widget_tree) + g15uigconf.configure_combo_from_gconf(gconf_client, gconf_key + "/audio_source_name", "AudioSource", source_name, widget_tree) + g15uigconf.configure_spinner_from_gconf(gconf_client, gconf_key + "/bar_width", "BarWidthSpinner", 16, widget_tree) + g15uigconf.configure_spinner_from_gconf(gconf_client, gconf_key + "/spacing", "SpacingSpinner", 0, widget_tree) + g15uigconf.configure_spinner_from_gconf(gconf_client, gconf_key + "/rows", "RowsSpinner", 16, widget_tree) + g15uigconf.configure_spinner_from_gconf(gconf_client, gconf_key + "/bar_height", "BarHeightSpinner", 2, widget_tree) + g15uigconf.configure_colorchooser_from_gconf(gconf_client, gconf_key + "/col1", "Color1", ( 255, 0, 0 ), widget_tree, default_alpha = 255) + g15uigconf.configure_colorchooser_from_gconf(gconf_client, gconf_key + "/col2", "Color2", ( 0, 0, 255 ), widget_tree, default_alpha = 255) + g15uigconf.configure_adjustment_from_gconf(gconf_client, gconf_key + "/frame_rate", "FrameRateAdjustment", 10.0, widget_tree) + g15uigconf.configure_adjustment_from_gconf(gconf_client, gconf_key + "/gain", "GainAdjustment", 1.0, widget_tree) + + if driver.get_bpp() == 0: + widget_tree.get_object("LCDTable").set_visible(False) + + + dialog.run() + dialog.hide() + +class G15ImpulsePainter(g15screen.Painter): + + def __init__(self, plugin): + g15screen.Painter.__init__(self, g15screen.BACKGROUND_PAINTER, -5000) + self.theme_module = None + self.backlight_acquisition = None + self.mkey_acquisition = None + self.mode = "default" + self.plugin = plugin + self.last_sound = datetime.datetime.now() + + def do_lights(self, audio_sample_array = None): + if not audio_sample_array: + audio_sample_array = self._get_sample() + + if self.backlight_acquisition is not None: + self.backlight_acquisition.set_value(self._col_avg(audio_sample_array)) + tot_avg = self._tot_avg(audio_sample_array) + if self.mkey_acquisition is not None: + self._set_mkey_lights(tot_avg) + return tot_avg + + def is_idle(self): + return datetime.datetime.now() > ( self.last_sound + datetime.timedelta(0, 5.0) ) + + def paint(self, canvas): + if not self.theme_module: + return + audio_sample_array = self._get_sample() + tot_avg = self.do_lights(audio_sample_array) + if tot_avg > 0: + self.last_sound = datetime.datetime.now() + + canvas.save() + self.theme_module.on_draw( audio_sample_array, canvas, self.plugin ) + canvas.restore() + + """ + Private + """ + + def _get_sample(self): + fft = False + if hasattr( self.theme_module, "fft" ) and self.theme_module.fft: + fft = True + + audio_sample_array = impulse.getSnapshot( fft ) + if self.plugin.gain != 1: + arr = [] + for a in audio_sample_array: + arr.append(a * self.plugin.gain) + audio_sample_array = arr + + return audio_sample_array + + def _col_avg(self, list): + cols = [] + each = len(list) / 3 + z = 0 + for j in range(0, 3): + t = 0 + for x in range(0, each): + t += min(255, list[z] * 340) + z += 1 + cols.append(int(t / each)) + return ( cols[0], cols[1], cols[2] ) + + def _tot_avg(self, list): + sz = len(list) + z = 0 + t = 0 + for x in range(0, sz): + t += min(255, list[z] * 340) + z += 1 + return t / sz + + def _set_mkey_lights(self, val): + if val > 200: + self.mkey_acquisition.set_value(g15driver.MKEY_LIGHT_MR | g15driver.MKEY_LIGHT_1 | g15driver.MKEY_LIGHT_2 | g15driver.MKEY_LIGHT_3) + elif val > 100: + self.mkey_acquisition.set_value(g15driver.MKEY_LIGHT_1 | g15driver.MKEY_LIGHT_2 | g15driver.MKEY_LIGHT_3) + elif val > 50: + self.mkey_acquisition.set_value(g15driver.MKEY_LIGHT_1 | g15driver.MKEY_LIGHT_2) + elif val > 25: + self.mkey_acquisition.set_value(g15driver.MKEY_LIGHT_1) + else: + self.mkey_acquisition.set_value(0) + + def _release_mkey_acquisition(self): + if self.mkey_acquisition: + self.plugin.screen.driver.release_control(self.mkey_acquisition) + self.mkey_acquisition = None + + def _release_backlight_acquisition(self): + if self.backlight_acquisition is not None: + self.plugin.screen.driver.release_control(self.backlight_acquisition) + self.backlight_acquisition = None + +class G15Impulse(): + def __init__(self, gconf_key, gconf_client, screen): + self.screen = screen + self.hidden = False + self.gconf_client = gconf_client + self.gconf_key = gconf_key + self.active = False + self.last_paint = None + self.audio_source_index = 0 + self.config_change_timer = None + + import impulse + sys.modules[ __name__ ].impulse = impulse + sys.path.append(os.path.join(os.path.dirname(__file__), "themes")) + + def set_audio_source( self, *args, **kwargs ): + impulse.setSourceIndex( self.audio_source_index ) + + def activate(self): + self.painter = G15ImpulsePainter(self) + self.width = self.screen.driver.get_size()[0] + self.height = self.screen.driver.get_size()[1] + self.active = True + self.page = None + self.visible = False + self.timer = None + self._load_config() + self.notify_handle = self.gconf_client.notify_add(self.gconf_key, self._config_changed) + self.redraw() + + def deactivate(self): + self.painter._release_backlight_acquisition() + self.painter._release_mkey_acquisition() + self.active = False + self.refresh_interval = 1.0 / 25.0 + self.gconf_client.notify_remove(self.notify_handle); + self.hide_page() + self._clear_painter() + + def hide_page(self): + self.stop_redraw() + if self.page != None: + self.screen.del_page(self.page) + self.page = None + + def on_shown(self): + self.visible = True + self._schedule_redraw() + + def on_hidden(self): + self.visible = False + self.stop_redraw() + + def stop_redraw(self): + if self.timer != None: + self.timer.cancel() + self.timer = None + g15scheduler.clear_jobs("impulseQueue") + + def destroy(self): + pass + + def paint(self, canvas): + if not self.theme_module: + return + + fft = False + if hasattr( self.theme_module, "fft" ) and self.theme_module.fft: + fft = True + + audio_sample_array = impulse.getSnapshot( fft ) + + if self.backlight_acquisition is not None: + self.backlight_acquisition.set_value(self._col_avg(audio_sample_array)) + + if self.mkey_acquisition is not None: + self._set_mkey_lights(self._tot_avg(audio_sample_array)) + + canvas.save() + self.theme_module.on_draw( audio_sample_array, canvas, self ) + canvas.restore() + + def redraw(self): + if self.screen.driver.get_bpp() == 0: + self.painter.do_lights() + else: + if self.paint_mode == "screen" and self.visible: + self.screen.redraw(self.page, queue = False) + elif self.paint_mode != "screen": + self.screen.redraw(redraw_content = False, queue = False) + self._schedule_redraw() + + """ + Private + """ + + def _schedule_redraw(self): + if self.active: + next_tick = self.refresh_interval + if self.painter.is_idle(): + next_tick = 1.0 + self.timer = g15scheduler.queue("impulseQueue", "ImpulseRedraw", next_tick, self.redraw) + + def _config_changed(self, client, connection_id, entry, args): + if self.config_change_timer is not None: + self.config_change_timer.cancel() + self.config_change_timer = g15scheduler.schedule("ConfigReload", 1, self._do_config_changed) + + def _do_config_changed(self): + self.stop_redraw() + self._load_config() + self.redraw() + self.config_change_timer = None + + def _on_load_theme (self): + if not self.painter.theme_module or self.mode != self.painter.theme_module.__name__: + self.painter.theme_module = __import__( self.mode ) + self.painter.theme_module.load_theme(self) + + def _activate_painter(self): + if not self.painter in self.screen.painters: + self.screen.painters.append(self.painter) + + def _clear_painter(self): + if self.painter in self.screen.painters: + self.screen.painters.remove(self.painter) + + def _load_config(self): + logger.info("Reloading configuration") + self.audio_source_index = get_source_index(self.gconf_client.get_string(self.gconf_key + "/audio_source_name")) + gobject.idle_add(self.set_audio_source) + self.mode = self.gconf_client.get_string(self.gconf_key + "/mode") + self.disco = g15gconf.get_bool_or_default(self.gconf_client, self.gconf_key + "/disco", False) + self.refresh_interval = 1.0 / g15gconf.get_float_or_default(self.gconf_client, self.gconf_key + "/frame_rate", 25.0) + self.gain = g15gconf.get_float_or_default(self.gconf_client, self.gconf_key + "/gain", 1.0) + logger.info("Refresh interval is %f", self.refresh_interval) + self.animate_mkeys = g15gconf.get_bool_or_default(self.gconf_client, self.gconf_key + "/animate_mkeys", False) + if self.mode == None or self.mode == "" or self.mode == "spectrum" or self.mode == "scope": + self.mode = "default" + self.paint_mode = self.gconf_client.get_string(self.gconf_key + "/paint") + if self.paint_mode == None or self.mode == "": + self.paint_mode = "screen" + self._on_load_theme() + + self.bars = self.gconf_client.get_int(self.gconf_key + "/bars") + if self.bars == 0: + self.bars = 16 + self.bar_width = self.gconf_client.get_int(self.gconf_key + "/bar_width") + if self.bar_width == 0: + self.bar_width = 16 + self.bar_height = self.gconf_client.get_int(self.gconf_key + "/bar_height") + if self.bar_height == 0: + self.bar_height = 2 + self.rows = self.gconf_client.get_int(self.gconf_key + "/rows") + if self.rows == 0: + self.rows = 16 + self.spacing = self.gconf_client.get_int(self.gconf_key + "/spacing") + self.col1 = g15gconf.get_cairo_rgba_or_default(self.gconf_client, self.gconf_key + "/col1", ( 255, 0, 0, 255 )) + self.col2 = g15gconf.get_cairo_rgba_or_default(self.gconf_client, self.gconf_key + "/col2", ( 0, 0, 255, 255 )) + + self.peak_heights = [ 0 for i in range( self.bars ) ] + + paint = self.gconf_client.get_string(self.gconf_key + "/paint") + if paint != self.last_paint and self.screen.driver.get_bpp() != 0: + self.last_paint = paint + self._clear_painter() + if paint == "screen": + if self.page == None: + self.page = g15theme.G15Page(id, self.screen, title = name, painter = self.painter.paint, on_shown = self.on_shown, on_hidden = self.on_hidden, originating_plugin = self) + self.screen.add_page(self.page) + else: + self.screen.set_priority(self.page, g15screen.PRI_HIGH, revert_after = 3.0) + elif paint == "foreground": + self.painter.place = g15screen.FOREGROUND_PAINTER + self._activate_painter() + self.hide_page() + elif paint == "background": + self.painter.place = g15screen.BACKGROUND_PAINTER + self._activate_painter() + self.hide_page() + + # Acquire the backlight control if appropriate + control = self.screen.driver.get_control_for_hint(g15driver.HINT_DIMMABLE) + if control: + if self.disco and self.painter.backlight_acquisition is None: + self.painter.backlight_acquisition = self.screen.driver.acquire_control(control) + elif not self.disco and self.painter.backlight_acquisition is not None: + self.painter._release_backlight_acquisition() + + # Acquire the M-Key lights control if appropriate + if self.animate_mkeys and self.painter.mkey_acquisition is None: + self.painter.mkey_acquisition = self.screen.driver.acquire_control_with_hint(g15driver.HINT_MKEYS) + elif not self.animate_mkeys and self.painter.mkey_acquisition is not None: + self.painter._release_mkey_acquisition() diff --git a/src/plugins/impulse15/impulse15.ui b/src/plugins/impulse15/impulse15.ui new file mode 100644 index 0000000..f071e41 --- /dev/null +++ b/src/plugins/impulse15/impulse15.ui @@ -0,0 +1,632 @@ + + + + + + 64 + 1 + 10 + + + + + + + + + + + 240 + 1 + 10 + + + 320 + 1 + 10 + + + 1 + 255 + 32 + 1 + 1 + + + 50 + 1 + 10 + + + 10 + 1 + 10 + + + + + + + + + + + default + Default + + + original + Original + + + circlelcd + Circle LCD + + + circleline + Circle Line + + + + + + + + + + + + + background + Background + + + foreground + Foreground + + + screen + Screen + + + + + 8 + 240 + 1 + 1 + + + False + 5 + Impulse Preferences + False + True + center-on-parent + dialog + + + True + False + 2 + + + True + False + 8 + + + True + False + + + True + True + 0 + + + + + True + False + AudioSourceModel + + + + 1 + + + + + True + True + 1 + + + + + True + False + 6 + 4 + 8 + 5 + + + + + + + + + True + False + 0 + Mode + + + 2 + GTK_FILL + 2 + + + + + True + False + ModeModel + + + + 0 + + + + + 2 + 4 + 2 + + + + + True + False + 0 + Display + + + 2 + 1 + 2 + GTK_FILL + 2 + + + + + True + False + PaintModel + + + + 0 + + + + + 2 + 4 + 1 + 2 + 2 + + + + + 114 + True + False + 0 + Color 1 + + + 2 + 3 + + + + + 114 + True + False + 0 + Color 2 + + + 2 + 3 + 2 + 3 + + + + + 64 + True + True + True + True + #000000000000 + + + 1 + 2 + 2 + 3 + GTK_FILL + + + + + 64 + True + True + True + True + #000000000000 + + + 3 + 4 + 2 + 3 + GTK_FILL + + + + + 114 + True + False + 0 + Bars + + + 3 + 4 + + + + + 114 + True + False + 0 + Rows + + + 2 + 3 + 3 + 4 + + + + + 64 + True + True + + 2 + True + False + False + True + True + BarsAdjustment + + + 1 + 2 + 3 + 4 + GTK_FILL + + + + + 64 + True + True + + 2 + True + False + False + True + True + RowsAdjustment + + + 3 + 4 + 3 + 4 + GTK_FILL + + + + + 114 + True + False + 0 + Bar Width + + + 4 + 5 + + + + + 114 + True + False + 0 + Bar Height + + + 2 + 3 + 4 + 5 + + + + + 64 + True + True + + 2 + True + False + False + True + True + BarWidthAdjustment + + + 1 + 2 + 4 + 5 + GTK_FILL + + + + + 64 + True + True + + 2 + True + False + False + True + True + BarHeightAdjustment + + + 3 + 4 + 4 + 5 + GTK_FILL + + + + + 64 + True + True + + True + False + False + True + True + SpacingAdjustment + + + 1 + 2 + 5 + 6 + GTK_FILL + + + + + 114 + True + False + 0 + Spacing + + + 5 + 6 + GTK_FILL + + + + + True + True + 8 + 2 + + + + + False + False + 0 + + + + + True + False + + + Disco mode + True + True + False + True + + + True + True + 0 + + + + + Animate M-Key lights + True + True + False + True + + + True + True + 1 + + + + + False + False + 8 + 1 + + + + + True + False + 2 + 2 + 4 + 4 + + + True + False + 0 + Frame rate + + + GTK_FILL + + + + + True + True + FrameRateAdjustment + on + 2 + 2 + + + 1 + 2 + + + + + True + False + 0 + Gain + + + 1 + 2 + GTK_FILL + + + + + True + True + GainAdjustment + on + 2 + 2 + + + 1 + 2 + 1 + 2 + + + + + True + True + 2 + + + + + + + + True + False + end + + + gtk-close + True + True + True + True + + + False + False + 0 + + + + + False + True + end + 5 + + + + + + button9 + + + + 100 + 1 + 10 + + diff --git a/src/plugins/impulse15/themes/Makefile.am b/src/plugins/impulse15/themes/Makefile.am new file mode 100644 index 0000000..c9fefd9 --- /dev/null +++ b/src/plugins/impulse15/themes/Makefile.am @@ -0,0 +1,2 @@ +SUBDIRS = circlelcd circleline default original + diff --git a/src/plugins/impulse15/themes/circlelcd/Makefile.am b/src/plugins/impulse15/themes/circlelcd/Makefile.am new file mode 100644 index 0000000..cf160bf --- /dev/null +++ b/src/plugins/impulse15/themes/circlelcd/Makefile.am @@ -0,0 +1,5 @@ +plugindir = $(datadir)/gnome15/plugins/impulse15/themes/circlelcd +plugin_DATA = __init__.py + +EXTRA_DIST = \ + $(plugin_DATA) diff --git a/src/plugins/impulse15/themes/circlelcd/__init__.py b/src/plugins/impulse15/themes/circlelcd/__init__.py new file mode 100644 index 0000000..ae55247 --- /dev/null +++ b/src/plugins/impulse15/themes/circlelcd/__init__.py @@ -0,0 +1,53 @@ +import math + +fft = True + +def load_theme( screenlet ): + + ''' + screenlet.resize( 300, 300 ) + + screenlet.add_option( ColorOption( + 'Impulse', 'cc', + cc, 'Color', + 'Example options group using color' + ) ) + ''' + + +def on_after_set_attribute ( self, name, value, screenlet ): + setattr( self, name, value ) + +def on_draw( audio_sample_array, cr, screenlet ): + + l = len( audio_sample_array ) + + width, height = ( screenlet.width, screenlet.height ) + + + n_bars = screenlet.bars + + cr.set_line_width( screenlet.bar_width ) + + for i in range( 0, l, l / n_bars ): + bar_amp_norm = audio_sample_array[ i ] + + + bar_height = ( bar_amp_norm * ( screenlet.width / 2 ) + screenlet.bar_width ) * ( screenlet.bar_height / 10.0 ) + + cc = screenlet.col2 + cr.set_source_rgba( cc[ 0 ], cc[ 1 ], cc[ 2 ], cc[ 3 ] ) + for j in range( 0, int( bar_height / 5 ), max(max(1, screenlet.spacing) / 5, 1) ): + cr.arc( + width / 2, + height / 2, + 20 + j * screenlet.bar_width, + ( math.pi*2 / n_bars ) * ( i / ( l / n_bars ) ), + ( math.pi*2 / n_bars ) * ( i / ( l / n_bars ) + 1 ) - .05 + ) + + cr.stroke( ) + + if j == 0: + cc = screenlet.col1 + cr.set_source_rgba( cc[ 0 ], cc[ 1 ], cc[ 2 ], cc[ 3 ] ) diff --git a/src/plugins/impulse15/themes/circlelcd/theme.conf b/src/plugins/impulse15/themes/circlelcd/theme.conf new file mode 100644 index 0000000..29279c8 --- /dev/null +++ b/src/plugins/impulse15/themes/circlelcd/theme.conf @@ -0,0 +1,7 @@ +# An example of theme-configuration file + +[Theme] +name=dev +author=Ian Halpern +version=1.0 +info=The original theme diff --git a/src/plugins/impulse15/themes/circleline/Makefile.am b/src/plugins/impulse15/themes/circleline/Makefile.am new file mode 100644 index 0000000..0ed98d7 --- /dev/null +++ b/src/plugins/impulse15/themes/circleline/Makefile.am @@ -0,0 +1,5 @@ +plugindir = $(datadir)/gnome15/plugins/impulse15/themes/circleline +plugin_DATA = __init__.py + +EXTRA_DIST = \ + $(plugin_DATA) diff --git a/src/plugins/impulse15/themes/circleline/__init__.py b/src/plugins/impulse15/themes/circleline/__init__.py new file mode 100644 index 0000000..2004abf --- /dev/null +++ b/src/plugins/impulse15/themes/circleline/__init__.py @@ -0,0 +1,66 @@ +import math + +fft=True + +def load_theme( screenlet ): + ''' + screenlet.resize( 300, 300 ) + + screenlet.add_option( ColorOption( + 'Impulse', 'co', + co, 'Color', + 'Example options group using color' + ) ) + ''' + +def on_after_set_attribute ( self, name, value, screenlet ): + setattr( self, name, value ) + +def on_draw( audio_sample_array, cr, screenlet ): + + l = len( audio_sample_array ) + + width, height = ( screenlet.width, screenlet.height ) + + co = screenlet.col1 + cr.set_source_rgba( co[ 0 ], co[ 1 ], co[ 2 ], co[ 3 ] ) + + n_bars = screenlet.bars + + cr.set_line_width( screenlet.bar_width ) + + h = screenlet.bar_height + + fx = 0 + fy = 0 + + for i in range( 0, l, l / n_bars ): + + bar_amp_norm = audio_sample_array[ i ] + + bar_height = bar_amp_norm * 100 + + + a = ( math.pi*2 / n_bars ) * ( i / ( l / n_bars ) ) + + x = ( math.sin( a ) * ( h + bar_height ) + width / 2 ) + y = ( math.cos( a ) * ( h + bar_height ) + height / 2 ) + + if not i: + fx = x + fy = y + cr.move_to( x, y ) + + cr.curve_to( + x, y, + x, y, + x, y + ) + + cr.curve_to( + fx, fy, + fx, fy, + fx, fy + ) + + cr.stroke( ) diff --git a/src/plugins/impulse15/themes/circleline/theme.conf b/src/plugins/impulse15/themes/circleline/theme.conf new file mode 100644 index 0000000..5d123d7 --- /dev/null +++ b/src/plugins/impulse15/themes/circleline/theme.conf @@ -0,0 +1,7 @@ +# An example of theme-configuration file + +[Theme] +name=Circle Line +author=Ian Halpern +version=1.0 +info= diff --git a/src/plugins/impulse15/themes/default/Makefile.am b/src/plugins/impulse15/themes/default/Makefile.am new file mode 100644 index 0000000..40ebf63 --- /dev/null +++ b/src/plugins/impulse15/themes/default/Makefile.am @@ -0,0 +1,5 @@ +plugindir = $(datadir)/gnome15/plugins/impulse15/themes/default +plugin_DATA = __init__.py + +EXTRA_DIST = \ + $(plugin_DATA) diff --git a/src/plugins/impulse15/themes/default/__init__.py b/src/plugins/impulse15/themes/default/__init__.py new file mode 100644 index 0000000..353a788 --- /dev/null +++ b/src/plugins/impulse15/themes/default/__init__.py @@ -0,0 +1,67 @@ +peak_heights = [ 0 for i in range( 256 ) ] +peak_acceleration = [ 0.0 for i in range( 256 ) ] +fft = True + +def load_theme ( screenlet): + pass + +def on_draw ( audio_sample_array, cr, screenlet ): + n_cols = screenlet.bars + col_width = screenlet.bar_width + col_spacing = screenlet.spacing + bar_color = screenlet.col1 + row_height = screenlet.bar_height + n_rows = screenlet.rows + row_spacing = screenlet.spacing + peak_color = screenlet.col2 + freq = len( audio_sample_array ) / n_cols + actual_cols = ( len( audio_sample_array ) / freq ) + 1 + + total_width = ( actual_cols * ( col_width + col_spacing ) ) - col_spacing + +# print "Total width = %d, Cols = %d, width = %d, spacing = %d, freq = %f, len = %d, actual_cols = %d" % ( total_width, n_cols, col_width, col_spacing, freq, len(audio_sample_array), actual_cols ) + + cr.save() + cr.translate( ( screenlet.width - total_width ) / 2, 0) + + for i in range( 0, len( audio_sample_array ), freq ): + + col = i / freq + rows = int( audio_sample_array[ i ] * ( n_rows - 2 ) ) + + cr.set_source_rgba( bar_color[ 0 ], bar_color[ 1 ], bar_color[ 2 ], bar_color[ 3 ] ) + + if rows > peak_heights[ i ]: + peak_heights[ i ] = rows + peak_acceleration[ i ] = 0.0 + else: + peak_acceleration[ i ] += .1 + peak_heights[ i ] -= peak_acceleration[ i ] + + if peak_heights[ i ] < 0: + peak_heights[ i ] = 0 + + for row in range( 0, rows ): + + cr.rectangle( + col * ( col_width + col_spacing ), + screenlet.height - row * ( row_height + row_spacing ), + col_width, -row_height + ) + + cr.fill( ) + + cr.set_source_rgba( peak_color[ 0 ], peak_color[ 1 ], peak_color[ 2 ], peak_color[ 3 ] ) + + cr.rectangle( + col * ( col_width + col_spacing ), + screenlet.height - peak_heights[ i ] * ( row_height + row_spacing ), + col_width, -row_height + ) + + cr.fill( ) + + cr.fill( ) + cr.stroke( ) + cr.restore() + diff --git a/src/plugins/impulse15/themes/default/theme.conf b/src/plugins/impulse15/themes/default/theme.conf new file mode 100644 index 0000000..a12e64c --- /dev/null +++ b/src/plugins/impulse15/themes/default/theme.conf @@ -0,0 +1,9 @@ +# An example of theme-configuration file + +[Theme] +name=default +author=Ian Halpern +version=1.0 +info=The default theme + +fft=True diff --git a/src/plugins/impulse15/themes/original/Makefile.am b/src/plugins/impulse15/themes/original/Makefile.am new file mode 100644 index 0000000..899900e --- /dev/null +++ b/src/plugins/impulse15/themes/original/Makefile.am @@ -0,0 +1,5 @@ +plugindir = $(datadir)/gnome15/plugins/impulse15/themes/original +plugin_DATA = __init__.py + +EXTRA_DIST = \ + $(plugin_DATA) diff --git a/src/plugins/impulse15/themes/original/__init__.py b/src/plugins/impulse15/themes/original/__init__.py new file mode 100644 index 0000000..c0877e0 --- /dev/null +++ b/src/plugins/impulse15/themes/original/__init__.py @@ -0,0 +1,44 @@ +fft = True + +def load_theme( screenlet ): + pass + +def on_draw( audio_sample_array, cr, screenlet ): + + l = len( audio_sample_array ) + + width, height = ( screenlet.width, screenlet.height ) + + # start drawing spectrum + + + n_bars = screenlet.bars + bar_width = screenlet.bar_width + bar_spacing = screenlet.spacing + + + freq = len( audio_sample_array ) / n_bars + actual_cols = ( len( audio_sample_array ) / freq ) + 1 + total_width = ( actual_cols * ( bar_width + bar_spacing ) ) - bar_spacing + cr.translate( ( screenlet.width - total_width ) / 2, 0) + + for i in range( 0, l, l / n_bars ): + + bar_amp_norm = audio_sample_array[ i ] + + bar_height = ( bar_amp_norm * height + 2 ) * ( screenlet.bar_height / 10.0 ) + + cr.rectangle( + ( bar_width + bar_spacing ) * ( i / ( l / n_bars ) ), + height / 2 - bar_height / 2, + bar_width, + bar_height + ) + + co = screenlet.col1 + cr.set_source_rgba( co[ 0 ], co[ 1 ], co[ 2 ], co[ 3 ] ) + cr.fill_preserve() + co = screenlet.col2 + cr.set_source_rgba( co[ 0 ], co[ 1 ], co[ 2 ], co[ 3 ] ) + cr.stroke() + diff --git a/src/plugins/impulse15/themes/original/theme.conf b/src/plugins/impulse15/themes/original/theme.conf new file mode 100644 index 0000000..b80dd18 --- /dev/null +++ b/src/plugins/impulse15/themes/original/theme.conf @@ -0,0 +1,7 @@ +# An example of theme-configuration file + +[Theme] +name=original +author=Ian Halpern +version=1.0 +info=The original theme diff --git a/src/plugins/indicator-messages/Makefile.am b/src/plugins/indicator-messages/Makefile.am new file mode 100644 index 0000000..8b58f19 --- /dev/null +++ b/src/plugins/indicator-messages/Makefile.am @@ -0,0 +1,8 @@ +plugindir = $(datadir)/gnome15/plugins/indicator-messages +plugin_DATA = indicator-messages.py \ + indicator-messages.ui \ + mono-mail-new.gif \ + mono-mail-error.gif + +EXTRA_DIST = \ + $(plugin_DATA) \ No newline at end of file diff --git a/src/plugins/indicator-messages/default/Makefile.am b/src/plugins/indicator-messages/default/Makefile.am new file mode 100644 index 0000000..8effca2 --- /dev/null +++ b/src/plugins/indicator-messages/default/Makefile.am @@ -0,0 +1,12 @@ +themedir = $(datadir)/gnome15/plugins/indicator-messages/default +theme_DATA = indicator_messages_default_common.py \ + default.svg \ + default-entry.svg \ + indicator_messages_default_default.py \ + g19.svg \ + g19-entry.svg \ + g19-separator.svg \ + indicator_messages_default_g19.py + +EXTRA_DIST = \ + $(theme_DATA) diff --git a/src/plugins/indicator-messages/default/default-entry.svg b/src/plugins/indicator-messages/default/default-entry.svg new file mode 100644 index 0000000..1ae3abb --- /dev/null +++ b/src/plugins/indicator-messages/default/default-entry.svg @@ -0,0 +1,150 @@ + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + ${item_name} + + + ${item_name} + + + diff --git a/src/plugins/indicator-messages/default/default.svg b/src/plugins/indicator-messages/default/default.svg new file mode 100644 index 0000000..94dec73 --- /dev/null +++ b/src/plugins/indicator-messages/default/default.svg @@ -0,0 +1,91 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + ${title} + + + diff --git a/src/plugins/indicator-messages/default/g19-entry.svg b/src/plugins/indicator-messages/default/g19-entry.svg new file mode 100644 index 0000000..ba3d128 --- /dev/null +++ b/src/plugins/indicator-messages/default/g19-entry.svg @@ -0,0 +1,294 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + ${item_name} + + ${item_alt} + + + + + ${item_name} + ${item_alt} + + diff --git a/src/plugins/indicator-messages/default/g19-separator.svg b/src/plugins/indicator-messages/default/g19-separator.svg new file mode 100644 index 0000000..950d0a4 --- /dev/null +++ b/src/plugins/indicator-messages/default/g19-separator.svg @@ -0,0 +1,210 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + diff --git a/src/plugins/indicator-messages/default/g19.svg b/src/plugins/indicator-messages/default/g19.svg new file mode 100644 index 0000000..ecf80b3 --- /dev/null +++ b/src/plugins/indicator-messages/default/g19.svg @@ -0,0 +1,166 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + ${title} + + diff --git a/src/plugins/indicator-messages/default/indicator_messages_default_common.py b/src/plugins/indicator-messages/default/indicator_messages_default_common.py new file mode 100644 index 0000000..7f30bda --- /dev/null +++ b/src/plugins/indicator-messages/default/indicator_messages_default_common.py @@ -0,0 +1,56 @@ +import gnome15.g15_theme as g15theme +import gnome15.util.g15cairo as g15cairo +import gnome15.util.g15icontools as g15icontools +import os +import time + +class Theme(): + + def __init__(self, screen, theme): + self.theme = theme + self.entry_theme = g15theme.G15Theme(os.path.dirname(__file__), screen, "entry") + self.separator_theme = g15theme.G15Theme(os.path.dirname(__file__), screen, "separator") + self.screen = screen + + def paint_foreground(self, canvas, properties, attributes, args, x_offset, y_offset): + items = attributes["items"] + selected = attributes["selected"] + + canvas.save() + y = y_offset + + # How many complete items fit on the screen? Make sure the selected item is visible + # TODO again, this needs turning into a re-usable component - see menu and rss + item_height = self.entry_theme.bounds[3] + max_items = int( ( self.screen.height - y ) / item_height) + if selected != None: + sel_index = items.index(selected) + diff = sel_index + 1 - max_items + if diff > 0: + y -= diff * item_height + canvas.rectangle(x_offset, y_offset, self.screen.width - ( x_offset * 2 ), self.screen.height - y_offset) + canvas.clip() + + canvas.translate(x_offset, y) + for item in items: + item_properties = {} + if selected == item: + item_properties["item_selected"] = True + item_properties["item_name"] = item.get_label() + item_properties["item_alt"] = item.get_right_side_text() + item_properties["item_type"] = item.get_type() + icon_name = item.get_icon_name() + if icon_name != None: + item_properties["item_icon"] = g15cairo.load_surface_from_file(g15icontools.get_icon_path(self.screen.applet.conf_client, icon_name)) + else: + item_properties["item_icon"] = item.get_icon() + + if item.get_type() == "separator": + self.separator_theme.draw(canvas, item_properties) + else: + self.entry_theme.draw(canvas, item_properties) + canvas.translate(0, item_height) + y += item_height + if y + item_height > self.theme.screen.height: + break + canvas.restore() \ No newline at end of file diff --git a/src/plugins/indicator-messages/default/indicator_messages_default_default.py b/src/plugins/indicator-messages/default/indicator_messages_default_default.py new file mode 100644 index 0000000..1a8a6fa --- /dev/null +++ b/src/plugins/indicator-messages/default/indicator_messages_default_default.py @@ -0,0 +1,10 @@ +import indicator_messages_default_common + +class Theme(indicator_messages_default_common.Theme): + + + def __init__(self, screen, theme): + indicator_messages_default_common.Theme.__init__(self, screen, theme) + + def paint_foreground(self, canvas, properties, attributes, args): + indicator_messages_default_common.Theme.paint_foreground(self, canvas, properties, attributes, args, 1, 12) \ No newline at end of file diff --git a/src/plugins/indicator-messages/default/indicator_messages_default_g19.py b/src/plugins/indicator-messages/default/indicator_messages_default_g19.py new file mode 100644 index 0000000..8ff2179 --- /dev/null +++ b/src/plugins/indicator-messages/default/indicator_messages_default_g19.py @@ -0,0 +1,9 @@ +import indicator_messages_default_common + +class Theme(indicator_messages_default_common.Theme): + + def __init__(self, screen, theme): + indicator_messages_default_common.Theme.__init__(self, screen, theme) + + def paint_foreground(self, canvas, properties, attributes, args): + indicator_messages_default_common.Theme.paint_foreground(self, canvas, properties, attributes, args, 0, 42) \ No newline at end of file diff --git a/src/plugins/indicator-messages/i18n/indicator-messages.en_GB.po b/src/plugins/indicator-messages/i18n/indicator-messages.en_GB.po new file mode 100644 index 0000000..888ba8c --- /dev/null +++ b/src/plugins/indicator-messages/i18n/indicator-messages.en_GB.po @@ -0,0 +1,46 @@ +# English translations for gnome15-plugins package. +# Copyright (C) 2012 THE gnome15-plugins'S COPYRIGHT HOLDER +# This file is distributed under the same license as the gnome15-plugins package. +# Automatically generated, 2012. +# +msgid "" +msgstr "" +"Project-Id-Version: gnome15-plugins 0.8.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: 2012-01-08 11:23+0000\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: en_GB\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=ASCII\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: i18n/indicator-messages.glade.h:1 +msgid "Indicator Messages Preferences" +msgstr "Indicator Messages Preferences" + +#: i18n/indicator-messages.glade.h:2 +msgid "Raise page when menu changes" +msgstr "Raise page when menu changes" + +#: i18n/indicator-messages.glade.h:3 +msgid "center" +msgstr "center" + +#: i18n/indicator-messages.glade.h:4 +msgid "scale" +msgstr "scale" + +#: i18n/indicator-messages.glade.h:5 +msgid "stretch" +msgstr "stretch" + +#: i18n/indicator-messages.glade.h:6 +msgid "tile" +msgstr "tile" + +#: i18n/indicator-messages.glade.h:7 +msgid "zoom" +msgstr "zoom" diff --git a/src/plugins/indicator-messages/i18n/indicator-messages.glade.h b/src/plugins/indicator-messages/i18n/indicator-messages.glade.h new file mode 100644 index 0000000..61db099 --- /dev/null +++ b/src/plugins/indicator-messages/i18n/indicator-messages.glade.h @@ -0,0 +1,7 @@ +char *s = N_("Indicator Messages Preferences"); +char *s = N_("Raise page when menu changes"); +char *s = N_("center"); +char *s = N_("scale"); +char *s = N_("stretch"); +char *s = N_("tile"); +char *s = N_("zoom"); diff --git a/src/plugins/indicator-messages/i18n/indicator-messages.pot b/src/plugins/indicator-messages/i18n/indicator-messages.pot new file mode 100644 index 0000000..243e48b --- /dev/null +++ b/src/plugins/indicator-messages/i18n/indicator-messages.pot @@ -0,0 +1,46 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: i18n/indicator-messages.glade.h:1 +msgid "Indicator Messages Preferences" +msgstr "" + +#: i18n/indicator-messages.glade.h:2 +msgid "Raise page when menu changes" +msgstr "" + +#: i18n/indicator-messages.glade.h:3 +msgid "center" +msgstr "" + +#: i18n/indicator-messages.glade.h:4 +msgid "scale" +msgstr "" + +#: i18n/indicator-messages.glade.h:5 +msgid "stretch" +msgstr "" + +#: i18n/indicator-messages.glade.h:6 +msgid "tile" +msgstr "" + +#: i18n/indicator-messages.glade.h:7 +msgid "zoom" +msgstr "" diff --git a/src/plugins/indicator-messages/indicator-messages.py b/src/plugins/indicator-messages/indicator-messages.py new file mode 100644 index 0000000..8d4be2f --- /dev/null +++ b/src/plugins/indicator-messages/indicator-messages.py @@ -0,0 +1,258 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2010 Brett Smith +# +# 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 . + +import gnome15.g15locale as g15locale +_ = g15locale.get_translation("indicator-messages", modfile = __file__).ugettext + +import gnome15.g15globals as g15globals +import gnome15.g15screen as g15screen +import gnome15.util.g15convert as g15convert +import gnome15.util.g15uigconf as g15uigconf +import gnome15.util.g15gconf as g15gconf +import gnome15.util.g15cairo as g15cairo +import gnome15.util.g15icontools as g15icontools +import gnome15.g15theme as g15theme +import gnome15.g15driver as g15driver +import gnome15.g15plugin as g15plugin +import gobject +import time +import dbus +import os +import gtk +from PIL import Image +import gnome15.dbusmenu as dbusmenu + +from lxml import etree + +import logging +logger = logging.getLogger(__name__) + +# Only works in Unity +if not "XDG_CURRENT_DESKTOP" in os.environ or os.environ["XDG_CURRENT_DESKTOP"] != "Unity": + raise Exception("Only works in Ubuntu Unity desktop") + +# Plugin details - All of these must be provided +id="indicator-messages" +name=_("Indicator Messages") +description=_("Indicator that shows waiting messages.") +author="Brett Smith " +copyright=_("Copyright (C)2010 Brett Smith") +site="http://www.russo79.com/gnome15" +has_preferences=True +unsupported_models = [ g15driver.MODEL_G110, g15driver.MODEL_G11, g15driver.MODEL_G930, g15driver.MODEL_G35 ] +actions={ + g15driver.PREVIOUS_SELECTION : _("Previous item"), + g15driver.NEXT_SELECTION : _("Next item"), + g15driver.NEXT_PAGE : _("Next page"), + g15driver.PREVIOUS_PAGE : _("Previous page"), + g15driver.SELECT : _("Activate item") + } + +def create(gconf_key, gconf_client, screen): + return G15IndicatorMessages(gconf_client, gconf_key, screen) + +''' +Indicator Messages DBUSMenu property names +''' + +APP_RUNNING = "app-running" +INDICATOR_LABEL = "indicator-label" +INDICATOR_ICON = "indicator-icon" +RIGHT_SIDE_TEXT = "right-side-text" + +''' +Indicator Messages DBUSMenu types +''' +TYPE_APPLICATION_ITEM = "application-item" +TYPE_INDICATOR_ITEM = "indicator-item" + + +def show_preferences(parent, driver, gconf_client, gconf_key): + widget_tree = gtk.Builder() + widget_tree.add_from_file(os.path.join(os.path.dirname(__file__), "indicator-messages.ui")) + dialog = widget_tree.get_object("IndicatorMessagesDialog") + dialog.set_transient_for(parent) + g15uigconf.configure_checkbox_from_gconf(gconf_client, "%s/raise" % gconf_key, "RaisePageCheckbox", True, widget_tree) + dialog.run() + dialog.hide() + +class IndicatorMessagesMenuEntry(dbusmenu.DBUSMenuEntry): + def __init__(self, id, properties, menu): + dbusmenu.DBUSMenuEntry.__init__(self, id, properties, menu) + + def set_properties(self, properties): + dbusmenu.DBUSMenuEntry.set_properties(self, properties) + if self.type == TYPE_INDICATOR_ITEM and INDICATOR_LABEL in self.properties: + self.label = self.properties[INDICATOR_LABEL] + if self.type == TYPE_INDICATOR_ITEM: + self.icon = self.properties[INDICATOR_ICON] if INDICATOR_ICON in self.properties else None + + def get_alt_label(self): + return self.properties[RIGHT_SIDE_TEXT] if RIGHT_SIDE_TEXT in self.properties else "" + + def is_app_running(self): + return APP_RUNNING in self.properties and self.properties[APP_RUNNING] + +class IndicatorMessagesMenu(dbusmenu.DBUSMenu): + def __init__(self, session_bus, on_change = None): + try: + dbusmenu.DBUSMenu.__init__(self, session_bus, "com.canonical.indicator.messages", "/com/canonical/indicator/messages/menu", "com.canonical.dbusmenu", on_change, True) + except dbus.DBusException as dbe: + logger.debug("Could not create DBUS menu, trying alternative", exc_info = dbe) + dbusmenu.DBUSMenu.__init__(self, session_bus, "org.ayatana.indicator.messages", "/org/ayatana/indicator/messages/menu", "org.ayatana.dbusmenu", on_change, False) + + def create_entry(self, id, properties): + return IndicatorMessagesMenuEntry(id, properties, self) + + +class G15IndicatorMessages(g15plugin.G15MenuPlugin): + + def __init__(self, gconf_client, gconf_key, screen): + g15plugin.G15MenuPlugin.__init__(self, gconf_client, gconf_key, screen, ["indicator-messages"], id, name) + self._hide_timer = None + self._session_bus = None + self._gconf_client = gconf_client + self._session_bus = dbus.SessionBus() + + def activate(self): + self._status_icon = None + self._raise_timer = None + self._attention = False + self._light_control = None + + g15plugin.G15MenuPlugin.activate(self) + + # Start listening for events + if self._messages_menu.natty: + self._session_bus.add_signal_receiver(self._icon_changed, dbus_interface = "com.canonical.indicator.messages.service", signal_name = "IconChanged") + self._session_bus.add_signal_receiver(self._attention_changed, dbus_interface = "com.canonical.indicator.messages.service", signal_name = "AttentionChanged") + else: + self._session_bus.add_signal_receiver(self._icon_changed, dbus_interface = "org.ayatana.indicator.messages.service", signal_name = "IconChanged") + self._session_bus.add_signal_receiver(self._attention_changed, dbus_interface = "org.ayatana.indicator.messages.service", signal_name = "AttentionChanged") + + def create_menu(self): + self._messages_menu = IndicatorMessagesMenu(self._session_bus) + self._messages_menu.on_change = self._menu_changed + self._check_status() + return g15theme.DBusMenu(self._messages_menu) + + def deactivate(self): + g15plugin.G15MenuPlugin.deactivate(self) + self._stop_blink() + if self._messages_menu.natty: + self._session_bus.remove_signal_receiver(self._icon_changed, dbus_interface = "com.canonical.indicator.messages.service", signal_name = "IconChanged") + self._session_bus.remove_signal_receiver(self._attention_changed, dbus_interface = "com.canonical.indicator.messages.service", signal_name = "AttentionChanged") + else: + self._session_bus.remove_signal_receiver(self._icon_changed, dbus_interface = "org.ayatana.indicator.messages.service", signal_name = "IconChanged") + self._session_bus.remove_signal_receiver(self._attention_changed, dbus_interface = "org.ayatana.indicator.messages.service", signal_name = "AttentionChanged") + + def create_page(self): + page = g15plugin.G15MenuPlugin.create_page(self) + page.panel_painter = self._paint_panel + return page + + def get_theme_properties(self): + return { + "title" : _("Messages"), + "alt_title" : "", + "icon" : g15icontools.get_icon_path("indicator-messages-new" if self._attention else "indicator-messages"), + "attention": self._attention + } + + ''' + Messages Service callbacks + ''' + + def _icon_changed(self, new_icon): + pass + + def _attention_changed(self, attention): + self._attention = attention + if self._attention == 1: + self._start_blink() + if self.screen.driver.get_bpp() == 1: + self.thumb_icon = g15cairo.load_surface_from_file(os.path.join(os.path.dirname(__file__), "mono-mail-new.gif")) + else: + self.thumb_icon = g15cairo.load_surface_from_file(g15icontools.get_icon_path("indicator-messages-new")) + self._popup() + else: + self._stop_blink() + if self.screen.driver.get_bpp() == 16: + self.thumb_icon = g15cairo.load_surface_from_file(g15icontools.get_icon_path("indicator-messages")) + self.screen.redraw() + + def _menu_changed(self, menu = None, property = None, value = None): +# self._messages_menu.menu_changed(menu, property, value) + self._popup() + + def _check_status(self): + """ + indicator-messages replaces indicator-me from Oneiric, so we get the current status icon if available + to show that on the panel too + """ + self._status_icon = None + for c in self._messages_menu.menu_map: + menu_entry = self._messages_menu.menu_map[c] + if menu_entry.toggle_type == dbusmenu.TOGGLE_TYPE_RADIO and menu_entry.toggle_state == 1: + icon_name = menu_entry.get_icon_name() + if icon_name is not None and \ + icon_name in [ "user-available", "user-away", + "user-busy", "user-offline", + "user-invisible", "user-indeterminate" ]: + self._status_icon = g15cairo.load_surface_from_file(g15icontools.get_icon_path(icon_name)) + + ''' + Private + ''' + + def _start_blink(self): + if not self._light_control: + self._light_control = self.screen.driver.acquire_control_with_hint(g15driver.HINT_MKEYS, val = g15driver.MKEY_LIGHT_1 | g15driver.MKEY_LIGHT_2 | g15driver.MKEY_LIGHT_3) + self._light_control.blink(off_val = self._get_mkey_value) + + def _get_mkey_value(self): + return g15driver.get_mask_for_memory_bank(self.screen.get_memory_bank()) + + def _stop_blink(self): + if self._light_control: + self.screen.driver.release_control(self._light_control) + self._light_control = None + + def _popup(self): + self._check_status() + if g15gconf.get_bool_or_default(self.gconf_client,"%s/raise" % self.gconf_key, True): + if not self.page.is_visible(): + self._raise_timer = self.screen.set_priority(self.page, g15screen.PRI_HIGH, revert_after = 4.0) + self.screen.redraw(self.page) + else: + self._reset_raise() + + def _reset_raise(self): + ''' + Reset the timer if the page is already visible because of a timer + ''' + if self.screen.is_on_timer(self.page): + self._raise_timer = self.screen.set_priority(self.page, g15screen.PRI_HIGH, revert_after = 4.0) + self.screen.redraw(self.page) + + def _paint_panel(self, canvas, allocated_size, horizontal): + if self.page != None: + t = 0 + if self.thumb_icon != None and self._attention == 1: + t += g15cairo.paint_thumbnail_image(allocated_size, self.thumb_icon, canvas) + if self._status_icon != None: + t += g15cairo.paint_thumbnail_image(allocated_size, self._status_icon, canvas) + return t diff --git a/src/plugins/indicator-messages/indicator-messages.ui b/src/plugins/indicator-messages/indicator-messages.ui new file mode 100644 index 0000000..bc636e2 --- /dev/null +++ b/src/plugins/indicator-messages/indicator-messages.ui @@ -0,0 +1,101 @@ + + + + + + + False + 5 + Indicator Messages Preferences + False + True + center-on-parent + dialog + + + True + False + 2 + + + True + False + 4 + + + Raise page when menu changes + True + True + False + True + + + True + True + 0 + + + + + False + False + 0 + + + + + True + False + end + + + gtk-close + True + True + True + True + + + False + False + 0 + + + + + False + True + end + 1 + + + + + + button9 + + + + + + + + + + zoom + + + tile + + + center + + + scale + + + stretch + + + + diff --git a/src/plugins/indicator-messages/mono-mail-error.gif b/src/plugins/indicator-messages/mono-mail-error.gif new file mode 100644 index 0000000..a616bfa Binary files /dev/null and b/src/plugins/indicator-messages/mono-mail-error.gif differ diff --git a/src/plugins/indicator-messages/mono-mail-new.gif b/src/plugins/indicator-messages/mono-mail-new.gif new file mode 100644 index 0000000..eb3dae3 Binary files /dev/null and b/src/plugins/indicator-messages/mono-mail-new.gif differ diff --git a/src/plugins/keyhelp/Makefile.am b/src/plugins/keyhelp/Makefile.am new file mode 100644 index 0000000..28c5211 --- /dev/null +++ b/src/plugins/keyhelp/Makefile.am @@ -0,0 +1,6 @@ +SUBDIRS = default +plugindir = $(datadir)/gnome15/plugins/keyhelp +plugin_DATA = keyhelp.py + +EXTRA_DIST = \ + $(plugin_DATA) \ No newline at end of file diff --git a/src/plugins/keyhelp/default/Makefile.am b/src/plugins/keyhelp/default/Makefile.am new file mode 100644 index 0000000..0f3abec --- /dev/null +++ b/src/plugins/keyhelp/default/Makefile.am @@ -0,0 +1,8 @@ +themedir = $(datadir)/gnome15/plugins/macros/default +theme_DATA = g19-menu-screen.svg \ + g19-menu-entry.svg \ + default-menu-screen.svg \ + default-menu-entry.svg + +EXTRA_DIST = \ + $(theme_DATA) diff --git a/src/plugins/keyhelp/default/default-menu-entry.svg b/src/plugins/keyhelp/default/default-menu-entry.svg new file mode 100644 index 0000000..b160dae --- /dev/null +++ b/src/plugins/keyhelp/default/default-menu-entry.svg @@ -0,0 +1,229 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + ${item_name} + ${item_key} + + + + ${item_name} + ${item_key} + + diff --git a/src/plugins/keyhelp/default/default-menu-screen.svg b/src/plugins/keyhelp/default/default-menu-screen.svg new file mode 100644 index 0000000..c1f3add --- /dev/null +++ b/src/plugins/keyhelp/default/default-menu-screen.svg @@ -0,0 +1,152 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + ${title} + + + + + + + + ${mkey} + No Macros Configured on ${mkey} + + diff --git a/src/plugins/keyhelp/default/g19.svg b/src/plugins/keyhelp/default/g19.svg new file mode 100644 index 0000000..b9e5456 --- /dev/null +++ b/src/plugins/keyhelp/default/g19.svg @@ -0,0 +1,487 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + ${view} + ${clear} + + + Ok + + + + + + + + + + + + + + + + + + + + + + + Menu + ${select} + ${up} + ${down} + ${right} + ${left} + + diff --git a/src/plugins/keyhelp/keyhelp.py b/src/plugins/keyhelp/keyhelp.py new file mode 100644 index 0000000..00729ef --- /dev/null +++ b/src/plugins/keyhelp/keyhelp.py @@ -0,0 +1,125 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2012 Brett Smith +# +# 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 . + +import gnome15.g15locale as g15locale +_ = g15locale.get_translation("keyhelp", modfile = __file__).ugettext + +import gnome15.g15driver as g15driver +import gnome15.g15theme as g15theme +import gnome15.g15plugin as g15plugin +import gnome15.g15actions as g15actions +import gnome15.g15devices as g15devices +import gnome15.g15profile as g15profile +import logging +import os +logger = logging.getLogger(__name__) + +# Actions +SHOW_KEY_HELP = 'key-help' + +# Register the action with all supported models +g15devices.g15_action_keys[SHOW_KEY_HELP] = g15actions.ActionBinding(SHOW_KEY_HELP, [ g15driver.G_KEY_M1 ], g15driver.KEY_STATE_HELD) +g15devices.z10_action_keys[SHOW_KEY_HELP] = g15actions.ActionBinding(SHOW_KEY_HELP, [ g15driver.G_KEY_L1 ], g15driver.KEY_STATE_HELD) +g15devices.g19_action_keys[SHOW_KEY_HELP] = g15actions.ActionBinding(SHOW_KEY_HELP, [ g15driver.G_KEY_M1 ], g15driver.KEY_STATE_HELD) + +# Plugin details - All of these must be provided +id="keyhelp" +name=_("Key Help Screen") +description=_("Displays key bindings on the current screen showing what\n\ +actions are available for a particular plugin.") +author="Brett Smith " +copyright=_("Copyright (C)2012 Brett Smith") +site="http://www.russo79.com/gnome15" +has_preferences=False +unsupported_models = [ g15driver.MODEL_G110, g15driver.MODEL_G11, g15driver.MODEL_MX5500, g15driver.MODEL_G930, g15driver.MODEL_G35 ] +actions={ + SHOW_KEY_HELP : _("Show Key Help"), + } + +def create(gconf_key, gconf_client, screen): + return G15KeyHelp(gconf_client, gconf_key, screen) + + +class G15KeyHelp(g15plugin.G15Plugin): + + def __init__(self, gconf_client, gconf_key, screen): + g15plugin.G15Plugin.__init__(self, gconf_client, gconf_key, screen) + + def activate(self): + self._keyhelp = None + g15plugin.G15Plugin.activate(self) + self.screen.key_handler.action_listeners.append(self) + + def deactivate(self): + self._hide_keyhelp() + g15plugin.G15Plugin.deactivate(self) + self.screen.key_handler.action_listeners.remove(self) + + def action_performed(self, binding): + if binding.action == SHOW_KEY_HELP: + if self._keyhelp is None: + self._show_keyhelp() + else: + self._hide_keyhelp() + return True + + def _hide_keyhelp(self): + if self._keyhelp is not None: + self._keyhelp.remove_from_parent() + self._keyhelp = None + + def _get_theme_properties(self): + if self.screen.driver.get_model_name() == g15driver.MODEL_G19: + return { "up": self._get_key_help(g15driver.G_KEY_UP, g15driver.KEY_STATE_UP), + "down": self._get_key_help(g15driver.G_KEY_DOWN, g15driver.KEY_STATE_UP), + "left": self._get_key_help(g15driver.G_KEY_LEFT, g15driver.KEY_STATE_UP), + "right": self._get_key_help(g15driver.G_KEY_RIGHT, g15driver.KEY_STATE_UP), + "select": self._get_key_help(g15driver.G_KEY_OK, g15driver.KEY_STATE_UP), + "view": self._get_key_help(g15driver.G_KEY_SETTINGS, g15driver.KEY_STATE_UP), + "clear": self._get_key_help(g15driver.G_KEY_BACK, g15driver.KEY_STATE_UP) + } + + # TODO + return {} + + def _get_key_help(self, key, state): + page = self.screen.get_visible_page() + originating_plugin = page.originating_plugin + if originating_plugin: + import gnome15.g15pluginmanager as g15pluginmanager + actions = g15pluginmanager.get_actions(g15pluginmanager.get_module_for_id(originating_plugin.__module__), self.screen.device) + active_profile = g15profile.get_active_profile(self.screen.driver.device) if self.screen.driver is not None else None + for action_id in actions: + # First try the active profile to see if the action has been re-mapped + action_binding = active_profile.get_binding_for_action(state, action_id) + if action_binding is None: + # No other keys bound to action, try the device defaults + device_info = g15devices.get_device_info(self.screen.driver.get_model_name()) + if action_id in device_info.action_keys: + action_binding = device_info.action_keys[action_id] + + if action_binding is not None and key in action_binding.keys: + return actions[action_id] + + return "?" + + def _show_keyhelp(self): + self._keyhelp = g15theme.Component("glasspane") + self._keyhelp.get_theme_properties = self._get_theme_properties + page = self.screen.get_visible_page() + page.add_child(self._keyhelp) + self._keyhelp.set_theme(g15theme.G15Theme(os.path.join(os.path.dirname(__file__), "default"))) + page.redraw() \ No newline at end of file diff --git a/src/plugins/lcdbiff/Makefile.am b/src/plugins/lcdbiff/Makefile.am new file mode 100644 index 0000000..81c653f --- /dev/null +++ b/src/plugins/lcdbiff/Makefile.am @@ -0,0 +1,13 @@ +SUBDIRS = default + +plugindir = $(datadir)/gnome15/plugins/lcdbiff +plugin_DATA = imap.ui \ + lcdbiff.py \ + mono-mail-new.gif \ + mono-mail-error.gif \ + mono-mail-refresh.gif \ + password.ui \ + pop3.ui + +EXTRA_DIST = \ + $(plugin_DATA) \ No newline at end of file diff --git a/src/plugins/lcdbiff/default/Makefile.am b/src/plugins/lcdbiff/default/Makefile.am new file mode 100644 index 0000000..1051af0 --- /dev/null +++ b/src/plugins/lcdbiff/default/Makefile.am @@ -0,0 +1,26 @@ +themedir = $(datadir)/gnome15/plugins/lcdbiff/default +theme_DATA = \ + default-menu-entry.svg + +EXTRA_DIST = \ + $(theme_DATA) + + +all-local: + for M_LOCALE in @ENABLED_LOCALES@; do \ + mkdir -p i18n/$$M_LOCALE/LC_MESSAGES ; \ + if [ `ls i18n/*.po 2>/dev/null|wc -l` -gt 0 ]; then \ + for M_PO in i18n/*.po; do \ + BN=`basename $$M_PO .po`; \ + LL=`basename $$BN .$$M_LOCALE`.mo; \ + echo "$$M_PO -> $$LL"; \ + msgfmt $$M_PO --output-file i18n/$$M_LOCALE/LC_MESSAGES/$$LL; \ + done; \ + fi; \ + done + +install-exec-hook: + for M_LOCALE in @ENABLED_LOCALES@; do \ + mkdir -p $(DESTDIR)$(datadir)/gnome15/plugins/im/default/i18n; \ + cp -pR i18n/$$M_LOCALE $(DESTDIR)$(datadir)/gnome15/plugins/im/default/i18n; \ + done \ No newline at end of file diff --git a/src/plugins/lcdbiff/default/default-menu-entry.svg b/src/plugins/lcdbiff/default/default-menu-entry.svg new file mode 100644 index 0000000..7d244b7 --- /dev/null +++ b/src/plugins/lcdbiff/default/default-menu-entry.svg @@ -0,0 +1,226 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + ${item_name} + ${item_alt} + + + + + ${item_name} + ${item_alt} + + + diff --git a/src/plugins/lcdbiff/i18n/imap.en_GB.po b/src/plugins/lcdbiff/i18n/imap.en_GB.po new file mode 100644 index 0000000..ee7bead --- /dev/null +++ b/src/plugins/lcdbiff/i18n/imap.en_GB.po @@ -0,0 +1,48 @@ +# English translations for gnome15-plugins package. +# Copyright (C) 2012 THE gnome15-plugins'S COPYRIGHT HOLDER +# This file is distributed under the same license as the gnome15-plugins package. +# Automatically generated, 2012. +# +msgid "" +msgstr "" +"Project-Id-Version: gnome15-plugins 0.8.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: 2012-01-08 11:23+0000\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: en_GB\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=ASCII\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: i18n/imap.glade.h:1 +msgid "Advanced" +msgstr "Advanced" + +#: i18n/imap.glade.h:2 +msgid "Folder" +msgstr "Folder" + +#: i18n/imap.glade.h:3 +msgid "Server:" +msgstr "Server:" + +#: i18n/imap.glade.h:4 +msgid "Use SSL" +msgstr "Use SSL" + +#: i18n/imap.glade.h:5 +msgid "Username" +msgstr "Username" + +#: i18n/imap.glade.h:6 +msgid "" +"You may enter a hostname or IP address to use the\n" +"default port, or suffix the hostname with a colon and\n" +"the port number, e.g. mail.mycompany.com:143" +msgstr "" +"You may enter a hostname or IP address to use the\n" +"default port, or suffix the hostname with a colon and\n" +"the port number, e.g. mail.mycompany.com:143" diff --git a/src/plugins/lcdbiff/i18n/imap.glade.h b/src/plugins/lcdbiff/i18n/imap.glade.h new file mode 100644 index 0000000..8b8c7a9 --- /dev/null +++ b/src/plugins/lcdbiff/i18n/imap.glade.h @@ -0,0 +1,8 @@ +char *s = N_("Advanced"); +char *s = N_("Folder"); +char *s = N_("Server:"); +char *s = N_("Use SSL"); +char *s = N_("Username"); +char *s = N_("You may enter a hostname or IP address to use the\n" + "default port, or suffix the hostname with a colon and\n" + "the port number, e.g. mail.mycompany.com:143"); diff --git a/src/plugins/lcdbiff/i18n/imap.pot b/src/plugins/lcdbiff/i18n/imap.pot new file mode 100644 index 0000000..08636cf --- /dev/null +++ b/src/plugins/lcdbiff/i18n/imap.pot @@ -0,0 +1,45 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: i18n/imap.glade.h:1 +msgid "Advanced" +msgstr "" + +#: i18n/imap.glade.h:2 +msgid "Folder" +msgstr "" + +#: i18n/imap.glade.h:3 +msgid "Server:" +msgstr "" + +#: i18n/imap.glade.h:4 +msgid "Use SSL" +msgstr "" + +#: i18n/imap.glade.h:5 +msgid "Username" +msgstr "" + +#: i18n/imap.glade.h:6 +msgid "" +"You may enter a hostname or IP address to use the\n" +"default port, or suffix the hostname with a colon and\n" +"the port number, e.g. mail.mycompany.com:143" +msgstr "" diff --git a/src/plugins/lcdbiff/i18n/lcdbiff.en_GB.po b/src/plugins/lcdbiff/i18n/lcdbiff.en_GB.po new file mode 100644 index 0000000..f997f1b --- /dev/null +++ b/src/plugins/lcdbiff/i18n/lcdbiff.en_GB.po @@ -0,0 +1,62 @@ +# English translations for gnome15-plugins package. +# Copyright (C) 2012 THE gnome15-plugins'S COPYRIGHT HOLDER +# This file is distributed under the same license as the gnome15-plugins package. +# Automatically generated, 2012. +# +msgid "" +msgstr "" +"Project-Id-Version: gnome15-plugins 0.8.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: 2012-01-08 11:23+0000\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: en_GB\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=ASCII\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: i18n/lcdbiff.glade.h:1 +msgid "Account Options" +msgstr "Account Options" + +#: i18n/lcdbiff.glade.h:2 +msgid "Accounts" +msgstr "Accounts" + +#: i18n/lcdbiff.glade.h:3 +msgid "Options" +msgstr "Options" + +#: i18n/lcdbiff.glade.h:4 +msgid "Check every" +msgstr "Check every" + +#: i18n/lcdbiff.glade.h:5 +msgid "IMAP" +msgstr "IMAP" + +#: i18n/lcdbiff.glade.h:6 +msgid "Mail Notification Preferences" +msgstr "Mail Notification Preferences" + +#: i18n/lcdbiff.glade.h:7 +msgid "POP3" +msgstr "POP3" + +#: i18n/lcdbiff.glade.h:8 +msgid "Type:" +msgstr "Type:" + +#: i18n/lcdbiff.glade.h:9 +msgid "minutes" +msgstr "minutes" + +#: i18n/lcdbiff.glade.h:10 +msgid "toolbutton1" +msgstr "toolbutton1" + +#: i18n/lcdbiff.glade.h:11 +msgid "toolbutton2" +msgstr "toolbutton2" diff --git a/src/plugins/lcdbiff/i18n/lcdbiff.glade.h b/src/plugins/lcdbiff/i18n/lcdbiff.glade.h new file mode 100644 index 0000000..cf5ceba --- /dev/null +++ b/src/plugins/lcdbiff/i18n/lcdbiff.glade.h @@ -0,0 +1,11 @@ +char *s = N_("Account Options"); +char *s = N_("Accounts"); +char *s = N_("Options"); +char *s = N_("Check every"); +char *s = N_("IMAP"); +char *s = N_("Mail Notification Preferences"); +char *s = N_("POP3"); +char *s = N_("Type:"); +char *s = N_("minutes"); +char *s = N_("toolbutton1"); +char *s = N_("toolbutton2"); diff --git a/src/plugins/lcdbiff/i18n/lcdbiff.pot b/src/plugins/lcdbiff/i18n/lcdbiff.pot new file mode 100644 index 0000000..11a8dab --- /dev/null +++ b/src/plugins/lcdbiff/i18n/lcdbiff.pot @@ -0,0 +1,62 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: i18n/lcdbiff.glade.h:1 +msgid "Account Options" +msgstr "" + +#: i18n/lcdbiff.glade.h:2 +msgid "Accounts" +msgstr "" + +#: i18n/lcdbiff.glade.h:3 +msgid "Options" +msgstr "" + +#: i18n/lcdbiff.glade.h:4 +msgid "Check every" +msgstr "" + +#: i18n/lcdbiff.glade.h:5 +msgid "IMAP" +msgstr "" + +#: i18n/lcdbiff.glade.h:6 +msgid "Mail Notification Preferences" +msgstr "" + +#: i18n/lcdbiff.glade.h:7 +msgid "POP3" +msgstr "" + +#: i18n/lcdbiff.glade.h:8 +msgid "Type:" +msgstr "" + +#: i18n/lcdbiff.glade.h:9 +msgid "minutes" +msgstr "" + +#: i18n/lcdbiff.glade.h:10 +msgid "toolbutton1" +msgstr "" + +#: i18n/lcdbiff.glade.h:11 +msgid "toolbutton2" +msgstr "" diff --git a/src/plugins/lcdbiff/i18n/password.en_GB.po b/src/plugins/lcdbiff/i18n/password.en_GB.po new file mode 100644 index 0000000..f6e7adc --- /dev/null +++ b/src/plugins/lcdbiff/i18n/password.en_GB.po @@ -0,0 +1,38 @@ +# English translations for gnome15-plugins package. +# Copyright (C) 2012 THE gnome15-plugins'S COPYRIGHT HOLDER +# This file is distributed under the same license as the gnome15-plugins package. +# Automatically generated, 2012. +# +msgid "" +msgstr "" +"Project-Id-Version: gnome15-plugins 0.8.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: 2012-01-08 11:23+0000\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: en_GB\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=ASCII\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: i18n/password.glade.h:1 +msgid "Cancel" +msgstr "Cancel" + +#: i18n/password.glade.h:2 +msgid "Email Notification Password" +msgstr "Email Notification Password" + +#: i18n/password.glade.h:3 +msgid "Ok" +msgstr "Ok" + +#: i18n/password.glade.h:4 +msgid "Password" +msgstr "Password" + +#: i18n/password.glade.h:5 +msgid "Password Text" +msgstr "Password Text" diff --git a/src/plugins/lcdbiff/i18n/password.glade.h b/src/plugins/lcdbiff/i18n/password.glade.h new file mode 100644 index 0000000..ccd7ad3 --- /dev/null +++ b/src/plugins/lcdbiff/i18n/password.glade.h @@ -0,0 +1,5 @@ +char *s = N_("Cancel"); +char *s = N_("Email Notification Password"); +char *s = N_("Ok"); +char *s = N_("Password"); +char *s = N_("Password Text"); diff --git a/src/plugins/lcdbiff/i18n/password.pot b/src/plugins/lcdbiff/i18n/password.pot new file mode 100644 index 0000000..0a372f5 --- /dev/null +++ b/src/plugins/lcdbiff/i18n/password.pot @@ -0,0 +1,38 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: i18n/password.glade.h:1 +msgid "Cancel" +msgstr "" + +#: i18n/password.glade.h:2 +msgid "Email Notification Password" +msgstr "" + +#: i18n/password.glade.h:3 +msgid "Ok" +msgstr "" + +#: i18n/password.glade.h:4 +msgid "Password" +msgstr "" + +#: i18n/password.glade.h:5 +msgid "Password Text" +msgstr "" diff --git a/src/plugins/lcdbiff/i18n/pop3.en_GB.po b/src/plugins/lcdbiff/i18n/pop3.en_GB.po new file mode 100644 index 0000000..454f8b3 --- /dev/null +++ b/src/plugins/lcdbiff/i18n/pop3.en_GB.po @@ -0,0 +1,44 @@ +# English translations for gnome15-plugins package. +# Copyright (C) 2012 THE gnome15-plugins'S COPYRIGHT HOLDER +# This file is distributed under the same license as the gnome15-plugins package. +# Automatically generated, 2012. +# +msgid "" +msgstr "" +"Project-Id-Version: gnome15-plugins 0.8.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: 2012-01-08 11:23+0000\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: en_GB\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=ASCII\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: i18n/pop3.glade.h:1 +msgid "Advanced" +msgstr "Advanced" + +#: i18n/pop3.glade.h:2 +msgid "Server:" +msgstr "Server:" + +#: i18n/pop3.glade.h:3 +msgid "Use SSL" +msgstr "Use SSL" + +#: i18n/pop3.glade.h:4 +msgid "Username" +msgstr "Username" + +#: i18n/pop3.glade.h:5 +msgid "" +"You may enter a hostname or IP address to use the\n" +"default port, or suffix the hostname with a colon and\n" +"the port number, e.g. mail.mycompany.com:110" +msgstr "" +"You may enter a hostname or IP address to use the\n" +"default port, or suffix the hostname with a colon and\n" +"the port number, e.g. mail.mycompany.com:110" diff --git a/src/plugins/lcdbiff/i18n/pop3.glade.h b/src/plugins/lcdbiff/i18n/pop3.glade.h new file mode 100644 index 0000000..6627d15 --- /dev/null +++ b/src/plugins/lcdbiff/i18n/pop3.glade.h @@ -0,0 +1,7 @@ +char *s = N_("Advanced"); +char *s = N_("Server:"); +char *s = N_("Use SSL"); +char *s = N_("Username"); +char *s = N_("You may enter a hostname or IP address to use the\n" + "default port, or suffix the hostname with a colon and\n" + "the port number, e.g. mail.mycompany.com:110"); diff --git a/src/plugins/lcdbiff/i18n/pop3.pot b/src/plugins/lcdbiff/i18n/pop3.pot new file mode 100644 index 0000000..aa3fd39 --- /dev/null +++ b/src/plugins/lcdbiff/i18n/pop3.pot @@ -0,0 +1,41 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: i18n/pop3.glade.h:1 +msgid "Advanced" +msgstr "" + +#: i18n/pop3.glade.h:2 +msgid "Server:" +msgstr "" + +#: i18n/pop3.glade.h:3 +msgid "Use SSL" +msgstr "" + +#: i18n/pop3.glade.h:4 +msgid "Username" +msgstr "" + +#: i18n/pop3.glade.h:5 +msgid "" +"You may enter a hostname or IP address to use the\n" +"default port, or suffix the hostname with a colon and\n" +"the port number, e.g. mail.mycompany.com:110" +msgstr "" diff --git a/src/plugins/lcdbiff/imap.ui b/src/plugins/lcdbiff/imap.ui new file mode 100644 index 0000000..2b89798 --- /dev/null +++ b/src/plugins/lcdbiff/imap.ui @@ -0,0 +1,175 @@ + + + + + + False + + + True + False + + + True + False + 2 + 2 + 8 + 8 + + + True + False + 0 + Server: + + + GTK_FILL + + + + + + True + False + 0 + Username + + + 1 + 2 + GTK_FILL + + + + + + True + True + You may enter a hostname or IP address to use the +default port, or suffix the hostname with a colon and +the port number, e.g. mail.mycompany.com:143 + + False + False + True + True + + + 1 + 2 + GTK_FILL + + + + + + True + True + + False + False + True + True + + + 1 + 2 + 1 + 2 + + + + + + False + False + 8 + 0 + + + + + True + True + 4 + + + True + False + 4 + + + True + False + + + True + False + Folder + + + False + False + 0 + + + + + True + True + + False + False + True + True + + + True + True + 4 + 1 + + + + + True + True + 0 + + + + + Use SSL + True + True + False + True + + + True + True + 1 + + + + + + + True + False + Advanced + + + + + False + False + 4 + 1 + + + + + + diff --git a/src/plugins/lcdbiff/lcdbiff.py b/src/plugins/lcdbiff/lcdbiff.py new file mode 100644 index 0000000..ff33a7d --- /dev/null +++ b/src/plugins/lcdbiff/lcdbiff.py @@ -0,0 +1,517 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2010 Brett Smith +# +# 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 . + +import gnome15.g15locale as g15locale +_ = g15locale.get_translation("lcdbiff", modfile = __file__).ugettext + +import gnome15.util.g15convert as g15convert +import gnome15.util.g15scheduler as g15scheduler +import gnome15.util.g15cairo as g15cairo +import gnome15.util.g15icontools as g15icontools +import gnome15.g15theme as g15theme +import gnome15.g15driver as g15driver +import gnome15.g15plugin as g15plugin +import gnome15.g15accounts as g15accounts +import gnome15.g15globals as g15globals +import os, os.path +import pwd +import gtk +import re +from poplib import POP3_SSL +from poplib import POP3 +from imaplib import IMAP4 +from imaplib import IMAP4_SSL + +# Logging +import logging +logger = logging.getLogger(__name__) + +# Plugin details - All of these must be provided +id = "lcdbiff" +name = _("POP3 / IMAP Email Notification") +description = _("Periodically checks your email accounts for any waiting messages. Currently supports POP3 and IMAP \ +protocols. For models without a screen, the M-Key lights will be flashed when there is an email \ +waiting. For models with a screen, a page showing all unread mail counts will be displayed, and an \ +icon added to the panel indicating overall status.") +author = "Brett Smith " +copyright = _("Copyright (C)2010 Brett Smith") +site = "http://www.russo79.com/gnome15" +has_preferences = True +needs_network = True +unsupported_models = [ g15driver.MODEL_G930, g15driver.MODEL_G35 ] +actions={ + g15driver.PREVIOUS_SELECTION : _("Previous item"), + g15driver.NEXT_SELECTION : _("Next item"), + g15driver.NEXT_PAGE : _("Next page"), + g15driver.PREVIOUS_PAGE : _("Previous page"), + g15driver.SELECT : _("Compose new mail"), + g15driver.VIEW : _("Check mail status") + } + +# Constants +CURRENT_USERNAME=pwd.getpwuid(os.getuid())[0] +PROTO_POP3 = "pop3" +PROTO_IMAP = "imap" +TYPES = [ PROTO_POP3, PROTO_IMAP ] +CONFIG_PATH = os.path.join(g15globals.user_config_dir, "plugin-data" , "lcdbiff", "mailboxes.xml") +CONFIG_ITEM_NAME = "mailbox" + +def create(gconf_key, gconf_client, screen): + return G15Biff(gconf_client, gconf_key, screen) + +def show_preferences(parent, driver, gconf_client, gconf_key): + G15BiffPreferences(parent, gconf_client, gconf_key) + +def changed(widget, key, gconf_client): + gconf_client.set_bool(key, widget.get_active()) + +def get_update_time(gconf_client, gconf_key): + val = gconf_client.get_int(gconf_key + "/update_time") + if val == 0: + val = 10 + return val + +''' +Abstract mail checker. Subclasses are responsible for connecting +to mail stores and retrieving the number of unread messages. +''' +class Checker(): + + def __init__(self, account_manager): + self.account_manager = account_manager + + def get_username(self, account): + username = account.get_property("username", "") + return username if username != "" else CURRENT_USERNAME + + def get_hostname(self, account): + hostname = account.get_property("server", "") + pre, _, _ = hostname.partition(":") + return pre if pre != "" else "localhost" + + def get_port_or_default(self, account, default_port): + hostname = account.get_property("server", "") + _, sep, post = hostname.partition(":") + if sep == "": + return default_port + return int(post) + + def save_password(self, account, password, default_port): + hostname = self.get_hostname(account) + port = self.get_port_or_default(account, default_port) + self.account_manager.store_password(account, password, hostname, port) + + def get_password(self, account, default_port, force_dialog = False): + hostname = self.get_hostname(account) + port = self.get_port_or_default(account, default_port) + return self.account_manager.retrieve_password(account, hostname, port, force_dialog) + +''' +POP3 checker. Does the actual work of checking for emails using +the POP3 protocol. +''' +class POP3Checker(Checker): + + + def __init__(self, account_manager): + Checker.__init__(self, account_manager) + + def check(self, account): + ssl = account.get_property("ssl", "false") + default_port = 995 if ssl else 110 + port = self.get_port_or_default(account, default_port) + if ssl: + pop = POP3_SSL(self.get_hostname(account), port) + else: + pop = POP3(self.get_hostname(account), port, 7.0) + try : + username = self.get_username(account) + for i in range(0, 3): + password = self.get_password(account, default_port, i > 0) + if password == None or password == "": + raise Exception(_("Authentication cancelled")) + try : + pop.user(username) + pop.pass_(password) + self.save_password(account, password, default_port) + return pop.stat() + except Exception as e: + logger.debug("Error while checking", exc_info = e) + try : + pop.apop(username, password) + self.save_password(account, password, default_port) + return pop.stat() + except Exception as e2: + logger.debug("Error while checking", exc_info = e2) + finally : + pop.quit() + return (0, 0) + +''' +IMAP checker. Does the actual work of checking for emails using +the IMAP protocol. +''' +class IMAPChecker(Checker): + + def __init__(self, account_manager): + Checker.__init__(self, account_manager) + + def check(self, account): + ssl = account.get_property("ssl", "false") + folder = account.get_property("folder", "INBOX") + default_port = 993 if ssl else 143 + port = self.get_port_or_default(account, default_port) + count = ( 0, 0 ) + username = self.get_username(account) + for i in range(0, 3): + + for j in range(0, 2): + if ssl: + imap = IMAP4_SSL(self.get_hostname(account), port) + else: + imap = IMAP4(self.get_hostname(account), port) + + try : + password = self.get_password(account, default_port, i > 0) + if password == None or password == "": + raise Exception(_("Authentication cancelled")) + + try : + if j == 0: + imap.login(username, password) + else: + imap.login_cram_md5(username, password) + self.save_password(account, password, default_port) + status = imap.status(folder, "(UNSEEN)") + unread = int(re.search("UNSEEN (\d+)", status[1][0]).group(1)) + count = ( unread, count ) + return count + except Exception as e: + logger.debug("Error while checking", exc_info = e) + + finally: + imap.logout() + + return count + + +''' +Superclass of the UI mail protocol specific configuration. Currently +all types support server, username and SSL options, although +this may change in future +''' +class G15BiffOptions(g15accounts.G15AccountOptions): + def __init__(self, account, account_ui): + g15accounts.G15AccountOptions.__init__(self, account, account_ui) + + self.widget_tree = gtk.Builder() + self.widget_tree.add_from_file(os.path.join(os.path.dirname(__file__), "%s.ui" % account.type)) + self.component = self.widget_tree.get_object("OptionPanel") + + # Both currently have server, username and SSL widgets + server = self.widget_tree.get_object("Server") + username = self.widget_tree.get_object("Username") + ssl = self.widget_tree.get_object("SSL") + + # Events + server.connect("changed", self._server_changed) + username.connect("changed", self._username_changed) + ssl.connect("toggled", self._ssl_changed) + + # Set initial values + server.set_text(self.account.properties["server"] if "server" in self.account.properties else "") + username.set_text(self.account.properties["username"] if "username" in self.account.properties else "") + ssl.set_active(self.account.properties["ssl"] == "true" if "ssl" in self.account.properties else False) + + def _server_changed(self, widget): + self.account.properties["server"] = widget.get_text() + self.account_ui.save_accounts() + + def _ssl_changed(self, widget): + self.account.properties["ssl"] = "true" if widget.get_active() else "false" + self.account_ui.save_accounts() + + def _username_changed(self, widget): + self.account.properties["username"] = widget.get_text() + self.account_ui.save_accounts() + +''' +POP3 configuration UI +''' +class G15BiffPOP3Options(G15BiffOptions): + + + def __init__(self, account, account_ui): + G15BiffOptions.__init__(self, account, account_ui) + +''' +IMAP configuration UI. Adds the additional Folder widget +''' +class G15BiffIMAPOptions(G15BiffOptions): + + def __init__(self, account, account_ui): + G15BiffOptions.__init__(self, account, account_ui) + folder = self.widget_tree.get_object("Folder") + folder.connect("changed", self._folder_changed) + folder.set_text(self.account.properties["folder"] if "folder" in self.account.properties else "INBOX") + + def _folder_changed(self, widget): + self.account.properties["folder"] = widget.get_text() + self.account_ui.save_accounts() + +''' +Configuration UI +''' +class G15BiffPreferences(g15accounts.G15AccountPreferences): + + + def __init__(self, parent, gconf_client, gconf_key): + g15accounts.G15AccountPreferences.__init__(self, parent, gconf_client, \ + gconf_key, \ + CONFIG_PATH, \ + CONFIG_ITEM_NAME, \ + 10) + + def get_account_types(self): + return [ PROTO_POP3, PROTO_IMAP ] + + def get_account_type_name(self, account_type): + return _(account_type) + + def create_options_for_type(self, account, account_type): + if account_type == PROTO_POP3: + return G15BiffPOP3Options(account, self) + else: + return G15BiffIMAPOptions(account, self) + + +''' +Account menu item +''' + +class MailItem(g15theme.MenuItem): + def __init__(self, component_id, gconf_client, account, plugin): + g15theme.MenuItem.__init__(self, component_id) + self.account = account + self.count = 0 + self.gconf_client = gconf_client + self.status = "Unknown" + self.error = None + self.plugin = plugin + self.refreshing = False + + def get_theme_properties(self): + item_properties = g15theme.MenuItem.get_theme_properties(self) + item_properties["item_name"] = self.account.name + if self.error != None: + item_properties["item_alt"] = _("Error") + else: + if self.count > 0: + item_properties["item_alt"] = "%d" % ( self.count ) + else: + item_properties["item_alt"] = _("None") + item_properties["item_type"] = "" + + if self.refreshing: + if self.plugin.screen.driver.get_bpp() == 1: + item_properties["item_icon"] = os.path.join(os.path.dirname(__file__), "mono-mail-refresh.gif") + else: + item_properties["item_icon"] = g15icontools.get_icon_path(["view-refresh", "stock_refresh", "gtk-refresh", "view-refresh-symbolic"]) + elif self.error is not None: + if self.plugin.screen.driver.get_bpp() == 1: + item_properties["item_icon"] = os.path.join(os.path.dirname(__file__), "mono-mail-error.gif") + else: + item_properties["item_icon"] = g15icontools.get_icon_path("new-messages-red") + else: + if self.count > 0: + if self.plugin.screen.driver.get_bpp() == 1: + item_properties["item_icon"] = os.path.join(os.path.dirname(__file__), "mono-mail-new.gif") + else: + item_properties["item_icon"] = g15icontools.get_icon_path("indicator-messages-new") + else: + if self.plugin.screen.driver.get_bpp() == 1: + item_properties["item_icon"] = "" + else: + item_properties["item_icon"] = g15icontools.get_icon_path("indicator-messages") + + return item_properties + + def activate(self): + email_client = self.gconf_client.get_string("/desktop/gnome/url-handlers/mailto/command") + logger.info("Running email client %s", email_client) + if email_client != None: + call_str = "%s &" % email_client.replace("%s", "").replace("%U", "mailto:") + os.system(call_str) + +''' +Gnome15 LCDBiff plugin +''' + +class G15Biff(g15plugin.G15MenuPlugin): + + def __init__(self, gconf_client, gconf_key, screen): + g15plugin.G15MenuPlugin.__init__(self, gconf_client, gconf_key, screen, ["mail-inbox", "mail-folder-inbox" ], id, "Email") + self.refresh_timer = None + + def activate(self): + self.total_count = 0 + self.items = [] + self.attention = False + self.thumb_icon = None + self.index = 0 + self.light_control = None + self.account_manager = g15accounts.G15AccountManager(CONFIG_PATH, CONFIG_ITEM_NAME) + self.account_manager.add_change_listener(self) + self.checkers = { PROTO_POP3 : POP3Checker(self.account_manager), PROTO_IMAP: IMAPChecker(self.account_manager) } + if self.screen.driver.get_bpp() > 0: + g15plugin.G15MenuPlugin.activate(self) + self.update_time_changed_handle = self.gconf_client.notify_add(self.gconf_key + "/update_time", self._update_time_changed) + self.schedule_refresh(10.0) + self.screen.key_handler.action_listeners.append(self) + + def deactivate(self): + self.screen.key_handler.action_listeners.remove(self) + g15plugin.G15MenuPlugin.deactivate(self) + self._stop_blink() + if self.refresh_timer: + self.refresh_timer.cancel() + self.refresh_timer.task_queue.stop() + self.gconf_client.notify_remove(self.update_time_changed_handle) + + def action_performed(self, binding): + if binding.action == g15driver.VIEW: + if self.refresh_timer: + self.refresh_timer.cancel() + self.refresh() + + def load_menu_items(self): + items = [] + self.account_manager.load() + i = 0 + for account in self.account_manager.accounts: + items.append(MailItem("mailitem-%d" % i, self.gconf_client, account, self)) + i += 1 + if self.screen.driver.get_bpp() != 0: + self.menu.selected = items[0] if len(items) > 0 else None + self.menu.remove_all_children() + self.menu.set_children(items) + self.items = items + + def create_page(self): + page = g15plugin.G15MenuPlugin.create_page(self) + page.panel_painter = self._paint_panel + page.thumbnail_painter = self._paint_thumbnail + return page + + def schedule_refresh(self, time = - 1): + if time == -1: + time = get_update_time(self.gconf_client, self.gconf_key) * 60.0 + self.refresh_timer = g15scheduler.queue("lcdbiff-%s" % self.screen.device.uid, "MailRefreshTimer", time, self.refresh) + + def refresh(self): + t_count = 0 + t_errors = 0 + for item in self.items: + try : + item.refreshing = True + self.page.redraw() + status = self._check_account(item.account) + item.count = status[0] + t_count += item.count + item.error = None + item.refreshing = False + except Exception as e: + item.refreshing = False + t_errors += 1 + item.error = e + item.count = 0 + logger.debug("Error while refreshing item %s", str(item), exc_info = e) + + self.total_count = t_count + self.total_errors = t_errors + + if self.total_errors > 0: + self._stop_blink() + self.attention = True + if self.screen.driver.get_bpp() == 1: + self.thumb_icon = g15cairo.load_surface_from_file(os.path.join(os.path.dirname(__file__), "mono-mail-error.gif")) + elif self.screen.driver.get_bpp() > 0: + self.thumb_icon = g15cairo.load_surface_from_file(g15icontools.get_icon_path(["new-messages-red","messagebox_critical"])) + else: + if self.total_count > 0: + self._start_blink() + self.attention = True + if self.screen.driver.get_bpp() == 1: + self.thumb_icon = g15cairo.load_surface_from_file(os.path.join(os.path.dirname(__file__), "mono-mail-new.gif")) + elif self.screen.driver.get_bpp() > 0: + self.thumb_icon = g15cairo.load_surface_from_file(g15icontools.get_icon_path(["indicator-messages-new", "mail-message-new"])) + else: + self._stop_blink() + self.attention = False + if self.screen.driver.get_bpp() == 1: + self.thumb_icon = None + elif self.screen.driver.get_bpp() > 0: + self.thumb_icon = g15cairo.load_surface_from_file(g15icontools.get_icon_path(["indicator-messages", "mail-message"])) + + if self.screen.driver.get_bpp() > 0: + self.screen.redraw(self.page) + + self.schedule_refresh() + + ''' + Private + ''' + def _accounts_changed(self, account_manager): + self._reload_menu() + self.schedule_refresh() + + def _check_account(self, account): + return self.checkers[account.type].check(account) + + def _start_blink(self): + if not self.light_control: + self.light_control = self.screen.driver.acquire_control_with_hint(g15driver.HINT_MKEYS, val = g15driver.MKEY_LIGHT_1 | g15driver.MKEY_LIGHT_2 | g15driver.MKEY_LIGHT_3) + self.light_control.blink(off_val = self._get_mkey_value) + + def _get_mkey_value(self): + return g15driver.get_mask_for_memory_bank(self.screen.get_memory_bank()) + + def _stop_blink(self): + if self.light_control: + self.screen.driver.release_control(self.light_control) + self.light_control = None + + def _reload_menu(self): + self.load_menu_items() + if self.screen.driver.get_bpp() == 1: + self.screen.redraw(self.page) + + def _update_time_changed(self, client, connection_id, entry, args): + self.refresh_timer.cancel() + self.schedule_refresh() + + def _paint_thumbnail(self, canvas, allocated_size, horizontal): + if self.page != None: + if self.thumb_icon != None: + size = g15cairo.paint_thumbnail_image(allocated_size, self.thumb_icon, canvas) + return size + + def _paint_panel(self, canvas, allocated_size, horizontal): + if self.page != None: + if self.thumb_icon != None and self.attention: + size = g15cairo.paint_thumbnail_image(allocated_size, self.thumb_icon, canvas) + return size + diff --git a/src/plugins/lcdbiff/mono-mail-error.gif b/src/plugins/lcdbiff/mono-mail-error.gif new file mode 100644 index 0000000..a616bfa Binary files /dev/null and b/src/plugins/lcdbiff/mono-mail-error.gif differ diff --git a/src/plugins/lcdbiff/mono-mail-new.gif b/src/plugins/lcdbiff/mono-mail-new.gif new file mode 100644 index 0000000..eb3dae3 Binary files /dev/null and b/src/plugins/lcdbiff/mono-mail-new.gif differ diff --git a/src/plugins/lcdbiff/mono-mail-refresh.gif b/src/plugins/lcdbiff/mono-mail-refresh.gif new file mode 100644 index 0000000..af7a2c1 Binary files /dev/null and b/src/plugins/lcdbiff/mono-mail-refresh.gif differ diff --git a/src/plugins/lcdbiff/password.ui b/src/plugins/lcdbiff/password.ui new file mode 100644 index 0000000..5e84ae8 --- /dev/null +++ b/src/plugins/lcdbiff/password.ui @@ -0,0 +1,159 @@ + + + + + + False + 5 + Email Notification Password + True + center + normal + True + + + True + False + 2 + + + True + False + end + + + Ok + True + True + True + True + True + True + True + + + False + False + 0 + + + + + Cancel + True + True + True + + + False + False + 1 + + + + + False + True + end + 0 + + + + + True + False + 8 + + + True + False + gtk-dialog-question + 6 + + + True + True + 0 + + + + + True + False + 8 + + + True + False + Password Text + True + + + False + False + 0 + + + + + True + False + + + True + False + Password + + + True + False + 0 + + + + + True + True + False + + True + False + False + True + True + + + True + False + 1 + + + + + True + True + 1 + + + + + False + False + 1 + + + + + True + True + 1 + + + + + + togglebutton1 + button1 + + + diff --git a/src/plugins/lcdbiff/pop3.ui b/src/plugins/lcdbiff/pop3.ui new file mode 100644 index 0000000..ddf128b --- /dev/null +++ b/src/plugins/lcdbiff/pop3.ui @@ -0,0 +1,122 @@ + + + + + + False + + + True + False + + + True + False + 2 + 2 + 8 + 8 + + + True + False + 0 + Server: + + + GTK_FILL + + + + + + True + False + 0 + Username + + + 1 + 2 + GTK_FILL + + + + + + True + True + You may enter a hostname or IP address to use the +default port, or suffix the hostname with a colon and +the port number, e.g. mail.mycompany.com:110 + + False + False + True + True + + + 1 + 2 + GTK_FILL + + + + + + True + True + + False + False + True + True + + + 1 + 2 + 1 + 2 + + + + + + False + False + 8 + 0 + + + + + True + True + + + Use SSL + True + True + False + True + + + + + True + False + Advanced + + + + + False + False + 4 + 1 + + + + + + diff --git a/src/plugins/lcdshot/Makefile.am b/src/plugins/lcdshot/Makefile.am new file mode 100644 index 0000000..b648a12 --- /dev/null +++ b/src/plugins/lcdshot/Makefile.am @@ -0,0 +1,6 @@ +plugindir = $(datadir)/gnome15/plugins/lcdshot +plugin_DATA = lcdshot.py \ + lcdshot.ui + +EXTRA_DIST = \ + $(plugin_DATA) \ No newline at end of file diff --git a/src/plugins/lcdshot/i18n/lcdshot.en_GB.po b/src/plugins/lcdshot/i18n/lcdshot.en_GB.po new file mode 100644 index 0000000..ac2c6ab --- /dev/null +++ b/src/plugins/lcdshot/i18n/lcdshot.en_GB.po @@ -0,0 +1,50 @@ +# English translations for gnome15-plugins package. +# Copyright (C) 2012 THE gnome15-plugins'S COPYRIGHT HOLDER +# This file is distributed under the same license as the gnome15-plugins package. +# Automatically generated, 2012. +# +msgid "" +msgstr "" +"Project-Id-Version: gnome15-plugins 0.8.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: 2012-01-08 11:23+0000\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: en_GB\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=ASCII\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: i18n/lcdshot.glade.h:1 +msgid "Choose A Destination Folder For The LCD Shots" +msgstr "Choose A Destination Folder For The LCD Shots" + +#: i18n/lcdshot.glade.h:2 +msgid "Folder" +msgstr "Folder" + +#: i18n/lcdshot.glade.h:3 +msgid "LCD Screenshot Preferences" +msgstr "LCD Screenshot Preferences" + +#: i18n/lcdshot.glade.h:4 +msgid "center" +msgstr "center" + +#: i18n/lcdshot.glade.h:5 +msgid "scale" +msgstr "scale" + +#: i18n/lcdshot.glade.h:6 +msgid "stretch" +msgstr "stretch" + +#: i18n/lcdshot.glade.h:7 +msgid "tile" +msgstr "tile" + +#: i18n/lcdshot.glade.h:8 +msgid "zoom" +msgstr "zoom" diff --git a/src/plugins/lcdshot/i18n/lcdshot.glade.h b/src/plugins/lcdshot/i18n/lcdshot.glade.h new file mode 100644 index 0000000..3c5b3f2 --- /dev/null +++ b/src/plugins/lcdshot/i18n/lcdshot.glade.h @@ -0,0 +1,8 @@ +char *s = N_("Choose A Destination Folder For The LCD Shots"); +char *s = N_("Folder"); +char *s = N_("LCD Screenshot Preferences"); +char *s = N_("center"); +char *s = N_("scale"); +char *s = N_("stretch"); +char *s = N_("tile"); +char *s = N_("zoom"); diff --git a/src/plugins/lcdshot/i18n/lcdshot.pot b/src/plugins/lcdshot/i18n/lcdshot.pot new file mode 100644 index 0000000..18d30a7 --- /dev/null +++ b/src/plugins/lcdshot/i18n/lcdshot.pot @@ -0,0 +1,50 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: i18n/lcdshot.glade.h:1 +msgid "Choose A Destination Folder For The LCD Shots" +msgstr "" + +#: i18n/lcdshot.glade.h:2 +msgid "Folder" +msgstr "" + +#: i18n/lcdshot.glade.h:3 +msgid "LCD Screenshot Preferences" +msgstr "" + +#: i18n/lcdshot.glade.h:4 +msgid "center" +msgstr "" + +#: i18n/lcdshot.glade.h:5 +msgid "scale" +msgstr "" + +#: i18n/lcdshot.glade.h:6 +msgid "stretch" +msgstr "" + +#: i18n/lcdshot.glade.h:7 +msgid "tile" +msgstr "" + +#: i18n/lcdshot.glade.h:8 +msgid "zoom" +msgstr "" diff --git a/src/plugins/lcdshot/lcdshot.py b/src/plugins/lcdshot/lcdshot.py new file mode 100644 index 0000000..e6de1a0 --- /dev/null +++ b/src/plugins/lcdshot/lcdshot.py @@ -0,0 +1,240 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2011 Brett Smith +# Copyright (C) 2013 Brett Smith +# Nuno Araujo +# +# 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 . + +import gnome15.g15locale as g15locale +_ = g15locale.get_translation("lcdshot", modfile = __file__).ugettext + +import gnome15.g15driver as g15driver +import gnome15.g15devices as g15devices +import gnome15.g15globals as g15globals +import gnome15.g15actions as g15actions +import os.path +import gtk +import gobject +import gnome15.util.g15convert as g15convert +import gnome15.g15notify as g15notify +import gnome15.util.g15uigconf as g15uigconf +import gnome15.util.g15gconf as g15gconf +import gnome15.util.g15os as g15os +import gnome15.util.g15cairo as g15cairo +import subprocess +import shutil +from threading import Thread + +# Logging +import logging +logger = logging.getLogger(__name__) + +# Custom actions +SCREENSHOT = "screenshot" + +# Register the action with all supported models +g15devices.g15_action_keys[SCREENSHOT] = g15actions.ActionBinding(SCREENSHOT, [ g15driver.G_KEY_MR ], g15driver.KEY_STATE_HELD) +g15devices.g19_action_keys[SCREENSHOT] = g15actions.ActionBinding(SCREENSHOT, [ g15driver.G_KEY_MR ], g15driver.KEY_STATE_HELD) + +# Plugin details - All of these must be provided +id="lcdshot" +name=_("LCD Screenshot") +description=_("Takes either a still screenshot or a video of the LCD\n\ +and places it in the configured directory.") +author="Brett Smith " +copyright=_("Copyright (C)2010 Brett Smith") +site="http://www.russo79.com/gnome15" +has_preferences=True +unsupported_models = [ g15driver.MODEL_G110, g15driver.MODEL_G11, g15driver.MODEL_MX5500, g15driver.MODEL_G930, g15driver.MODEL_G35, g15driver.MODEL_Z10 ] +actions={ + SCREENSHOT : "Take LCD screenshot" + } + + +''' +This simple plugin takes a screenshot of the LCD +''' + +def create(gconf_key, gconf_client, screen): + return G15LCDShot(screen, gconf_client, gconf_key) + +def show_preferences(parent, driver, gconf_client, gconf_key): + LCDShotPreferences(parent, driver, gconf_client, gconf_key) + +class LCDShotPreferences(): + def __init__(self, parent, driver, gconf_client, gconf_key): + self.gconf_client = gconf_client + self.gconf_key = gconf_key + widget_tree = gtk.Builder() + widget_tree.add_from_file(os.path.join(os.path.dirname(__file__), "lcdshot.ui")) + dialog = widget_tree.get_object("LCDShotDialog") + dialog.set_transient_for(parent) + chooser = gtk.FileChooserDialog("Open..", + None, + gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER, + (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, + gtk.STOCK_OPEN, gtk.RESPONSE_OK)) + chooser.set_default_response(gtk.RESPONSE_OK) + chooser_button = widget_tree.get_object("FileChooserButton") + chooser_button.dialog = chooser + chooser_button.connect("file-set", self._file_set) + chooser_button.connect("file-activated", self._file_activated) + chooser_button.connect("current-folder-changed", self._file_activated) + bg_img = g15gconf.get_string_or_default(self.gconf_client, "%s/folder" % self.gconf_key, os.path.expanduser("~/Desktop")) + chooser_button.set_current_folder(bg_img) + + # Reset the value of the mode setting to 'still' if mencoder is not installed + mencoder_is_installed = g15os.is_program_in_path('mencoder') + if not mencoder_is_installed: + gconf_client.set_string("%s/mode" % self.gconf_key, "still") + + # Initialize the mode combobox content + modes = widget_tree.get_object("ModeModel") + modes.clear() + modes.append(('still','Still', True)) + modes.append(('video','Video', mencoder_is_installed)) + + # Display a warning message to the user if mencoder is not installed + warning = widget_tree.get_object("NoVideoMessage") + warning.set_visible(not mencoder_is_installed) + + g15uigconf.configure_combo_from_gconf(self.gconf_client, "%s/mode" % self.gconf_key, "Mode", "still", widget_tree) + mode = widget_tree.get_object("Mode") + mode.connect("changed", self._mode_changed) + + g15uigconf.configure_spinner_from_gconf(self.gconf_client, "%s/fps" % gconf_key, "FPS", 10, widget_tree, False) + self._spinner = widget_tree.get_object("FPS") + self._mode_changed(mode) + + dialog.run() + dialog.hide() + + def _mode_changed(self, widget): + self._spinner.set_sensitive(widget.get_active() == 1) + + def _file_set(self, widget): + self.gconf_client.set_string(self.gconf_key + "/folder", widget.get_filename()) + + def _file_activated(self, widget): + self.gconf_client.set_string(self.gconf_key + "/folder", widget.get_filename()) + + +class G15LCDShot(): + + def __init__(self, screen, gconf_client, gconf_key): + self._screen = screen + self._gconf_client = gconf_client + self._gconf_key = gconf_key + self._recording = False + + def activate(self): + self._screen.key_handler.action_listeners.append(self) + + def deactivate(self): + self._screen.key_handler.action_listeners.remove(self) + + def destroy(self): + pass + + def action_performed(self, binding): + # TODO better key + if binding.action == SCREENSHOT: + mode = g15gconf.get_string_or_default(self._gconf_client, "%s/mode" % self._gconf_key, "still") + if mode == "still": + return self._take_still() + else: + if self._recording: + self._stop_recording() + else: + self._start_recording() + + def _encode(self): + cmd = ["mencoder", "-really-quiet", "mf://%s.tmp/*.jpeg" % self._record_to, "-mf", \ + "w=%d:h=%d:fps=%d:type=jpg" % (self._screen.device.lcd_size[0],self._screen.device.lcd_size[1],self._record_fps), "-ovc", "lavc", \ + "-lavcopts", "vcodec=mpeg4", "-oac", "copy", "-o", self._record_to] + try: + ret = subprocess.call(cmd) + if ret == 0: + g15notify.notify(_("LCD Screenshot"), _("Video encoding complete. Result at %s" % self._record_to), "dialog-info", timeout = 0) + shutil.rmtree("%s.tmp" % self._record_to, True) + else: + logger.error("Video encoding failed with status %d", ret) + g15notify.notify(_("LCD Screenshot"), _("Video encoding failed."), "dialog-error", timeout = 0) + except Exception as e: + logger.error("Video encoding failed.", exc_info = e) + g15notify.notify(_("LCD Screenshot"), _("Video encoding failed. Do you have mencoder installed?"), "dialog-error", timeout = 0) + + def _stop_recording(self): + self._recording = False + g15notify.notify(_("LCD Screenshot"), _("Video recording stopped. Now encoding"), "dialog-info", timeout = 0) + t = Thread(target = self._encode); + t.setName("LCDScreenshotEncode") + t.start() + + def _start_recording(self): + self._record_fps = g15gconf.get_int_or_default(self._gconf_client, "%s/fps" % self._gconf_key, 10) + path = self._find_next_free_filename("avi", _("Gnome15_Video")) + g15notify.notify(_("LCD Screenshot"), _("Started recording video"), "dialog-info") + g15os.mkdir_p("%s.tmp" % path) + self._frame_no = 1 + self._recording = True + self._record_to = path + self._frame() + + def _frame(self): + if self._recording: + try: + self._screen.draw_lock.acquire() + try: + path = os.path.join("%s.tmp" % self._record_to, "%012d.jpeg" % self._frame_no) + pixbuf = g15cairo.surface_to_pixbuf(self._screen.old_surface) + finally: + self._screen.draw_lock.release() + + pixbuf.save(path, "jpeg", {"quality":"100"}) + self._frame_no += 1 + except Exception as e: + logger.error("Failed to save screenshot.", exc_info = e) + self._screen.error_on_keyboard_display(_("Failed to save screenshot to %s. %s") % (dir, str(e))) + self._recording = False + + self._recording_timer = gobject.timeout_add(1000 / self._record_fps, self._frame) + + def _find_next_free_filename(self, ext, title): + dir_path = g15gconf.get_string_or_default(self._gconf_client, "%s/folder" % \ + self._gconf_key, os.path.expanduser("~/Desktop")) + for i in range(1, 9999): + path = "%s/%s-%s-%d.%s" % ( dir_path, \ + g15globals.name, title, i, ext ) + if not os.path.exists(path): + return path + raise Exception("Too many screenshots/videos in destination directory") + + def _take_still(self): + if self._screen.old_surface: + self._screen.draw_lock.acquire() + try: + path = self._find_next_free_filename("png", self._screen.get_visible_page().title) + self._screen.old_surface.write_to_png(path) + logger.info("Written to screenshot to %s", path) + g15notify.notify(_("LCD Screenshot"), _("Screenshot saved to %s") % path, "dialog-info", timeout = 0) + return True + except Exception as e: + logger.error("Failed to save screenshot.", exc_info = e) + self._screen.error_on_keyboard_display(_("Failed to save screenshot to %s. %s") % (dir, str(e))) + finally: + self._screen.draw_lock.release() + + return True + \ No newline at end of file diff --git a/src/plugins/lcdshot/lcdshot.ui b/src/plugins/lcdshot/lcdshot.ui new file mode 100644 index 0000000..2cb84d7 --- /dev/null +++ b/src/plugins/lcdshot/lcdshot.ui @@ -0,0 +1,200 @@ + + + + + + 1 + 100 + 10 + 1 + 10 + + + + + + + + + + + + + + 460 + False + 5 + LCD Screenshot Preferences + False + True + center-on-parent + dialog + + + True + False + 2 + + + True + False + 3 + 2 + 4 + + + 100 + True + False + 0 + Folder + + + + + 100 + True + False + 0 + Mode + + + 1 + 2 + + + + + True + False + select-folder + Choose A Destination Folder For The LCD Shots + + + 1 + 2 + + + + + True + False + ModeModel + + + + 2 + 1 + + + + + 1 + 2 + 1 + 2 + + + + + True + False + + + True + False + Frames per second + + + True + True + 0 + + + + + True + True + + True + False + False + True + True + FPSModel + + + True + True + 1 + + + + + 2 + 2 + 3 + + + + + False + True + 0 + + + + + True + False + end + + + gtk-close + True + True + True + True + + + False + False + 0 + + + + + False + True + end + 1 + + + + + True + False + 0 + 0 + 2 + mencoder is not installed in this computer. +You won't be able to record videos. + + + + + + + True + True + 2 + 2 + + + + + + button9 + + + diff --git a/src/plugins/lens/Makefile.am b/src/plugins/lens/Makefile.am new file mode 100644 index 0000000..7ebeea6 --- /dev/null +++ b/src/plugins/lens/Makefile.am @@ -0,0 +1,11 @@ +plugindir = $(datadir)/gnome15/plugins/lens +plugin_DATA = lens.py + +unitydir = $(datadir)/unity/lenses +unity_DATA = gnome15.svg + +placedir = $(datadir)/unity/lenses +place_DATA = gnome15.lens + +EXTRA_DIST = \ + $(plugin_DATA) $(place_DATA) $(unity_DATA) \ No newline at end of file diff --git a/src/plugins/lens/gnome15.lens b/src/plugins/lens/gnome15.lens new file mode 100644 index 0000000..117ece7 --- /dev/null +++ b/src/plugins/lens/gnome15.lens @@ -0,0 +1,15 @@ +# +# Core info on how to contact the place over DBus +# +[Lens] +DBusName=org.gnome15.Gnome15Lens +DBusPath=/org/gnome15/Gnome15Lens/lens +Name=Gnome15 Lens +Icon=/usr/share/unity/lenses/gnome15/gnome15.svg +Description=Lens that allows selection of the information on the LCD of your Logitech devices. +SearchHint=LCD Page name ... +Shortcut=g + +[Desktop Entry] +X-Ubuntu-Gettext-Domain=gnome15-lens + diff --git a/src/plugins/lens/gnome15.svg b/src/plugins/lens/gnome15.svg new file mode 100644 index 0000000..df59f82 --- /dev/null +++ b/src/plugins/lens/gnome15.svg @@ -0,0 +1,3076 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + G + G + G + G + G + diff --git a/src/plugins/lens/lens.py b/src/plugins/lens/lens.py new file mode 100644 index 0000000..a07f8ec --- /dev/null +++ b/src/plugins/lens/lens.py @@ -0,0 +1,280 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2011 Brett Smith +# +# 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 . + +import logging +import cairo +import os +logger = logging.getLogger(__name__) +import sys +from gi.repository import GLib, GObject, Gio +from gi.repository import Dee +# FIXME: Some weird bug in Dee or PyGI makes Dee fail unless we probe +# it *before* we import the Unity module... ?! +_m = dir(Dee.SequenceModel) +from gi.repository import Unity +from gnome15 import g15devices +from gnome15 import util.g15os as g15os +from gnome15 import util.g15icontools as g15icontools +from gnome15 import g15screen +from gnome15 import g15globals +from cStringIO import StringIO +import base64 + +# +# The primary bus name we grab *must* match what we specify in our .place file +# +BUS_NAME = "org.gnome15.Gnome15Lens" + +# These category ids must match the order in which we add them to the lens +CATEGORY_PAGES = 0 +CATEGORY_TOOLS = 1 + +# Plugin details - All of these must be provided +id="lens" +name="Unity Lens" +description="Integrates Gnome15 with Unity" +author="Brett Smith " +copyright="Copyright (C)2010 Brett Smith" +site="http://www.gnome15.org/" +has_preferences=False +global_plugin=True + +# Cached +cache_dir = os.path.join(g15globals.user_cache_dir, "lens") +if not os.path.exists(cache_dir): + g15os.mkdir_p(cache_dir) + +def create(gconf_key, gconf_client, service): + return G15Lens(service, gconf_client, gconf_key) + +class G15Lens(): + + def __init__(self, service, gconf_client, gconf_key): + self._service = service + self._gconf_client = gconf_client + self._gconf_key = gconf_key + self.listeners = [] + + def activate(self): + + session_bus_connection = Gio.bus_get_sync (Gio.BusType.SESSION, None) + session_bus = Gio.DBusProxy.new_sync (session_bus_connection, 0, None, + 'org.freedesktop.DBus', + '/org/freedesktop/DBus', + 'org.freedesktop.DBus', None) + result = session_bus.call_sync('RequestName', + GLib.Variant ("(su)", (BUS_NAME, 0x4)), + 0, -1, None) + + # Unpack variant response with signature "(u)". 1 means we got it. + result = result.unpack()[0] + + if result != 1 : + print >> sys.stderr, "Failed to own name %s. Bailing out." % BUS_NAME + raise Exception("Failed to own name %s. Bailing out." % BUS_NAME) + + self._lens = Unity.Lens.new("/org/gnome15/Gnome15Lens", "Gnome15Lens") + self._scope = Unity.PlaceEntryInfo.new ("/org/gnome15/Gnome15Lens/scope/main") + # + self._lens.props.search_hint = "LCD Page name ..." + self._lens.props.visible = True + self._lens.props.search_in_global = True + + # Populate categories + cats = [] + cats.append (Unity.Category.new ("Pages", + Gio.ThemedIcon.new("display"), + Unity.CategoryRenderer.VERTICAL_TILE)) + cats.append (Unity.Category.new ("Tools", + Gio.ThemedIcon.new("configuration-section"), + Unity.CategoryRenderer.VERTICAL_TILE)) + self._lens.props.categories = cats + + # Listen for changes and requests + self._scope.connect ("notify::active-search", self._on_search_changed) + self._scope.connect ("notify::active-global-search", self._on_global_search_changed) + + self._lens.add_local_scope (self._scope); + self._lens.export (); + + def do_activate(self, *args): + print "activate:", args + return Unity.ActivationStatus.ACTIVATED_HIDE_DASH + + def _activation(self, uri): + print uri + return True + + def screen_removed(self, screen): + for l in self.listeners: + if l.screen == screen: + self.listeners.remove(l) + return + + def service_stopped(self): + pass + + def screen_added(self, screen): + self._add_screen(screen) + + def service_stopping(self): + pass + + def service_starting_up(self): + pass + + def service_started_up(self): + pass + + def get_search_string (self): + search = self._scope.props.active_search + return search.get_search_string() if search else None + + def get_global_search_string (self): + search = self._scope.props.active_global_search + return search.get_search_string() if search else None + + def search_finished (self): + search = self._scope.props.active_search + if search: + search.emit ("finished") + + def global_search_finished (self): + search = self._scope.props.active_global_search + if search: + search.emit("finished") + + """ + Private + """ + def _on_activation(self, uri, callback, callback_target): + print "URI %s, %s, %s" % ( uri, str(callback), str(callback_target)) + + def _add_screen(self, screen): + listener = MenuScreenChangeListener(self, screen) + self.listeners.append(listener) + screen.add_screen_change_listener(listener) + + def _on_sections_synchronized (self, sections_model, *args): + # Column0: display name + # Column1: GIcon in string format + sections_model.clear () + for device in g15devices.find_all_devices(): + if device.model_id == 'virtual': + icon_file = g15icontools.get_icon_path(["preferences-system-window", "preferences-system-windows", "gnome-window-manager", "window_fullscreen"]) + else: + icon_file = g15icontools.get_app_icon(self._gconf_client, device.model_id) + icon = Gio.FileIcon(Gio.File(icon_file)) + sections_model.append (device.model_fullname, + icon.to_string()) + + def _on_groups_synchronized (self, groups_model, *args): + groups_model.clear () + groups_model.append ("UnityDefaultRenderer", + "Screens", + Gio.ThemedIcon("display").to_string()) + groups_model.append ("UnityDefaultRenderer", + "Tools", + Gio.ThemedIcon("preferences-system").to_string()) + + def _on_global_groups_synchronized (self, global_groups_model, *args): + # Just the same as the normal groups + self._on_groups_synchronized (global_groups_model) + + def _on_search_changed (self, *args): + search = self.get_search_string() + results = self._entry.props.results_model + + print "Search changed to: '%s'" % search + + self._update_results_model (search, results) + self.search_finished() + + def _on_global_search_changed (self, entry, param_spec): + search = self.get_global_search_string() + results = self._entry.props.global_renderer_info.props.results_model + + print "Global search changed to: '%s'" % search + + self._update_results_model (search, results) + self.global_search_finished() + + def _update_results_model (self, search, model): + model.clear () + search = search.lower() + print "Search> %s" % search + for listener in self.listeners: + print " L[%s]" % str(listener) + for page in listener.screen.pages: + if len(search) == 0 or search in page.title.lower(): + icon_hint = listener._get_page_filename(page) + uri = "gnome15://%s" % base64.encodestring(page.id) + print " URI %s" % uri + model.append (uri, # uri + icon_hint, # string formatted GIcon + CATEGORY_PAGES, # numeric group id + "text/html", # mimetype + page.title, # display name + page.title, # comment, + uri) # FIXME WHATSTHIS? + + print str(model) + + def deactivate(self): + pass + + def destroy(self): + pass + +class MenuScreenChangeListener(g15screen.ScreenChangeAdapter): + def __init__(self, plugin, screen): + self.plugin = plugin + self.screen = screen + for page in screen.pages: + self._add_page(page) + + def new_page(self, page): + print "Adding page %s for screen %s" % (page.id, self.screen.device.uid) + self._add_page(page) + + def title_changed(self, page, title): + self._update_page(page) + + def del_page(self, page): + filename = self._get_page_filename(page) + logger.info("Removing page thumbnail image", filename) + os.remove(filename) + + """ + Private + """ + def _get_page_filename(self, page): + return "%s/%s.png" % ( cache_dir, base64.encodestring(page.id) ) + + def _add_page(self, page): + self._update_page(page) + + def _update_page(self, page): + if page.thumbnail_painter != None: + img = cairo.ImageSurface(cairo.FORMAT_ARGB32, self.screen.width, self.screen.height) + thumb_canvas = cairo.Context(img) + try : + if page.thumbnail_painter(thumb_canvas, self.screen.height, True): + filename = self._get_page_filename(page) + logger.info("Writing thumbnail to %s", filename) + img.write_to_png(filename) + except Exception as e: + logger.warning("Problem with painting thumbnail.", exc_info = e) diff --git a/src/plugins/macro-recorder/Makefile.am b/src/plugins/macro-recorder/Makefile.am new file mode 100644 index 0000000..9a67b1d --- /dev/null +++ b/src/plugins/macro-recorder/Makefile.am @@ -0,0 +1,6 @@ +SUBDIRS = default +plugindir = $(datadir)/gnome15/plugins/macro-recorder +plugin_DATA = macro-recorder.py + +EXTRA_DIST = \ + $(plugin_DATA) \ No newline at end of file diff --git a/src/plugins/macro-recorder/default/Makefile.am b/src/plugins/macro-recorder/default/Makefile.am new file mode 100644 index 0000000..957a82b --- /dev/null +++ b/src/plugins/macro-recorder/default/Makefile.am @@ -0,0 +1,6 @@ +themedir = $(datadir)/gnome15/plugins/macro-recorder/default +theme_DATA = g19.svg \ + default.svg + +EXTRA_DIST = \ + $(theme_DATA) diff --git a/src/plugins/macro-recorder/default/default.svg b/src/plugins/macro-recorder/default/default.svg new file mode 100644 index 0000000..e09029e --- /dev/null +++ b/src/plugins/macro-recorder/default/default.svg @@ -0,0 +1,157 @@ + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + ${profile} + ${memory} + + ${message} + ${profile} + ${memory} + + + diff --git a/src/plugins/macro-recorder/default/g19.svg b/src/plugins/macro-recorder/default/g19.svg new file mode 100644 index 0000000..60f0209 --- /dev/null +++ b/src/plugins/macro-recorder/default/g19.svg @@ -0,0 +1,173 @@ + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + ${profile} + ${memory} + + ${message} + + diff --git a/src/plugins/macro-recorder/i18n/macro-recorder.en_GB.po b/src/plugins/macro-recorder/i18n/macro-recorder.en_GB.po new file mode 100644 index 0000000..9e74234 --- /dev/null +++ b/src/plugins/macro-recorder/i18n/macro-recorder.en_GB.po @@ -0,0 +1,60 @@ +# English translations for gnome15-plugins package. +# Copyright (C) 2012 THE gnome15-plugins'S COPYRIGHT HOLDER +# This file is distributed under the same license as the gnome15-plugins package. +# Automatically generated, 2012. +# +msgid "" +msgstr "" +"Project-Id-Version: gnome15-plugins 0.8.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: 2012-01-08 11:23+0000\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: en_GB\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=ASCII\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: macro-recorder.py:57 +msgid "Macro Recorder" +msgstr "Macro Recorder" + +#: macro-recorder.py:58 +msgid "" +"Allows recording of macros. All feedback is provided via the LCD (when " +"available), as well as blinking of memory bank lights when recording. You " +"may also delete macros by assigning an empty macro to a key. The macro will " +"be recorded on the currently selected profile and memory bank." +msgstr "" +"Allows recording of macros. All feedback is provided via the LCD (when " +"available), as well as blinking of memory bank lights when recording. You " +"may also delete macros by assigning an empty macro to a key. The macro will " +"be recorded on the currently selected profile and memory bank." + +#: macro-recorder.py:63 +msgid "Copyright (C)2010 Brett Smith" +msgstr "Copyright (C)2010 Brett Smith" + +#: macro-recorder.py:69 +msgid "Start recording macro" +msgstr "Start recording macro" + +#: macro-recorder.py:307 +#, python-format +msgid "" +"Recording on M%s. Type in your macro then press the G-Key to assign it to, " +"or MR to cancel." +msgstr "" +"Recording on M%s. Type in your macro then press the G-Key to assign it to, " +"or MR to cancel." + +#: macro-recorder.py:311 +msgid "No Profile" +msgstr "No Profile" + +#: macro-recorder.py:313 +msgid "You have no profiles configured. Configure one now using the Macro tool" +msgstr "" +"You have no profiles configured. Configure one now using the Macro tool" diff --git a/src/plugins/macro-recorder/i18n/macro-recorder.pot b/src/plugins/macro-recorder/i18n/macro-recorder.pot new file mode 100644 index 0000000..7d22c3c --- /dev/null +++ b/src/plugins/macro-recorder/i18n/macro-recorder.pot @@ -0,0 +1,53 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: macro-recorder.py:57 +msgid "Macro Recorder" +msgstr "" + +#: macro-recorder.py:58 +msgid "" +"Allows recording of macros. All feedback is provided via the LCD (when " +"available), as well as blinking of memory bank lights when recording. You " +"may also delete macros by assigning an empty macro to a key. The macro will " +"be recorded on the currently selected profile and memory bank." +msgstr "" + +#: macro-recorder.py:63 +msgid "Copyright (C)2010 Brett Smith" +msgstr "" + +#: macro-recorder.py:69 +msgid "Start recording macro" +msgstr "" + +#: macro-recorder.py:307 +#, python-format +msgid "" +"Recording on M%s. Type in your macro then press the G-Key to assign it to, " +"or MR to cancel." +msgstr "" + +#: macro-recorder.py:311 +msgid "No Profile" +msgstr "" + +#: macro-recorder.py:313 +msgid "You have no profiles configured. Configure one now using the Macro tool" +msgstr "" diff --git a/src/plugins/macro-recorder/macro-recorder.py b/src/plugins/macro-recorder/macro-recorder.py new file mode 100644 index 0000000..690379d --- /dev/null +++ b/src/plugins/macro-recorder/macro-recorder.py @@ -0,0 +1,334 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2010 Brett Smith +# +# 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 . + +import gnome15.g15locale as g15locale +_ = g15locale.get_translation("macro-recorder", modfile = __file__).ugettext + +import gnome15.g15screen as g15screen +import gnome15.g15theme as g15theme +import gnome15.g15devices as g15devices +import gnome15.util.g15icontools as g15icontools +import gnome15.g15driver as g15driver +import gnome15.g15profile as g15profile +import gnome15.g15actions as g15actions +import datetime +from threading import Timer +import gtk +import os +import sys +import time +import logging +logger = logging.getLogger(__name__) + +from Xlib import X, XK, display +from Xlib.ext import record +from Xlib.protocol import rq + +from threading import Thread + +# Custom actions +RECORD = "record" + +# Register the action with all supported models +g15devices.g15_action_keys[RECORD] = g15actions.ActionBinding(RECORD, [ g15driver.G_KEY_MR ], g15driver.KEY_STATE_UP) +g15devices.g19_action_keys[RECORD] = g15actions.ActionBinding(RECORD, [ g15driver.G_KEY_MR ], g15driver.KEY_STATE_UP) +g15devices.g110_action_keys[RECORD] = g15actions.ActionBinding(RECORD, [ g15driver.G_KEY_MR ], g15driver.KEY_STATE_UP) + +# Plugin details - All of these must be provided +id="macro-recorder" +name=_("Macro Recorder") +description=_("Allows recording of macros. All feedback is provided via the LCD (when available), \ +as well as blinking of memory bank lights when recording. \ +You may also delete macros by assigning an empty macro to a key. \ +The macro will be recorded on the currently selected profile and memory bank.") +author="Brett Smith " +copyright=_("Copyright (C)2010 Brett Smith") +site="http://www.russo79.com/gnome15" +has_preferences=False +default_enabled=True +unsupported_models = [ g15driver.MODEL_Z10, g15driver.MODEL_MX5500, g15driver.MODEL_G930, g15driver.MODEL_G35 ] +actions={ + RECORD : _("Start recording macro") + } + + +local_dpy = display.Display() +record_dpy = display.Display() + +def create(gconf_key, gconf_client, screen): + return G15MacroRecorder(gconf_key, gconf_client, screen) + + +class RecordThread(Thread): + def __init__(self, _record_callback): + Thread.__init__(self) + self.setDaemon(True) + self.name = "RecordThread" + self._record_callback = _record_callback + self.ctx = record_dpy.record_create_context( + 0, + [record.AllClients], + [{ + 'core_requests': (0, 0), + 'core_replies': (0, 0), + 'ext_requests': (0, 0, 0, 0), + 'ext_replies': (0, 0, 0, 0), + 'delivered_events': (0, 0), + 'device_events': (X.KeyPress, X.MotionNotify), + 'errors': (0, 0), + 'client_started': False, + 'client_died': False, + }]) + + def disable_record_context(self): + if self.ctx != None: + local_dpy.record_disable_context(self.ctx) + local_dpy.flush() + + def run(self): + record_dpy.record_enable_context(self.ctx, self._record_callback) + record_dpy.record_free_context(self.ctx) + +class MacroRecorderScreenChangeListener(g15screen.ScreenChangeAdapter): + def __init__(self, plugin): + self._plugin = plugin + + def memory_bank_changed(self, new_bank_number): + self._plugin._redraw() + +class G15MacroRecorder(): + + def __init__(self, gconf_key, gconf_client, screen): + self._screen = screen + self._gconf_client = gconf_client + self._gconf_key = gconf_key + self._record_key = None + self._record_thread = None + self._last_keys = None + self._page = None + self._key_down = None + self._message = None + self._lights_control = None + + def activate(self): + self._theme = g15theme.G15Theme(self) + self._screen.key_handler.action_listeners.append(self) + self._listener = MacroRecorderScreenChangeListener(self) + self._screen.add_screen_change_listener(self._listener) + + def deactivate(self): + self._cancel_macro() + self._screen.key_handler.action_listeners.remove(self) + self._screen.remove_screen_change_listener(self._listener) + + def destroy(self): + if self._record_thread != None: + self._record_thread.disable_record_context() + + def action_performed(self, binding): + if binding.action == RECORD: + if self._record_thread is None: + self._start_recording() + return True + else: + self._cancel_macro(None) + return True + + def handle_key(self, keys, state, post): + # Memory keys + + if self._record_thread != None: + # Let the M1-M3 and MR key be handled as actions + if g15driver.G_KEY_MR in keys or g15driver.G_KEY_M1 in keys or g15driver.G_KEY_M2 in keys or g15driver.G_KEY_M3 in keys: + return False + + # Stop recording on release of a macro key + if not post and ( state == g15driver.KEY_STATE_UP or state == g15driver.KEY_STATE_HELD): + """ + All other keys end recording. We use the UP keystate, so it doesn't trigger the + macro itself when it is released at the end of recording + """ + self._last_keys = keys + self._record_keys = keys + self._done_recording(state) + + # When recording, we want all key events until recording is done + return True + + ''' + Private + ''' + + def _lookup_keysym(self, keysym): + logger.debug("Looking up %s", keysym) + for name in dir(XK): + logger.debug(" %s", name) + if name[:3] == "XK_" and getattr(XK, name) == keysym: + return name[3:] + return "[%d]" % keysym + + def _record_callback(self, reply): + if reply.category != record.FromServer: + return + if reply.client_swapped: + return + if not len(reply.data) or ord(reply.data[0]) < 2: + # not an event + return + + data = reply.data + while len(data): + event, data = rq.EventField(None).parse_binary_value(data, record_dpy.display, None, None) + if event.type in [X.KeyPress, X.KeyRelease]: + pr = event.type == X.KeyPress and "Press" or "Release" + logger.debug("Event detail = %s", event.detail) + keysym = local_dpy.keycode_to_keysym(event.detail, 0) + if not keysym: + logger.debug("Recorded %s", event.detail) + self._record_key_callback(event, event.detail) + else: + logger.debug("Keysym = %s", str(keysym)) + s = self._lookup_keysym(keysym) + logger.debug("Recorded %s", s) + self._record_key_callback(event, s) + + self._redraw() + + def _record_key_callback(self, event, keyname): + if self._key_down == None: + self._key_down = time.time() + else: + now = time.time() + delay = time.time() - self._key_down + self._script_model.append(["Delay", str(int(delay * 1000))]) + self._key_down = now + pr = event.type == X.KeyPress and "Press" or "Release" + keydown = self._key_state[keyname] if keyname in self._key_state else None + if keydown is None: + if event.type == X.KeyPress: + self._key_state[keyname] = True + self._script_model.append([pr, keyname]) + else: + # Got a release without getting a press - ignore + pass + else: + if event.type == X.KeyRelease: + self._script_model.append([pr, keyname]) + del self._key_state[keyname] + + def _done_recording(self, state): + if self._record_keys != None: + record_keys = self._record_keys + self._halt_recorder() + + active_profile = g15profile.get_active_profile(self._screen.device) + key_name = ", ".join(g15driver.get_key_names(record_keys)) + if len(self._script_model) == 0: + self.icon = "edit-delete" + self._message = key_name + " deleted" + active_profile.delete_macro(state, self._screen.get_memory_bank(), record_keys) + self._screen.redraw(self._page) + else: + macro_script = "" + for row in self._script_model: + if len(macro_script) != 0: + macro_script += "\n" + macro_script += row[0] + " " + row[1] + self.icon = "tag-new" + self._message = key_name + " created" + memory = self._screen.get_memory_bank() + macro = active_profile.get_macro(state, memory, record_keys) + if macro: + macro.type = g15profile.MACRO_SCRIPT + macro.macro = macro_script + macro.save() + else: + active_profile.create_macro(memory, record_keys, key_name, g15profile.MACRO_SCRIPT, macro_script, state) + self._redraw() + self._hide_recorder(3.0) + else: + self._hide_recorder() + + def _hide_recorder(self, after = 0.0): + if self._lights_control: + self._screen.release_defeat_profile_change() + self._screen.driver.release_control(self._lights_control) + self._lights_control = None + if self._page: + if after == 0.0: + self._screen.del_page(self._page) + else: + self._screen.delete_after(after, self._page) + self._page = None + + def _halt_recorder(self): + if self._record_thread != None: + self._record_thread.disable_record_context() + self._key_down = None + self._record_key = None + self._record_thread = None + + def _cancel_macro(self,event = None,data=None): + self._halt_recorder() + self._hide_recorder() + + def _redraw(self): + if self._page != None: + self._screen.redraw(self._page) + + def _start_recording(self): + self._script_model = [] + self._key_state = {} + self._key_down = None + if self._screen.driver.get_bpp() > 0: + if self._page == None: + self._page = g15theme.G15Page(id, self._screen, priority=g15screen.PRI_EXCLUSIVE,\ + title = name, theme_properties_callback = self._get_theme_properties, \ + theme = self._theme, + originating_plugin = self) + self._screen.add_page(self._page) + self.icon = "media-record" + self._message = None + self._redraw() + self._record_thread = RecordThread(self._record_callback) + self._record_thread.start() + self._lights_control = self._screen.driver.acquire_control_with_hint(g15driver.HINT_MKEYS) + self._lights_control.set_value(self._screen.get_memory_bank() | g15driver.MKEY_LIGHT_MR) + self._lights_control.blink(0, 0.5) + self._screen.request_defeat_profile_change() + + def _get_theme_properties(self): + + active_profile = g15profile.get_active_profile(self._screen.device) + + properties = {} + properties["icon"] = g15icontools.get_icon_path(self.icon, self._screen.height) + properties["memory"] = "M%d" % self._screen.get_memory_bank() + + if active_profile != None: + properties["profile"] = active_profile.name + properties["profile_icon"] = active_profile.get_profile_icon_path(self._screen.height) + + if self._message == None: + properties["message"] = _("Recording on M%s. Type in your macro then press the G-Key to assign it to, or MR to cancel." % self._screen.get_memory_bank()) + else: + properties["message"] = self._message + else: + properties["profile"] = _("No Profile") + properties["profile_icon"] = "" + properties["message"] = _("You have no profiles configured. Configure one now using the Macro tool") + + return properties \ No newline at end of file diff --git a/src/plugins/macros/Makefile.am b/src/plugins/macros/Makefile.am new file mode 100644 index 0000000..52dd508 --- /dev/null +++ b/src/plugins/macros/Makefile.am @@ -0,0 +1,6 @@ +SUBDIRS = default +plugindir = $(datadir)/gnome15/plugins/macros +plugin_DATA = macros.py macros.ui + +EXTRA_DIST = \ + $(plugin_DATA) \ No newline at end of file diff --git a/src/plugins/macros/default/Makefile.am b/src/plugins/macros/default/Makefile.am new file mode 100644 index 0000000..0f3abec --- /dev/null +++ b/src/plugins/macros/default/Makefile.am @@ -0,0 +1,8 @@ +themedir = $(datadir)/gnome15/plugins/macros/default +theme_DATA = g19-menu-screen.svg \ + g19-menu-entry.svg \ + default-menu-screen.svg \ + default-menu-entry.svg + +EXTRA_DIST = \ + $(theme_DATA) diff --git a/src/plugins/macros/default/default-menu-entry.svg b/src/plugins/macros/default/default-menu-entry.svg new file mode 100644 index 0000000..b160dae --- /dev/null +++ b/src/plugins/macros/default/default-menu-entry.svg @@ -0,0 +1,229 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + ${item_name} + ${item_key} + + + + ${item_name} + ${item_key} + + diff --git a/src/plugins/macros/default/default-menu-screen.svg b/src/plugins/macros/default/default-menu-screen.svg new file mode 100644 index 0000000..c1f3add --- /dev/null +++ b/src/plugins/macros/default/default-menu-screen.svg @@ -0,0 +1,152 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + ${title} + + + + + + + + ${mkey} + No Macros Configured on ${mkey} + + diff --git a/src/plugins/macros/default/g19-menu-entry.svg b/src/plugins/macros/default/g19-menu-entry.svg new file mode 100644 index 0000000..65a8f19 --- /dev/null +++ b/src/plugins/macros/default/g19-menu-entry.svg @@ -0,0 +1,449 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + ${item_name} + + + + + diff --git a/src/plugins/macros/default/g19-menu-screen.svg b/src/plugins/macros/default/g19-menu-screen.svg new file mode 100644 index 0000000..5a05828 --- /dev/null +++ b/src/plugins/macros/default/g19-menu-screen.svg @@ -0,0 +1,237 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + ${title} + + ${mkey} + No Macros Configured For ${mkey} + + + + + + + diff --git a/src/plugins/macros/i18n/macros.en_GB.po b/src/plugins/macros/i18n/macros.en_GB.po new file mode 100644 index 0000000..01c562a --- /dev/null +++ b/src/plugins/macros/i18n/macros.en_GB.po @@ -0,0 +1,46 @@ +# English translations for gnome15-plugins package. +# Copyright (C) 2012 THE gnome15-plugins'S COPYRIGHT HOLDER +# This file is distributed under the same license as the gnome15-plugins package. +# Automatically generated, 2012. +# +msgid "" +msgstr "" +"Project-Id-Version: gnome15-plugins 0.8.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: 2012-01-08 11:23+0000\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: en_GB\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=ASCII\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: i18n/macros.glade.h:1 +msgid "Indicator Messages Preferences" +msgstr "Indicator Messages Preferences" + +#: i18n/macros.glade.h:2 +msgid "Raise page when profile or memory bank changes" +msgstr "Raise page when profile or memory bank changes" + +#: i18n/macros.glade.h:3 +msgid "center" +msgstr "center" + +#: i18n/macros.glade.h:4 +msgid "scale" +msgstr "scale" + +#: i18n/macros.glade.h:5 +msgid "stretch" +msgstr "stretch" + +#: i18n/macros.glade.h:6 +msgid "tile" +msgstr "tile" + +#: i18n/macros.glade.h:7 +msgid "zoom" +msgstr "zoom" diff --git a/src/plugins/macros/i18n/macros.glade.h b/src/plugins/macros/i18n/macros.glade.h new file mode 100644 index 0000000..02e93ac --- /dev/null +++ b/src/plugins/macros/i18n/macros.glade.h @@ -0,0 +1,7 @@ +char *s = N_("Indicator Messages Preferences"); +char *s = N_("Raise page when profile or memory bank changes"); +char *s = N_("center"); +char *s = N_("scale"); +char *s = N_("stretch"); +char *s = N_("tile"); +char *s = N_("zoom"); diff --git a/src/plugins/macros/i18n/macros.pot b/src/plugins/macros/i18n/macros.pot new file mode 100644 index 0000000..7b0f6a5 --- /dev/null +++ b/src/plugins/macros/i18n/macros.pot @@ -0,0 +1,46 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: i18n/macros.glade.h:1 +msgid "Indicator Messages Preferences" +msgstr "" + +#: i18n/macros.glade.h:2 +msgid "Raise page when profile or memory bank changes" +msgstr "" + +#: i18n/macros.glade.h:3 +msgid "center" +msgstr "" + +#: i18n/macros.glade.h:4 +msgid "scale" +msgstr "" + +#: i18n/macros.glade.h:5 +msgid "stretch" +msgstr "" + +#: i18n/macros.glade.h:6 +msgid "tile" +msgstr "" + +#: i18n/macros.glade.h:7 +msgid "zoom" +msgstr "" diff --git a/src/plugins/macros/macros.py b/src/plugins/macros/macros.py new file mode 100644 index 0000000..86c9f3e --- /dev/null +++ b/src/plugins/macros/macros.py @@ -0,0 +1,218 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2010 Brett Smith +# +# 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 . + +import gnome15.g15locale as g15locale +_ = g15locale.get_translation("macros", modfile = __file__).ugettext + +import gnome15.g15profile as g15profile +import gnome15.g15driver as g15driver +import gnome15.util.g15uigconf as g15uigconf +import gnome15.util.g15gconf as g15gconf +import gnome15.g15globals as g15globals +import gnome15.g15theme as g15theme +import gnome15.g15screen as g15screen +import gnome15.g15plugin as g15plugin +import gtk +import os +import logging +import time +logger = logging.getLogger(__name__) + +# Plugin details - All of these must be provided +id="macros" +name=_("Macro Information") +description=_("Displays the currently active macro profile and a summary of available keys.\ +Also, the screen will be cycled to when a macro is activated and the key will be \ +highlighted.") +author="Brett Smith " +copyright=_("Copyright (C)2010 Brett Smith") +site="http://www.russo79.com/gnome15" +has_preferences=True +unsupported_models = [ g15driver.MODEL_G110, g15driver.MODEL_Z10, g15driver.MODEL_G11, g15driver.MODEL_MX5500, g15driver.MODEL_G930, g15driver.MODEL_G35 ] +actions={ + g15driver.PREVIOUS_SELECTION : _("Previous macro"), + g15driver.NEXT_SELECTION : _("Next macro"), + g15driver.NEXT_PAGE : _("Next page"), + g15driver.PREVIOUS_PAGE : _("Previous page") + } + +def create(gconf_key, gconf_client, screen): + return G15Macros(gconf_client, gconf_key, screen) + +def show_preferences(parent, driver, gconf_client, gconf_key): + widget_tree = gtk.Builder() + widget_tree.add_from_file(os.path.join(os.path.dirname(__file__), "macros.ui")) + dialog = widget_tree.get_object("MacrosDialog") + dialog.set_transient_for(parent) + g15uigconf.configure_checkbox_from_gconf(gconf_client, "%s/raise" % gconf_key, "RaisePageCheckbox", True, widget_tree) + dialog.run() + dialog.hide() + +""" +Represents a mount as a single item in a menu +""" +class MacroMenuItem(g15theme.MenuItem): + def __init__(self, macro, component_id): + g15theme.MenuItem.__init__(self, component_id) + self.macro = macro + + def get_theme_properties(self): + item_properties = g15theme.MenuItem.get_theme_properties(self) + item_properties["item_name"] = self.macro.name + item_properties["item_type"] = "" + item_properties["item_key"] = ",".join(g15driver.get_key_names(self.macro.keys)) + for r in range(0, len(self.macro.keys)): + item_properties["icon%d" % (r + 1)] = os.path.join(g15globals.image_dir, "key-%s.png" % self.macro.keys[r]) + return item_properties + + def get_default_theme_dir(self): + return os.path.join(os.path.dirname(__file__), "default") + + def activate(self): + pass + +""" +Macros plugin class +""" +class G15Macros(g15plugin.G15MenuPlugin): + + def __init__(self, gconf_client, gconf_key, screen): + g15plugin.G15MenuPlugin.__init__(self, gconf_client, gconf_key, screen, ["preferences-desktop-keyboard-shortcuts", "input-keyboard"], id, name) + + def activate(self): + self._get_configuration() + g15plugin.G15MenuPlugin.activate(self) + self._notify_handles = [] + self._notify_handles.append(self.gconf_client.notify_add("/apps/gnome15/%s/active_profile" % self.screen.device.uid, self._profiles_changed)) + self._notify_handles.append(self.gconf_client.notify_add("/apps/gnome15/%s/locked" % self.screen.device.uid, self._profiles_changed)) + g15profile.profile_listeners.append(self._profiles_changed) + self.listener = MacrosScreenChangeAdapter(self) + self.screen.add_screen_change_listener(self.listener) + + def deactivate(self): + g15plugin.G15MenuPlugin.deactivate(self) + for h in self._notify_handles: + self.gconf_client.notify_remove(h) + g15profile.profile_listeners.remove(self._profiles_changed) + self.screen.remove_screen_change_listener(self.listener) + + def get_theme_path(self): + return os.path.join(os.path.dirname(__file__), "default") + + def get_theme_properties(self): + properties = g15plugin.G15MenuPlugin.get_theme_properties(self) + properties["title"] = self._active_profile.name + properties["mkey"] = "M%d" % self._mkey + properties["icon"] = self._get_active_profile_icon_path() + return properties + + def _get_active_profile_icon_path(self): + if self._active_profile == None: + return None + return self._active_profile.get_profile_icon_path(self.screen.height) + + """ + Screen change listener callbacks + + """ + def memory_bank_changed(self): + g15screen.run_on_redraw(self._reload_and_popup) + + """ + Private functions + """ + def _profiles_changed(self, arg0 = None, arg1 = None, arg2 = None, arg3 = None): + self._reload_and_popup() + + def _reload(self): + self.load_menu_items() + self.screen.redraw(self.page) + + def _reload_and_popup(self): + self._reload() + if g15gconf.get_bool_or_default(self.gconf_client, "%s/raise" % self.gconf_key, True): + self._popup() + + def load_menu_items(self): + """ + Reload all items for the current profile and bank + """ + self._get_configuration() + self.menu.remove_all_children() + self.page.set_title(_("Macros - %s") % self._active_profile.name) + + macro_keys = [] + macros = [] + self._load_profile(self._active_profile, macros, macro_keys) + macros.sort(self._comparator) + for macro in macros: + self.menu.add_child(MacroMenuItem(macro, "macro-%s" % macro.key_list_key)) + + def _load_profile(self, profile, macros, macro_keys): + for bank in profile.macros.values(): + for m in bank[self._mkey - 1]: + if not m.keys in macro_keys: + macros.append(m) + macro_keys.append(m.keys) + if profile.base_profile != None and profile.base_profile != "": + self._load_profile(g15profile.get_profile(profile.device, profile.base_profile), macros, macro_keys) + + def _comparator(self, o1, o2): + return o1.compare(o2) + + def _get_configuration(self): + self._mkey = self.screen.get_memory_bank() + self._active_profile = g15profile.get_active_profile(self.screen.device) + + def _popup(self): + """ + Popup the page + """ + self._raise_timer = self.screen.set_priority(self.page, g15screen.PRI_HIGH, revert_after = 4.0) + self.screen.redraw(self.page) + + def _remove_macro(self, macro): + """ + Remove a macro from the menu + """ + logger.info("Removing macro %s", str(macro.name)) + self.menu.remove_child(self._get_item_for_macro(macro)) + self.screen.redraw(self.page) + + def _get_item_for_macro(self, macro): + """ + Get the menu item for the given macro + """ + for item in self.menu.get_children(): + if isinstance(item, MacroMenuItem) and item.macro == macro: + return item + + def _add_macro(self, macro): + """ + Add a new macro to the menu + """ + item = MacroMenuItem(macro, self, "macro-%s" % macro.key_list_key) + self.menu.add_child(item) + +class MacrosScreenChangeAdapter(g15screen.ScreenChangeAdapter): + def __init__(self, plugin): + self.plugin = plugin + + def memory_bank_changed(self, new_bank_number): + self.plugin._get_configuration() + self.plugin._reload() + if g15gconf.get_bool_or_default(self.plugin.gconf_client, "%s/raise" % self.plugin.gconf_key, True): + self.plugin._popup() \ No newline at end of file diff --git a/src/plugins/macros/macros.ui b/src/plugins/macros/macros.ui new file mode 100644 index 0000000..e151c2e --- /dev/null +++ b/src/plugins/macros/macros.ui @@ -0,0 +1,101 @@ + + + + + + + False + 5 + Indicator Messages Preferences + False + True + center-on-parent + dialog + + + True + False + 2 + + + True + False + 4 + + + Raise page when profile or memory bank changes + True + True + False + True + + + True + True + 0 + + + + + False + False + 0 + + + + + True + False + end + + + gtk-close + True + True + True + True + + + False + False + 0 + + + + + False + True + end + 1 + + + + + + button9 + + + + + + + + + + zoom + + + tile + + + center + + + scale + + + stretch + + + + diff --git a/src/plugins/mediaplayer/Makefile.am b/src/plugins/mediaplayer/Makefile.am new file mode 100644 index 0000000..6b8b8bd --- /dev/null +++ b/src/plugins/mediaplayer/Makefile.am @@ -0,0 +1,7 @@ +SUBDIRS = default + +plugindir = $(datadir)/gnome15/plugins/mediaplayer +plugin_DATA = mediaplayer.py + +EXTRA_DIST = \ + $(plugin_DATA) \ No newline at end of file diff --git a/src/plugins/mediaplayer/default/Makefile.am b/src/plugins/mediaplayer/default/Makefile.am new file mode 100644 index 0000000..62e13e6 --- /dev/null +++ b/src/plugins/mediaplayer/default/Makefile.am @@ -0,0 +1,8 @@ +themedir = $(datadir)/gnome15/plugins/mediaplayer/default +theme_DATA = g19.svg \ + g19-mediakeys.svg \ + default.svg \ + default-mediakeys.svg + +EXTRA_DIST = \ + $(theme_DATA) diff --git a/src/plugins/mediaplayer/default/WIPg19.svg b/src/plugins/mediaplayer/default/WIPg19.svg new file mode 100644 index 0000000..a14b831 --- /dev/null +++ b/src/plugins/mediaplayer/default/WIPg19.svg @@ -0,0 +1,939 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + G1 + G2 + G3 + G4 + G5 + Open + Play + Stop + Aspect + Mute + ${aspect} + + + OK + + + + + + diff --git a/src/plugins/mediaplayer/default/default-mediakeys.svg b/src/plugins/mediaplayer/default/default-mediakeys.svg new file mode 100644 index 0000000..0f37757 --- /dev/null +++ b/src/plugins/mediaplayer/default/default-mediakeys.svg @@ -0,0 +1,987 @@ + + + + + + + ${track_name} + + + + + + + + + image/svg+xml + + + + + + + + + + + ${track_progress} / ${track_duration} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Aspect + + + Aspect + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/plugins/mediaplayer/default/default.svg b/src/plugins/mediaplayer/default/default.svg new file mode 100644 index 0000000..3a7cee7 --- /dev/null +++ b/src/plugins/mediaplayer/default/default.svg @@ -0,0 +1,394 @@ + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + Stop + + + ${track_progress} / ${track_duration} + + <Hold + Aspect + + + + Rew + Fwd + ${play_pause} + + + + Aspect + + + + Rew + + + + + Fwd + + + + + + + ${play_pause} + + + + + + Stop + + ${track_name} + + diff --git a/src/plugins/mediaplayer/default/g19-mediakeys.svg b/src/plugins/mediaplayer/default/g19-mediakeys.svg new file mode 100644 index 0000000..70ac49e --- /dev/null +++ b/src/plugins/mediaplayer/default/g19-mediakeys.svg @@ -0,0 +1,1224 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + ${play_pause} + Forward + Rewind + Aspect + Stop + ${aspect} + + + + + ${track_progress} / ${track_duration} + + + + + + + + + + + + + + + + + + diff --git a/src/plugins/mediaplayer/default/g19.svg b/src/plugins/mediaplayer/default/g19.svg new file mode 100644 index 0000000..8f422e6 --- /dev/null +++ b/src/plugins/mediaplayer/default/g19.svg @@ -0,0 +1,1178 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + ${play_pause} + Forward + Rewind + Aspect + Stop + ${aspect} + Ok + + + + + + + + + + ${track_progress} / ${track_duration} + + diff --git a/src/plugins/mediaplayer/gtkplayer.py b/src/plugins/mediaplayer/gtkplayer.py new file mode 100644 index 0000000..4444cfc --- /dev/null +++ b/src/plugins/mediaplayer/gtkplayer.py @@ -0,0 +1,72 @@ +#!/usr/bin/python + +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2011 Brett Smith +# +# 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 . + +import gst +import gtk + +class Main: + def __init__(self): + + # Create GUI objects + self.window = gtk.Window() + self.vbox = gtk.VBox() + self.da = gtk.DrawingArea() + self.bb = gtk.HButtonBox() + self.da.set_size_request(300, 150) + self.playButton = gtk.Button(stock="gtk - media - play") + self.playButton.connect("clicked", self.OnPlay) + self.stopButton = gtk.Button(stock="gtk - media - stop") + self.stopButton.connect("clicked", self.OnStop) + self.quitButton = gtk.Button(stock="gtk - quit") + self.quitButton.connect("clicked", self.OnQuit) + self.vbox.pack_start(self.da) + self.bb.add(self.playButton) + self.bb.add(self.stopButton) + self.bb.add(self.quitButton) + self.vbox.pack_start(self.bb) + self.window.add(self.vbox) + + # Create GStreamer pipeline + self.pipeline = gst.Pipeline("mypipeline") + # Set up our video test source + self.videotestsrc = gst.element_factory_make("videotestsrc", "video") + # Add it to the pipeline + self.pipeline.add(self.videotestsrc) + # Now we need somewhere to send the video + self.sink = gst.element_factory_make("xvimagesink", "sink") + # Add it to the pipeline + self.pipeline.add(self.sink) + # Link the video source to the sink - xv + self.videotestsrc.link(self.sink) + self.window.show_all() + + def OnPlay(self, widget): + print "play" + # Tell the video sink to display the output in our DrawingArea + self.sink.set_xwindow_id(self.da.window.xid) + self.pipeline.set_state(gst.STATE_PLAYING) + + def OnStop(self, widget): + print "stop" + self.pipeline.set_state(gst.STATE_READY) + + def OnQuit(self, widget): + gtk.main_quit() + +start = Main() +gtk.main() diff --git a/src/plugins/mediaplayer/i18n/videoplayer.en_GB.po b/src/plugins/mediaplayer/i18n/videoplayer.en_GB.po new file mode 100644 index 0000000..c4db6a8 --- /dev/null +++ b/src/plugins/mediaplayer/i18n/videoplayer.en_GB.po @@ -0,0 +1,64 @@ +# English translations for gnome15-plugins package. +# Copyright (C) 2012 THE gnome15-plugins'S COPYRIGHT HOLDER +# This file is distributed under the same license as the gnome15-plugins package. +# Automatically generated, 2012. +# +msgid "" +msgstr "" +"Project-Id-Version: gnome15-plugins 0.8.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2011-10-09 19:46+0100\n" +"PO-Revision-Date: 2011-10-09 19:46+0100\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: en_GB\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=ASCII\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: videoplayer.py:39 +msgid "Video Player" +msgstr "Video Player" + +#: videoplayer.py:40 +msgid "" +"Plays videos! Very much experimental, this plugin uses mplayer to generate " +"JPEG images which are then loaded and displayed on the LCD. This means it is " +"very CPU AND disk intensive and should only be used as a toy. " +msgstr "" +"Plays videos! Very much experimental, this plugin uses mplayer to generate " +"JPEG images which are then loaded and displayed on the LCD. This means it is " +"very CPU AND disk intensive and should only be used as a toy. " + +#: videoplayer.py:45 +msgid "Copyright (C)2010 Brett Smith" +msgstr "Copyright (C)2010 Brett Smith" + +#: videoplayer.py:50 +msgid "Stop" +msgstr "Stop" + +#: videoplayer.py:51 +msgid "Play" +msgstr "Play" + +#: videoplayer.py:52 +msgid "Open file" +msgstr "Open file" + +#: videoplayer.py:53 +msgid "Toggle Mute" +msgstr "Toggle Mute" + +#: videoplayer.py:54 +msgid "Change aspect" +msgstr "Change aspect" + +#: videoplayer.py:211 +msgid "All files" +msgstr "All files" + +#: videoplayer.py:216 +msgid "Movies" +msgstr "Movies" diff --git a/src/plugins/mediaplayer/i18n/videoplayer.pot b/src/plugins/mediaplayer/i18n/videoplayer.pot new file mode 100644 index 0000000..f617d20 --- /dev/null +++ b/src/plugins/mediaplayer/i18n/videoplayer.pot @@ -0,0 +1,61 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2011-10-09 19:46+0100\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: videoplayer.py:39 +msgid "Video Player" +msgstr "" + +#: videoplayer.py:40 +msgid "" +"Plays videos! Very much experimental, this plugin uses mplayer to generate " +"JPEG images which are then loaded and displayed on the LCD. This means it is " +"very CPU AND disk intensive and should only be used as a toy. " +msgstr "" + +#: videoplayer.py:45 +msgid "Copyright (C)2010 Brett Smith" +msgstr "" + +#: videoplayer.py:50 +msgid "Stop" +msgstr "" + +#: videoplayer.py:51 +msgid "Play" +msgstr "" + +#: videoplayer.py:52 +msgid "Open file" +msgstr "" + +#: videoplayer.py:53 +msgid "Toggle Mute" +msgstr "" + +#: videoplayer.py:54 +msgid "Change aspect" +msgstr "" + +#: videoplayer.py:211 +msgid "All files" +msgstr "" + +#: videoplayer.py:216 +msgid "Movies" +msgstr "" diff --git a/src/plugins/mediaplayer/mediaplayer.py b/src/plugins/mediaplayer/mediaplayer.py new file mode 100644 index 0000000..a63541e --- /dev/null +++ b/src/plugins/mediaplayer/mediaplayer.py @@ -0,0 +1,1077 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2012 Brett Smith +# +# 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 . + +import gnome15.g15locale as g15locale +_ = g15locale.get_translation("videoplayer", modfile = __file__).ugettext + +import gnome15.g15driver as g15driver +import gnome15.util.g15convert as g15convert +import gnome15.util.g15scheduler as g15scheduler +import gnome15.util.g15gconf as g15gconf +import gnome15.util.g15os as g15os +import gnome15.util.g15cairo as g15cairo +import gnome15.util.g15icontools as g15icontools +import gnome15.g15theme as g15theme +import gnome15.g15plugin as g15plugin +import gnome15.g15screen as g15screen +import gnome15.g15devices as g15devices +import gnome15.g15actions as g15actions +import gnome15.lcdsink as lcdsink +import gnome15.g15globals as g15globals +import gtk +import os +import gst +import cairo +import array +import gobject +import gio +import mimetypes +import dbus + +from threading import Lock + +import logging +logger = logging.getLogger(__name__) + +# Detect whether we will be able to grab multimedia keys +session_bus = dbus.SessionBus() +can_grab_media_keys = False +try: + dbus.Interface(session_bus.get_object('org.g.SettingsDaemon', + '/org/gnome/SettingsDaemon'), 'org.gnome.SettingsDaemon') + can_grab_media_keys = True +except dbus.DBusException as e: + logger.debug("Error when trying to check if media keys could be grabbed. Trying alternative.", + exc_info = e) + try: + dbus.Interface(session_bus.get_object('org.gnome.SettingsDaemon', + '/org/gnome/SettingsDaemon/MediaKeys'), + 'org.gnome.SettingsDaemon.MediaKeys') + can_grab_media_keys = True + except dbus.DBusException as e: + logger.debug("Error when trying to check if media keys could be grabbed.", exc_info = e) + pass + +# Register the custom actions + +NEXT_TRACK = "mediaplayer-next-track" +PREV_TRACK = "mediaplayer-previous-track" +PLAY_TRACK = "mediaplayer-play-track" +STOP_TRACK = "mediaplayer-stop-track" + +# Register the action with all supported models +g15devices.g15_action_keys[NEXT_TRACK] = g15actions.ActionBinding(NEXT_TRACK, [ g15driver.G_KEY_NEXT ], g15driver.KEY_STATE_UP) +g15devices.z10_action_keys[NEXT_TRACK] = g15actions.ActionBinding(NEXT_TRACK, [ g15driver.G_KEY_NEXT ], g15driver.KEY_STATE_UP) +g15devices.g19_action_keys[NEXT_TRACK] = g15actions.ActionBinding(NEXT_TRACK, [ g15driver.G_KEY_NEXT ], g15driver.KEY_STATE_UP) +g15devices.g15_action_keys[PREV_TRACK] = g15actions.ActionBinding(PREV_TRACK, [ g15driver.G_KEY_PREV ], g15driver.KEY_STATE_UP) +g15devices.z10_action_keys[PREV_TRACK] = g15actions.ActionBinding(PREV_TRACK, [ g15driver.G_KEY_PREV ], g15driver.KEY_STATE_UP) +g15devices.g19_action_keys[PREV_TRACK] = g15actions.ActionBinding(PREV_TRACK, [ g15driver.G_KEY_PREV ], g15driver.KEY_STATE_UP) +g15devices.g15_action_keys[STOP_TRACK] = g15actions.ActionBinding(STOP_TRACK, [ g15driver.G_KEY_STOP ], g15driver.KEY_STATE_UP) +g15devices.z10_action_keys[STOP_TRACK] = g15actions.ActionBinding(STOP_TRACK, [ g15driver.G_KEY_STOP ], g15driver.KEY_STATE_UP) +g15devices.g19_action_keys[STOP_TRACK] = g15actions.ActionBinding(STOP_TRACK, [ g15driver.G_KEY_STOP ], g15driver.KEY_STATE_UP) +g15devices.g15_action_keys[PLAY_TRACK] = g15actions.ActionBinding(PLAY_TRACK, [ g15driver.G_KEY_PLAY ], g15driver.KEY_STATE_UP) +g15devices.z10_action_keys[PLAY_TRACK] = g15actions.ActionBinding(PLAY_TRACK, [ g15driver.G_KEY_PLAY ], g15driver.KEY_STATE_UP) +g15devices.g19_action_keys[PLAY_TRACK] = g15actions.ActionBinding(PLAY_TRACK, [ g15driver.G_KEY_PLAY ], g15driver.KEY_STATE_UP) + + +# Plugin details - All of these must be provided +id = "mediaplayer" +name = _("Media Player") +description = _("GStreamer based media player and webcam viewer.\n\ +Supports audio and video from either DVDs, files, webcams or\n\ +pulse sources. The visualisation is displayed on the LCD for audio\n\ +sources.") +author = "Brett Smith " +copyright = _("Copyright (C)2010 Brett Smith") +site = "http://localhost" +has_preferences = False +unsupported_models = [ g15driver.MODEL_G930, g15driver.MODEL_G35, g15driver.MODEL_G110, g15driver.MODEL_G11, g15driver.MODEL_G11, g15driver.MODEL_MX5500 ] + +if can_grab_media_keys: + actions={ + PREV_TRACK : _("Skip Backward"), + NEXT_TRACK : _("Skip Forward"), + PLAY_TRACK : _("Play/Pause"), + STOP_TRACK : _("Stop"), + g15driver.VIEW : _("Change aspect") + } +else: + actions={ + g15driver.PREVIOUS_SELECTION : _("Skip Backward"), + g15driver.NEXT_SELECTION : _("Skip Forward"), + g15driver.SELECT : _("Play/Pause"), + g15driver.CLEAR : _("Stop"), + g15driver.VIEW : _("Change aspect") + } + actions_g19={ + g15driver.PREVIOUS_PAGE : _("Skip Backward"), + g15driver.NEXT_PAGE : _("Skip Forward"), + g15driver.SELECT : _("Play/Pause"), + g15driver.CLEAR : _("Stop"), + g15driver.VIEW : _("Change aspect") + } + + +icon_path = g15icontools.get_icon_path(["media-video", "emblem-video", "emblem-videos", "video", "video-player", "applications-multimedia" ]) + +def create(gconf_key, gconf_client, screen): + return G15MediaPlayer(gconf_client, gconf_key, screen) + +def get_visualisation(plugin): + """ + Get the currently configured visualisation. + + Keyword arguments: + plugin -- plugin instance + """ + return g15gconf.get_string_or_default( + plugin.gconf_client, + "%s/visualisation" % plugin.gconf_key, "goom") + +class PulseSourceMenuItem(g15theme.MenuItem): + """ + Menu item to activate a single pulse source. + """ + def __init__(self, device_name, device_description, plugin): + g15theme.MenuItem.__init__(self, "pulse-%s" % device_name, False, device_description) + self._plugin = plugin + self._device_name = device_name + + def get_theme_properties(self): + item_properties = g15theme.MenuItem.get_theme_properties(self) + item_properties["item_name"] = self.name + return item_properties + + def activate(self): + self._plugin._open_source(G15PulseSource(self._device_name, + get_visualisation(self._plugin))) + return True + +class MountMenuItem(g15theme.MenuItem): + """ + Menu item to activate a single mount (DVD etc) + """ + def __init__(self, id, mount, plugin): + g15theme.MenuItem.__init__(self, id, False, mount.get_name()) + self._mount = mount + self._plugin = plugin + + def get_theme_properties(self): + item_properties = g15theme.MenuItem.get_theme_properties(self) + item_properties["item_name"] = self._mount.get_name() + icon = self._mount.get_icon() + icon_names = [ icon.get_file().get_path() ] if isinstance(icon, gio.FileIcon) else icon.get_names() + icon_names += "gnome-dev-harddisk" + item_properties["item_icon"] = g15icontools.get_icon_path(icon_names) + return item_properties + + def activate(self): + self._plugin._open_source(G15RemovableSource(self._mount)) + return True + +class G15VideoDeviceMenuItem(g15theme.MenuItem): + """ + Menu item to activate a single V4L2 source (i.e. Webcam etc) + """ + + def __init__(self, plugin, video_device_name): + g15theme.MenuItem.__init__(self, video_device_name, False, video_device_name) + self.plugin = plugin + + def activate(self): + self.plugin._open_source(G15WebCamSource(self.id)) + +class G15VisualisationMenuItem(g15theme.MenuItem): + """ + Menu item to make a single visualisation the current default one + """ + + def __init__(self, name, plugin): + g15theme.MenuItem.__init__(self, "visualisation-%s" % name, False, name) + self._plugin = plugin + self.radio = True + + def get_theme_properties(self): + p = g15theme.MenuItem.get_theme_properties(self) + p["item_radio"] = True + p["item_radio_selected"] = get_visualisation(self._plugin) == self.name + return p + + def activate(self): + self._plugin.gconf_client.set_string("%s/visualisation" % self._plugin.gconf_key, self.name) + self._plugin.page.mark_dirty() + self._plugin.page.redraw() + +class G15VideoPainter(g15screen.Painter): + """ + Painter used to paint video or visualisation on the background of other + pages. + """ + + def __init__(self, video_page): + g15screen.Painter.__init__(self, g15screen.BACKGROUND_PAINTER, -2500) + self._video_page = video_page + + def paint(self, canvas): + if not self._video_page.is_visible(): + canvas.save() + self._video_page._paint_video_image(canvas) + canvas.restore() + +class G15MediaPlayerPage(g15theme.G15Page): + """ + The page used to display video or visualisation + """ + + def __init__(self, screen, source, plugin): + g15theme.G15Page.__init__(self, "videopage-%s" % source.name, screen, \ + priority = g15screen.PRI_NORMAL, \ + title = source.name, \ + theme = g15theme.G15Theme(self, variant = 'mediakeys' if plugin._grabbed_keys else None), thumbnail_painter = self._paint_thumbnail, originating_plugin = plugin) + self._sidebar_offset = 0 + self._source = source + self._muted = False + self._lock = Lock() + self._plugin = plugin + self._surface = None + self._hide_timer = None + self._screen = screen + self._full_screen = self._screen.driver.get_size() + self._aspect = self._full_screen + self._active = True + self._frame_index = 1 + self._last_seconds = -1 + self._thumb_icon = g15cairo.load_surface_from_file(icon_path) + self._setup_gstreamer() + self.screen.key_handler.action_listeners.append(self) + def on_delete(): + self._pipeline.set_state(gst.STATE_NULL) + self.screen.key_handler.action_listeners.remove(self) + self.screen.painters.remove(self.background_painter) + self._plugin.show_menu() + self._plugin._release_multimedia_keys() + self.on_deleted = on_delete + self.background_painter = G15VideoPainter(self) + self.screen.painters.append(self.background_painter) + self._plugin.hide_menu() + + def _setup_gstreamer(self): + # Create the video source + logger.info("Creating audio/visual source") + self._video_src = self._source.create_source() + + # Create our custom sink that is connected to the LCD + logger.info("Creating videosink that is connected to the LCD") + self._video_sink = lcdsink.CairoSurfaceThumbnailSink() + logger.info("Connecting to video sink") + self._video_sink.connect('thumbnail', self._redraw_cb) + + # Now create the actual pipeline + self._pipeline = gst.Pipeline("mypipeline") + logger.info("Building pipeline") + self._source.build_pipeline(self._video_src, self._video_sink, self._pipeline) + logger.info("Built pipeline") + self._connect_signals() + + def action_performed(self, binding): + # The custom actions which can be activated outside of visible page + if binding.action == PLAY_TRACK: + gobject.idle_add(self._play) + return True + elif binding.action == NEXT_TRACK: + gobject.idle_add(self._fwd) + return True + elif binding.action == PREV_TRACK: + gobject.idle_add(self._rew) + return True + elif binding.action == STOP_TRACK: + gobject.idle_add(self._stop) + return True + + if self.is_visible(): + if can_grab_media_keys: + # Default when media keys are available + if binding.action == g15driver.VIEW: + gobject.idle_add(self._change_aspect) + return True + else: + # Default when media keys are not available + if binding.action == g15driver.SELECT: + gobject.idle_add(self._play) + elif ( binding.action == g15driver.PREVIOUS_PAGE and self._screen.device.model_id == g15driver.MODEL_G19 ) or \ + ( binding.action == g15driver.PREVIOUS_SELECTION and self._screen.device.model_id != g15driver.MODEL_G19 ): + gobject.idle_add(self._rew) + elif ( binding.action == g15driver.NEXT_PAGE and self._screen.device.model_id == g15driver.MODEL_G19 ) or \ + ( binding.action == g15driver.NEXT_SELECTION and self._screen.device.model_id != g15driver.MODEL_G19 ): + gobject.idle_add(self._fwd) + elif binding.action == g15driver.VIEW: + gobject.idle_add(self._change_aspect) + elif binding.action == g15driver.CLEAR: + gobject.idle_add(self._stop) + else: + return False + return True + + + def get_theme_properties(self): + properties = {} + properties["aspect"] = "%d:%d" % self._aspect + try: + progress_pc, progress, duration = self._get_track_progress() + except Exception as e: + logger.debug("Could not read track progress. Setting values to 0", exc_info = e) + progress_pc, progress, duration = 0,(0,0,0),(0,0,0) + + if self._last_seconds != progress[2]: + self.mark_dirty() + self._last_seconds = progress[2] + + if self._plugin._mm_key is not None: + properties["key_%s" % self._plugin._mm_key] = True + + properties["track_progress_pc"] = str(progress_pc) + properties["track_progress"] = "%02d:%02d.%02d" % progress + properties["track_duration"] = "%02d:%02d.%02d" % duration + properties["track_name"] = "%s" % self._source.name + + properties["play_pause"] = _("Pause") if self._is_playing() else _("Play") + return properties + + def paint_theme(self, canvas, properties, attributes): + g15theme.G15Page.paint_theme(self, canvas, properties, attributes) + + def paint(self, canvas): + self._paint_video_image(canvas) + canvas.save() + if self._sidebar_offset < 0 and self._sidebar_offset > -(self.theme.bounds[2]): + self._sidebar_offset -= 5 + canvas.translate(self._sidebar_offset, 0) + g15theme.G15Page.paint(self, canvas) + canvas.restore() + if self._sidebar_offset < 0 and self._sidebar_offset > -(self.theme.bounds[2]): + g15scheduler.schedule("RepaintVideoOverly", 0.1, self.redraw) + + """ + GStreamer callbacks + """ + + def _on_sync_message(self, bus, message): + if message.structure is None: + return + message_name = message.structure.get_name() + logger.debug("Sync. %s", message) + + def _on_message(self, bus, message): + """ + Handle changes in the playing state. + """ + t = message.type + logger.debug("Message. %s", message) + if t == gst.MESSAGE_EOS: + self._pipeline.set_state(gst.STATE_NULL) + self._show_sidebar() + elif t == gst.MESSAGE_ERROR: + err, debug = message.parse_error() + self._pipeline.set_state(gst.STATE_NULL) + self._show_sidebar() + + def _redraw_cb(self, unused_thsink, timestamp): + if not self._plugin.active: + return + buf = self._video_sink.data + width = self._video_sink.width + height = self._video_sink.height + b = array.array("b") + b.fromstring(buf) + self._surface = cairo.ImageSurface.create_for_data(b, + # We don't use FORMAT_ARGB32 because Cairo uses premultiplied + # alpha, and gstreamer does not. Discarding the alpha channel + # is not ideal, but the alternative would be to compute the + # conversion in python (slow!). + cairo.FORMAT_RGB24, + width, + height, + width * 4) + + if self.is_visible(): + self.redraw() + else: + self.get_screen().redraw(redraw_content = False, queue = False) + + + ''' + Private + ''' + def _get_track_progress(self): + raw_pos = self._pipeline.query_position(gst.FORMAT_TIME, None)[0] + raw_dur = self._pipeline.query_duration(gst.FORMAT_TIME, None)[0] + pos = self._convert_time(int(raw_pos)) + if raw_dur < 0: + return 100, pos, (0,0,0) + dur = self._convert_time(int(raw_dur)) + pc = float(raw_pos) / float(raw_dur) + return int(pc * 100), pos, dur + + def _connect_signals(self): + # Watch signals coming from the bus + logger.info("Connecting signals") + bus = self._pipeline.get_bus() + bus.add_signal_watch() + bus.enable_sync_message_emission() + bus.connect("message", self._on_message) + bus.connect("sync-message::element", self._on_sync_message) + self._source.connect_signals() + logger.info("Connected signals") + + def _convert_time(self, time): + time = time / 1000000000 + mins = time % 3600 + time = time - mins + secs = mins % 60 + mins = mins - secs + hours = int(time / 3600) + mins = int(mins / 60) + secs = int(secs) + return hours,mins,secs + + def _paint_video_image(self, canvas): + size = self._screen.driver.get_size() + if self._surface != None: + target_size = ( float(size[0]), float(size[0]) * (float(self._aspect[1]) ) / float(self._aspect[0]) ) + sx = float(target_size[0]) / float(self._surface.get_width()) + sy = float(target_size[1]) / float(self._surface.get_height()) + canvas.save() + canvas.translate((size[0] - target_size[0]) / 2.0,(size[1] - target_size[1]) / 2.0) + canvas.scale(sx, sy) + canvas.set_source_surface(self._surface) + canvas.paint() + canvas.restore() + + def _hide_sidebar(self, after = 0.0): + if after == 0.0: + self._sidebar_offset = -1 + self._hide_timer = None + self.redraw() + else: + self._sidebar_offset = 0 + self._cancel_hide() + self._hide_timer = g15scheduler.schedule("HideSidebar", after, self._hide_sidebar) + + def _cancel_hide(self): + if self._hide_timer != None: + self._hide_timer.cancel() + self._hide_timer = None + + def _change_aspect(self): + if self._aspect == (16, 9): + self._aspect = (4, 3) + elif self._aspect == (4, 3): + # Just take up the most room + self._aspect = (24, 9) + elif self._aspect == (24, 9): + self._aspect = self._full_screen + else: + self._aspect = (16, 9) + if self._sidebar_offset != 0: + self._show_sidebar() + self._hide_sidebar(3.0) + + def _rew(self): + pos_int = self._pipeline.query_position(gst.FORMAT_TIME, None)[0] + seek_ns = pos_int - (10 * 1000000000) + self._pipeline.seek_simple(gst.FORMAT_TIME, gst.SEEK_FLAG_FLUSH, seek_ns) + if self._sidebar_offset != 0: + self._show_sidebar() + self._hide_sidebar(3.0) + + def _fwd(self): + pos_int = self._pipeline.query_position(gst.FORMAT_TIME, None)[0] + seek_ns = pos_int + (10 * 1000000000) + self._pipeline.seek_simple(gst.FORMAT_TIME, gst.SEEK_FLAG_FLUSH, seek_ns) + if self._sidebar_offset != 0: + self._show_sidebar() + self._hide_sidebar(3.0) + + def _is_paused(self): + return gst.STATE_PAUSED == self._pipeline.get_state()[1] + + def _is_playing(self): + return gst.STATE_PLAYING == self._pipeline.get_state()[1] + + def _play(self): + self._lock.acquire() + try: + if self._is_playing(): + self._pipeline.set_state(gst.STATE_PAUSED) + self._cancel_hide() + self._show_sidebar() + else: + self._pipeline.set_state(gst.STATE_PLAYING) + self._hide_sidebar(3.0) + finally: + self._lock.release() + + def _show_sidebar(self): + self._sidebar_offset = 0 + self.redraw() + + def _stop(self): + self._lock.acquire() + try: + self._pipeline.set_state(gst.STATE_READY) + self.delete() + self.screen.raise_page(self._plugin.page) + finally: + self._lock.release() + + def _paint_thumbnail(self, canvas, allocated_size, horizontal): + if self._surface != None and self._screen.driver.get_bpp() == 16: + return g15cairo.paint_thumbnail_image(allocated_size, self._surface, canvas) + + +class G15MediaSource(): + """ + Superclass of all media sources + """ + + def __init__(self, name): + self.name = name + + def create_source(self): + raise Exception("Not implemented") + + def build_pipeline(self, video_src, video_sink, pipeline): + raise Exception("Not implemented") + + def connect_signals(self): + pass + +class G15VideoFileSource(G15MediaSource): + + """ + Media source for playing an audio visual movie. + """ + + def __init__(self, name, path): + G15MediaSource.__init__(self, name) + self._path = path + + def create_source(self): + src = gst.element_factory_make("filesrc", "video-source") + src.set_property("location", self._path) + return src + + def build_pipeline(self, video_src, video_sink, pipeline): + + # Create the pipeline elements + self._decodebin = gst.element_factory_make("decodebin2") + self._autoconvert = gst.element_factory_make("autoconvert") + + # As a precaution add videio capability filter + # in the video processing pipeline. + videocap = gst.Caps("video/x-raw-yuv") + + self._filter = gst.element_factory_make("capsfilter") + self._filter.set_property("caps", videocap) + + # Converts the video from one colorspace to another + self._color_space = gst.element_factory_make("ffmpegcolorspace") + + self._audioconvert = gst.element_factory_make("audioconvert") + self._audiosink = gst.element_factory_make("autoaudiosink") + + # Queues + self._queue1 = gst.element_factory_make("queue") + self._queue2 = gst.element_factory_make("queue") + + pipeline.add(video_src, + self._decodebin, + self._autoconvert, + self._audioconvert, + self._queue1, + self._queue2, + self._filter, + self._color_space, + self._audiosink, + video_sink) + + # Link everything we can link now + gst.element_link_many(video_src, self._decodebin) + gst.element_link_many(self._queue1, self._autoconvert, + self._filter, self._color_space, + video_sink) + gst.element_link_many(self._queue2, self._audioconvert, + self._audiosink) + + def connect_signals(self): + if not self._decodebin is None: + self._decodebin.connect("pad_added", self._decodebin_pad_added) + + def _decodebin_pad_added(self, decodebin, pad): + compatible_pad = None + caps = pad.get_caps() + name = caps[0].get_name() + if name[:5] == 'video': + compatible_pad = self._queue1.get_compatible_pad(pad, caps) + elif name[:5] == 'audio': + compatible_pad = self._queue2.get_compatible_pad(pad, caps) + + if compatible_pad: + pad.link(compatible_pad) + + +class G15AudioFileSource(G15MediaSource): + + """ + Media source for playing an audio file. Video is provided by the + currently configured visualisation + """ + + def __init__(self, name, path, visualisation): + G15MediaSource.__init__(self, name) + self._path = path + self._visualisation = visualisation + + def create_source(self): + src = gst.element_factory_make("filesrc", "video-source") + src.set_property("location", self._path) + return src + + def build_pipeline(self, video_src, video_sink, pipeline): + self._decodebin = gst.element_factory_make("decodebin2") + self._visualiser = gst.element_factory_make(self._visualisation) + self._color_space = gst.element_factory_make("ffmpegcolorspace") + self._audioconvert = gst.element_factory_make("audioconvert") + self._audiosink = gst.element_factory_make("autoaudiosink") + self._tee = gst.element_factory_make('tee', "tee") + self._queue1 = gst.element_factory_make("queue") + self._queue2 = gst.element_factory_make("queue") + pipeline.add(video_src, + self._decodebin, + self._audioconvert, + self._tee, + self._queue1, + self._audiosink, + self._queue2, + self._visualiser, + self._color_space, + video_sink) + gst.element_link_many(video_src, self._decodebin) + gst.element_link_many(self._audioconvert, self._tee) + self._tee.link(self._queue1) + self._queue1.link(self._audiosink) + self._tee.link(self._queue2) + gst.element_link_many(self._queue2, self._visualiser,self._color_space, video_sink) + + def connect_signals(self): + if not self._decodebin is None: + self._decodebin.connect("pad_added", self._decodebin_pad_added) + + def _decodebin_pad_added(self, decodebin, pad): + self._decodebin.link(self._audioconvert) + + +class G15PulseSource(G15MediaSource): + + """ + Media source for a pulse audio monitor. Audio is not directed, it is + just monitored to produce visualisation video + """ + + def __init__(self, name, visualisation): + G15MediaSource.__init__(self, name) + self._visualisation = visualisation + + def create_source(self): + src = gst.element_factory_make("pulsesrc", "video-source") + src.set_property("device", self.name) + return src + + def build_pipeline(self, video_src, video_sink, pipeline): + self._visualiser = gst.element_factory_make(self._visualisation) + self._color_space = gst.element_factory_make("ffmpegcolorspace") + self._audioconvert = gst.element_factory_make("audioconvert") + pipeline.add(video_src, + self._audioconvert, + self._visualiser, + self._color_space, + video_sink) + gst.element_link_many(video_src, self._audioconvert, self._visualiser,self._color_space, video_sink) + +class G15RemovableSource(G15VideoFileSource): + + """ + An audio / video source that reads from removable media such as DVD + """ + + def __init__(self, mount): + G15MediaSource.__init__(self, mount.get_name(), mount.get_root().get_path()) + self._mount = mount + + def create_source(self): + src = gst.element_factory_make("dvdreadsrc", "video-source") + return src + +class G15WebCamSource(G15MediaSource): + + """ + Video only source that reads from a V4L2 device such as a webcam + """ + + def __init__(self, name): + G15MediaSource.__init__(self, name) + + def create_source(self): + src = gst.element_factory_make("v4l2src", "video-source") + device_path = "/dev/%s" % self.name + logger.info("Opening Video device %s", device_path) + src.set_property("device", device_path) + return src + + def build_pipeline(self, video_src, video_sink, pipeline): + # Create the pipeline elements + self._decodebin = gst.element_factory_make("decodebin2") + self._autoconvert = gst.element_factory_make("autoconvert") + + videocap = gst.Caps("video/x-raw-yuv") + self._filter = gst.element_factory_make("capsfilter") + self._filter.set_property("caps", videocap) + + # Converts the video from one colorspace to another + self._color_space = gst.element_factory_make("ffmpegcolorspace") + + self._queue1 = gst.element_factory_make("queue") + + pipeline.add(video_src, + self._decodebin, + self._autoconvert, + self._queue1, + self._filter, + self._color_space, + video_sink) + + # Link everything we can link now + gst.element_link_many(video_src, self._decodebin) + gst.element_link_many(self._queue1, self._autoconvert, + self._filter, self._color_space, + video_sink) + + def connect_signals(self): + if not self._decodebin is None: + self._decodebin.connect("pad_added", self._decodebin_pad_added) + + def _decodebin_pad_added(self, decodebin, pad): + compatible_pad = None + caps = pad.get_caps() + name = caps[0].get_name() + if name[:5] == 'video': + compatible_pad = self._queue1.get_compatible_pad(pad, caps) + + if compatible_pad: + pad.link(compatible_pad) + + +class G15MediaPlayer(g15plugin.G15MenuPlugin): + """ + The main Media Player plugin class which is presented as a menu of + video sources and options + """ + + def __init__(self, gconf_client, gconf_key, screen): + g15plugin.G15MenuPlugin.__init__(self, gconf_client, gconf_key, screen, icon_path, id, name) + self.player_pages = [] + self._grabbed_keys = None + self._settings = None + self._app_name = None + self._mm_key = None + self._mm_key_timer = None + + def activate(self): + g15plugin.G15MenuPlugin.activate(self) + + def load_menu_items(self): + items = [] + self.volume_monitor_signals = [] + + # Webcams etc + video_devices = [] + for i in os.listdir("/dev"): + if i.startswith("video"): + video_devices.append(i) + + if len(video_devices) > 0: + items.append(g15theme.MenuItem("video-devices", True, _("Video Devices"), icon = g15icontools.get_icon_path(["camera-web", "camera-video"]), activatable = False)) + for i in video_devices: + items.append(G15VideoDeviceMenuItem(self, i)) + + # Video File + def activate_video_file(): + gobject.idle_add(self._open_video_file) + items.append(g15theme.MenuItem("video-file", True, _("Open Audio/Video File"), activate = activate_video_file, icon = g15icontools.get_icon_path("folder"))) + + # DVD / Mounts + self.volume_monitor = gio.VolumeMonitor() + self.volume_monitor_signals.append(self.volume_monitor.connect("mount_added", self._on_mount_added)) + self.volume_monitor_signals.append(self.volume_monitor.connect("mount_removed", self._on_mount_removed)) + removable_media_items = [] + for i, mount in enumerate(self.volume_monitor.get_mounts()): + drive = mount.get_drive() + if not mount.is_shadowed() and drive is not None and drive.is_media_removable(): + removable_media_items.append(MountMenuItem('mount-%d' % i, mount, self)) + if len(removable_media_items): + items.append(g15theme.MenuItem("removable-devices", True, _("Removable Devices"), icon = g15icontools.get_icon_path(["driver-removable-media", "gnome-dev-removable"]), activatable = False)) + items += removable_media_items + + # Pulse + status, output = g15os.get_command_output("pacmd list-sources") + if status == 0 and len(output) > 0: + i = 0 + pulse_items = [] + for line in output.split("\n"): + line = line.strip() + if line.startswith("name: "): + name = line[7:-1] + elif line.startswith("device.description = "): + pulse_items.append(PulseSourceMenuItem(name, line[22:-1], self)) + if len(pulse_items) > 0: + items.append(g15theme.MenuItem("pulse-sources", True, _("PulseAudio Source"), icon = g15icontools.get_icon_path(["audio-card", "audio-speakers", "audio-volume-high", "audio-x-generic"]), activatable = False)) + items += pulse_items + + + # Visualisations - TODO - there must be a better way to list them + items.append(g15theme.MenuItem("visualisation-mode", True, _("Visualisation Mode"), icon = g15icontools.get_icon_path(["preferences-color", "gtk-select-color", "preferences-desktop-screensaver", "kscreensaver", "xscreensaver"]), activatable = False)) + for c in [ "goom", \ + "libvisual_bumpscope", \ + "libvisual_corona", \ + "libvisual_infinite", \ + "libvisual_jakdaw", \ + "libvisual_jess", \ + "libvisual_lv_analyzer", \ + "libvisual_lv_scope", \ + "libvisual_lv_oinksie", \ + "synaesthesia", \ + "spacescope", \ + "spectrascope", \ + "synaescope", \ + "wavescope", \ + "monoscope"]: + try: + gst.element_factory_make(c) + items.append(G15VisualisationMenuItem(c, self)) + except Exception as e: + logger.debug("Error creating visualizations", exc_info = e) + pass + + self.menu.set_children(items) + if len(items) > 0: + self.menu.selected = items[0] + else: + self.menu.selected = None + + def deactivate(self): + g15plugin.G15MenuPlugin.deactivate(self) + for p in self.player_pages: + p.delete() + for c in self.volume_monitor_signals: + self.volume_monitor.disconnect(c) + + ''' + Private + ''' + def _on_mount_added(self, monitor, mount, *args): + self.load_menu_items() + + def _on_mount_removed(self, monitor, mount, *args): + self.load_menu_items() + + def _open_video_file(self): + path = self._open_file() + if path: + mime_type, _ = mimetypes.guess_type(path) + if mime_type.startswith("audio"): + self._open_source(G15AudioFileSource(os.path.basename(path), path, get_visualisation(self))) + else: + self._open_source(G15VideoFileSource(os.path.basename(path), path)) + + def _grab_multimedia_keys(self): + try: + if self._grabbed_keys is not None: + raise Exception("Already grabbed") + self._app_name = "%s-%s" % ( g15globals.name, name) + def _on_key(app, key): + if app == self._app_name: + self._mm_key = None + if key == "Play": + self._mm_key = g15driver.G_KEY_PLAY + self._player_page._play() + elif key == "Stop": + self._mm_key = g15driver.G_KEY_STOP + self._player_page._stop() + elif key == "Next": + self._mm_key = g15driver.G_KEY_NEXT + self._player_page._fwd() + elif key == "Previous": + self._mm_key = g15driver.G_KEY_PREV + self._player_page._rew() + else: + logger.warning("Unsupported media key %s", key) + if self._mm_key_timer is not None: + self._mm_key_timer.cancel() + self._mm_key_timer = None + self._mm_key_timer = g15scheduler.schedule("CancelMMKey", 1.0, self._clear_mm_key) + + try: + self._settings = dbus.Interface(session_bus.get_object('org.g.SettingsDaemon', + '/org/gnome/SettingsDaemon'), 'org.gnome.SettingsDaemon') + self._settings.GrabMediaPlayerKeys(self._app_name, 0) + self._grabbed_keys = self._settings.connect_to_signal('MediaPlayerKeyPressed', _on_key) + except dbus.DBusException as e: + logger.debug("Error grabing multimedia keys. Trying alternative", exc_info = e) + self._settings = dbus.Interface(session_bus.get_object('org.gnome.SettingsDaemon', + '/org/gnome/SettingsDaemon/MediaKeys'), + 'org.gnome.SettingsDaemon.MediaKeys') + self._settings.GrabMediaPlayerKeys(self._app_name, 0) + self._grabbed_keys = self._settings.connect_to_signal('MediaPlayerKeyPressed', _on_key) + + logger.info("Grabbed multimedia keys") + except dbus.DBusException as error: + logger.warning("Could not grab multi-media keys.", exc_info = error) + + def _clear_mm_key(self): + self._mm_key = None + self.screen.redraw() + + def _release_multimedia_keys(self): + if self._grabbed_keys: + self._settings.ReleaseMediaPlayerKeys(self._app_name) + session_bus.remove_signal_receiver(self._grabbed_keys) + self._grabbed_keys = None + + def _open_source(self, source): + gobject.idle_add(self._do_open_source, source) + + def _do_open_source(self, source): + if can_grab_media_keys: + self._grab_multimedia_keys() + self._player_page = G15MediaPlayerPage(self.screen, source, self) + self.player_pages.append(self._player_page) + self.screen.add_page(self._player_page) + self.screen.redraw(self._player_page) + gobject.idle_add(self._player_page._play) + + def _reload_menu(self): + self.load_menu_items() + self.screen.redraw(self.page) + + def _open_file(self): + dialog = gtk.FileChooserDialog("Open..", + None, + gtk.FILE_CHOOSER_ACTION_OPEN, + (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, + gtk.STOCK_OPEN, gtk.RESPONSE_OK)) + dialog.set_default_response(gtk.RESPONSE_OK) + + filter = gtk.FileFilter() + filter.set_name(_("All files")) + filter.add_pattern("*") + dialog.add_filter(filter) + + # Video + filter = gtk.FileFilter() + filter.set_name(_("Video Files")) + + filter.add_mime_type("application/ogg") + + filter.add_mime_type("video/ogg") + filter.add_mime_type("video/mpeg") + filter.add_mime_type("video/quicktime") + filter.add_mime_type("video/x-la-asf") + filter.add_mime_type("video/x-ms-asf") + filter.add_mime_type("video/x-msvideo") + filter.add_mime_type("video/x-sgi-movie") + + filter.add_pattern("*.ogx") + filter.add_pattern("*.ogv") + filter.add_pattern("*.mp2") + filter.add_pattern("*.mpa") + filter.add_pattern("*.mpe") + filter.add_pattern("*.mpeg") + filter.add_pattern("*.mpg") + filter.add_pattern("*.mpv2") + filter.add_pattern("*.mov") + filter.add_pattern("*.qt") + filter.add_pattern("*.lsf") + filter.add_pattern("*.lsx") + filter.add_pattern("*.asf") + filter.add_pattern("*.asr") + filter.add_pattern("*.asx") + filter.add_pattern("*.avi") + filter.add_pattern("*.movie") + dialog.add_filter(filter) + + # Audio + filter = gtk.FileFilter() + filter.set_name(_("Audio Files")) + + filter.add_mime_type("audio/ogg") + filter.add_mime_type("audio/vorbis") + filter.add_mime_type("audio/flac") + filter.add_mime_type("audio/x-ogg") + filter.add_mime_type("audio/x-vorbis") + filter.add_mime_type("audio/x-flac") + filter.add_mime_type("audio/basic") + filter.add_mime_type("audio/mid") + filter.add_mime_type("audio/mpeg") + filter.add_mime_type("audio/aiff") + filter.add_mime_type("audio/x-aiff") + filter.add_mime_type("audio/x-mpegurl") + filter.add_mime_type("audio/x-pn-realaudio") + filter.add_mime_type("audio/x-realaudio") + filter.add_mime_type("audio/wav") + filter.add_mime_type("audio/x-wav") + filter.add_mime_type("audio/x-au") + filter.add_mime_type("audio/x-midi") + filter.add_mime_type("audio/x-mpeg") + filter.add_mime_type("audio/x-mpeg3") + filter.add_mime_type("audio/x-mpeg-3") + filter.add_mime_type("audio/midi") + filter.add_mime_type("audio/x-mid") + + filter.add_pattern("*.flac") + filter.add_pattern("*.oga") + filter.add_pattern("*.ogg") + filter.add_pattern("*.au") + filter.add_pattern("*.snd") + filter.add_pattern("*.mid") + filter.add_pattern("*.rmi") + filter.add_pattern("*.mp3") + filter.add_pattern("*.aif") + filter.add_pattern("*.aifc") + filter.add_pattern("*.aiff") + filter.add_pattern("*.m3u") + filter.add_pattern("*.ra") + filter.add_pattern("*.ram") + filter.add_pattern("*.wav") + dialog.add_filter(filter) + + response = dialog.run() + while gtk.events_pending(): + gtk.main_iteration(False) + try: + if response == gtk.RESPONSE_OK: + return dialog.get_filename() + finally: + dialog.destroy() diff --git a/src/plugins/mediaplayer/oldvideoplayer.py b/src/plugins/mediaplayer/oldvideoplayer.py new file mode 100644 index 0000000..0890094 --- /dev/null +++ b/src/plugins/mediaplayer/oldvideoplayer.py @@ -0,0 +1,334 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2010 Brett Smith +# +# 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 . + +import gnome15.g15locale as g15locale +_ = g15locale.get_translation("videoplayer", modfile = __file__).ugettext + +import gnome15.g15driver as g15driver +import gnome15.util.g15convert as g15convert +import gnome15.util.g15scheduler as g15scheduler +import gnome15.util.g15cairo as g15cairo +import gnome15.util.g15icontools as g15icontools +import gnome15.g15theme as g15theme +from threading import Timer +import gtk +import os +import select +import gobject +import tempfile +import subprocess +from threading import Lock +from threading import Thread + +# Plugin details - All of these must be provided +id = "videoplayer" +name = _("Video Player") +description = _("Plays videos! Very much experimental, this plugin uses \ +mplayer to generate JPEG images which are then loaded \ +and displayed on the LCD. This means it is very CPU AND \ +disk intensive and should only be used as a toy. ") +author = "Brett Smith " +copyright = _("Copyright (C)2010 Brett Smith") +site = "http://localhost" +has_preferences = False +unsupported_models = [ g15driver.MODEL_G110, g15driver.MODEL_G11, g15driver.MODEL_G11, g15driver.MODEL_MX5500, g15driver.MODEL_G930, g15driver.MODEL_G35 ] +actions={ + g15driver.PREVIOUS_SELECTION : _("Stop"), + g15driver.NEXT_SELECTION : _("Play"), + g15driver.SELECT : _("Open file"), + g15driver.CLEAR : _("Toggle Mute"), + g15driver.VIEW : _("Change aspect") + } + +''' +This simple plugin displays system statistics +''' + +def create(gconf_key, gconf_client, screen): + return G15VideoPlayer(gconf_key, gconf_client, screen) + + +class PlayThread(Thread): + + def __init__(self, page): + Thread.__init__(self) + self.name = "PlayThread" + self.setDaemon(True) + self._page = page + + self.temp_dir = tempfile.mkdtemp("g15", "tmp") + self._process = subprocess.Popen(['mplayer', '-slave', '-noconsolecontrols','-really-quiet', + '-vo', 'jpeg', self._page._movie_path], cwd=self.temp_dir, stdin=subprocess.PIPE, stdout=subprocess.PIPE) + self._page.redraw() + + def _playing(self): + return self._process.poll() == None + + def _stop(self): + try: + self._process.terminate() + except OSError: + # Got killed + pass + self._page.redraw() + + def _mute(self, mute): + if mute: + print self._command("mute", "1") + else: + print self._command("mute", "0") + + def _readlines(self): + ret = [] + while any(select.select([self._process.stdout.fileno()], [], [], 0.6)): + ret.append( self._process.stdout.readline() ) + return ret + + def _command(self, name, *args): + cmd = '%s%s%s\n'%(name, + ' ' if args else '', + ' '.join(repr(a) for a in args) + ) + self._process.stdin.write(cmd) + if name == 'quit': + return + return self.readlines() + + def set_aspect(self, aspect): + pass +# self.command("switch_ratio",str(float(aspect[0]) / float(aspect[1]))) + + def run(self): + self._process.wait() + +class G15VideoPage(g15theme.G15Page): + + def __init__(self, screen): + g15theme.G15Page.__init__(self, id, screen, title = name, theme = g15theme.G15Theme(self), thumbnail_painter = self._paint_thumbnail) + self._sidebar_offset = 0 + self._muted = False + self._lock = Lock() + self._surface = None + self._hide_timer = None + self._screen = screen + self._full_screen = self._screen.driver.get_size() + self._aspect = self._full_screen + self._playing = None + self._active = True + self._frame_index = 1 + self._frame_wait = 0.04 + self._thumb_icon = g15cairo.load_surface_from_file(g15icontools.get_icon_path(["media-video", "emblem-video", "emblem-videos", "video", "video-player" ])) + + def get_theme_properties(self): + properties = g15theme.G15Page.get_theme_properties(self) + properties["aspect"] = "%d:%d" % self._aspect + return properties + + def paint_theme(self, canvas, properties, attributes): + canvas.save() + + if self._sidebar_offset < 0 and self._sidebar_offset > -(self.theme.bounds[2]): + self._sidebar_offset -= 5 + + canvas.translate(self._sidebar_offset, 0) + g15theme.G15Page.paint_theme(self, canvas, properties, attributes) + canvas.restore() + + def paint(self, canvas): + g15theme.G15Page.paint(self, canvas) + wait = self._frame_wait + size = self._screen.driver.get_size() + + if self._playing != None: + + # Process may have been killed + if not self._playing._playing(): + self._stop() + + dir = sorted(os.listdir(self._playing.temp_dir), reverse=True) + if len(dir) > 1: + dir = dir[1:] + file = os.path.join(self._playing.temp_dir, dir[0]) + self._surface = g15cairo.load_surface_from_file(file) + for path in dir: + file = os.path.join(self._playing.temp_dir, path) + os.remove(file) + else: + wait = 0.1 + + if self._surface != None: + target_size = ( float(size[0]), float(size[0]) * (float(self._aspect[1]) ) / float(self._aspect[0]) ) + sx = float(target_size[0]) / float(self._surface.get_width()) + sy = float(target_size[1]) / float(self._surface.get_height()) + canvas.save() + canvas.translate((size[0] - target_size[0]) / 2.0,(size[1] - target_size[1]) / 2.0) + canvas.scale(sx, sy) + canvas.set_source_surface(self._surface) + canvas.paint() + canvas.restore() + + if self._playing != None: + timer = Timer(wait, self.redraw) + timer.name = "VideoRedrawTimer" + timer.setDaemon(True) + timer.start() + + ''' Functions specific to plugin + ''' + def _hide_sidebar(self, after = 0.0): + if after == 0.0: + self._sidebar_offset = -1 + self._hide_timer = None + else: + self._sidebar_offset = 0 + if self._hide_timer != None: + self._hide_timer.cancel() + self._hide_timer = g15scheduler.schedule("HideSidebar", after, self._hide_sidebar) + + def _open(self): + dialog = gtk.FileChooserDialog("Open..", + None, + gtk.FILE_CHOOSER_ACTION_OPEN, + (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, + gtk.STOCK_OPEN, gtk.RESPONSE_OK)) + dialog.set_default_response(gtk.RESPONSE_OK) + + filter = gtk.FileFilter() + filter.set_name(_("All files")) + filter.add_pattern("*") + dialog.add_filter(filter) + + filter = gtk.FileFilter() + filter.set_name(_("Movies")) + filter.add_mime_type("video/mpeg") + filter.add_mime_type("video/quicktime") + filter.add_mime_type("video/x-la-asf") + filter.add_mime_type("video/x-ms-asf") + filter.add_mime_type("video/x-msvideo") + filter.add_mime_type("video/x-sgi-movie") + filter.add_pattern("*.mp2") + filter.add_pattern("*.mpa") + filter.add_pattern("*.mpe") + filter.add_pattern("*.mpeg") + filter.add_pattern("*.mpg") + filter.add_pattern("*.mpv2") + filter.add_pattern("*.mov") + filter.add_pattern("*.qt") + filter.add_pattern("*.lsf") + filter.add_pattern("*.lsx") + filter.add_pattern("*.asf") + filter.add_pattern("*.asr") + filter.add_pattern("*.asx") + filter.add_pattern("*.avi") + filter.add_pattern("*.movie") + dialog.add_filter(filter) + + response = dialog.run() + while gtk.events_pending(): + gtk.main_iteration(False) + if response == gtk.RESPONSE_OK: + print dialog.get_filename(), 'selected' + self._movie_path = dialog.get_filename() + if self._playing: + self._stop() + self._play() + dialog.destroy() + return False + + def _change_aspect(self): + if self._aspect == (16, 9): + self._aspect = (4, 3) + elif self._aspect == (4, 3): + # Just take up the most room + self._aspect = (24, 9) + elif self._aspect == (24, 9): + self._aspect = self._full_screen + else: + self._aspect = (16, 9) + self._screen.redraw(self._page) + + def _play(self): + self._lock.acquire() + try: + self._hide_sidebar(3.0) + self._playing = PlayThread(self) + self._playing.set_aspect(self._aspect) + self._playing.mute(self.muted) + self._playing.start() + finally: + self._lock.release() + + def _stop(self): + self._lock.acquire() + try: + if self._hide_timer != None: + self._hide_timer.cancel() + self._sidebar_offset = 0 + self._playing._stop() + self._playing = None + finally: + self._lock.release() + + def _paint_thumbnail(self, canvas, allocated_size, horizontal): + if self._thumb_icon != None and self._screen.driver.get_bpp() == 16: + return g15cairo.paint_thumbnail_image(allocated_size, self._thumb_icon, canvas) + + +class G15VideoPlayer(): + + + def __init__(self, gconf_key, gconf_client, screen): + self._screen = screen + self._gconf_client = gconf_client + self._gconf_key = gconf_key + + def activate(self): + self._page = G15VideoPage(self._screen) + self._screen.add_page(self._page) + self._screen.redraw(self._page) + self._screen.key_handler.action_listeners.append(self) + + def deactivate(self): + if self._page._playing != None: + self._page._stop() + self._screen.del_page(self._page) + self._screen.key_handler.action_listeners.remove(self) + + def destroy(self): + pass + + def action_performed(self, binding): + if self._page is not None and self._page.is_visible(): + if binding.action == g15driver.SELECT: + gobject.idle_add(self._page._open) + elif binding.action == g15driver.NEXT_SELECTION: + if self._page._playing == None: + self._page._play() + elif binding.action == g15driver.PREVIOUS_SELECTION: + if self._page._playing != None: + self._page._stop() + elif binding.action == g15driver.VIEW: + if self._page._playing != None: + self._page._hide_sidebar(3.0) + self._change_aspect() + elif binding.action == g15driver.CLEAR: + self._page.muted = not self._page.muted + if self._page._playing != None: + self._page._hide_sidebar(3.0) + self._playing.mute(self._page.muted) + else: + return False + return True diff --git a/src/plugins/menu/Makefile.am b/src/plugins/menu/Makefile.am new file mode 100644 index 0000000..6617517 --- /dev/null +++ b/src/plugins/menu/Makefile.am @@ -0,0 +1,5 @@ +plugindir = $(datadir)/gnome15/plugins/menu +plugin_DATA = menu.py + +EXTRA_DIST = \ + $(plugin_DATA) \ No newline at end of file diff --git a/src/plugins/menu/i18n/menu.en_GB.po b/src/plugins/menu/i18n/menu.en_GB.po new file mode 100644 index 0000000..0b02f9b --- /dev/null +++ b/src/plugins/menu/i18n/menu.en_GB.po @@ -0,0 +1,56 @@ +# English translations for gnome15-plugins package. +# Copyright (C) 2012 THE gnome15-plugins'S COPYRIGHT HOLDER +# This file is distributed under the same license as the gnome15-plugins package. +# Automatically generated, 2012. +# +msgid "" +msgstr "" +"Project-Id-Version: gnome15-plugins 0.8.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: 2012-01-08 11:23+0000\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: en_GB\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=ASCII\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: menu.py:39 +msgid "Menu" +msgstr "Menu" + +#: menu.py:40 +msgid "" +"Allows selections of any currently active screen through a menu on the LCD." +msgstr "" +"Allows selections of any currently active screen through a menu on the LCD." + +#: menu.py:42 +msgid "Copyright (C)2010 Brett Smith" +msgstr "Copyright (C)2010 Brett Smith" + +#: menu.py:48 +msgid "Previous item" +msgstr "Previous item" + +#: menu.py:49 +msgid "Next item" +msgstr "Next item" + +#: menu.py:50 +msgid "Next page" +msgstr "Next page" + +#: menu.py:51 +msgid "Previous page" +msgstr "Previous page" + +#: menu.py:52 +msgid "Show selected item" +msgstr "Show selected item" + +#: menu.py:53 +msgid "Show menu" +msgstr "Show menu" diff --git a/src/plugins/menu/i18n/menu.pot b/src/plugins/menu/i18n/menu.pot new file mode 100644 index 0000000..db009a0 --- /dev/null +++ b/src/plugins/menu/i18n/menu.pot @@ -0,0 +1,55 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: menu.py:39 +msgid "Menu" +msgstr "" + +#: menu.py:40 +msgid "" +"Allows selections of any currently active screen through a menu on the LCD." +msgstr "" + +#: menu.py:42 +msgid "Copyright (C)2010 Brett Smith" +msgstr "" + +#: menu.py:48 +msgid "Previous item" +msgstr "" + +#: menu.py:49 +msgid "Next item" +msgstr "" + +#: menu.py:50 +msgid "Next page" +msgstr "" + +#: menu.py:51 +msgid "Previous page" +msgstr "" + +#: menu.py:52 +msgid "Show selected item" +msgstr "" + +#: menu.py:53 +msgid "Show menu" +msgstr "" diff --git a/src/plugins/menu/menu.py b/src/plugins/menu/menu.py new file mode 100644 index 0000000..7ff0f1b --- /dev/null +++ b/src/plugins/menu/menu.py @@ -0,0 +1,185 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2010 Brett Smith +# +# 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 . + +import gnome15.g15locale as g15locale +_ = g15locale.get_translation("menu", modfile = __file__).ugettext + +import gnome15.g15theme as g15theme +import gnome15.g15driver as g15driver +import gnome15.g15screen as g15screen +import gnome15.g15plugin as g15plugin +from gnome15.util.g15pythonlang import find +import sys +import cairo +import base64 +from cStringIO import StringIO +import logging +logger = logging.getLogger(__name__) + +# Plugin details - All of these must be provided +id="menu" +name=_("Menu") +description=_("Allows selections of any currently active screen through a menu on the LCD.") +author="Brett Smith " +copyright=_("Copyright (C)2010 Brett Smith") +site="http://www.russo79.com/gnome15" +has_preferences=False +default_enabled=True +unsupported_models = [ g15driver.MODEL_G110, g15driver.MODEL_G11, g15driver.MODEL_G930, g15driver.MODEL_G35 ] +actions={ + g15driver.PREVIOUS_SELECTION : _("Previous item"), + g15driver.NEXT_SELECTION : _("Next item"), + g15driver.NEXT_PAGE : _("Next page"), + g15driver.PREVIOUS_PAGE : _("Previous page"), + g15driver.SELECT : _("Show selected item"), + g15driver.MENU : _("Show menu") + } + + +def create(gconf_key, gconf_client, screen): + return G15Menu(gconf_client, gconf_key, screen) + +class MenuItem(g15theme.MenuItem): + + + def __init__(self, item_page, plugin, id): + g15theme.MenuItem.__init__(self, id) + self._item_page = item_page + self.thumbnail = None + self.plugin = plugin + + def get_page(self): + return self._item_page + + def activate(self): + self.plugin.hide_menu() + self.plugin.screen.raise_page(self._item_page) + self.plugin.screen.resched_cycle() + + def get_theme_properties(self): + item_properties = g15theme.MenuItem.get_theme_properties(self) + item_properties["item_name"] = self._item_page.title + item_properties["item_alt"] = "" + item_properties["item_type"] = "" + item_properties["item_icon"] = self.thumbnail + return item_properties + +class G15Menu(g15plugin.G15MenuPlugin): + + def __init__(self, gconf_client, gconf_key, screen): + g15plugin.G15MenuPlugin.__init__(self, gconf_client, gconf_key, screen, [ "gnome-main-menu", "gnome-window-manager", "gnome-gmenu", "kmenuedit" ], id, name) + self._show_on_activate = False + + def activate(self): + self.delete_timer = None + g15plugin.G15MenuPlugin.activate(self) + self.reload_theme() + self.listener = MenuScreenChangeListener(self) + self.screen.add_screen_change_listener(self.listener) + self.screen.key_handler.action_listeners.append(self) + + def deactivate(self): + g15plugin.G15MenuPlugin.deactivate(self) + self.screen.key_handler.action_listeners.remove(self) + self.screen.remove_screen_change_listener(self.listener) + + def action_performed(self, binding): + if self.page is not None: + self._reset_delete_timer() + if binding.action == g15driver.MENU: + if self.page is not None: + self.hide_menu() + return True + else: + self.show_menu() + self.page.set_priority(g15screen.PRI_HIGH) + return True + + def hide_menu(self): + g15plugin.G15MenuPlugin.hide_menu(self) + + def show_menu(self): + visible_page = self.screen.get_visible_page() + g15plugin.G15MenuPlugin.show_menu(self) + self._reset_delete_timer() + if visible_page: + item = find(lambda m: m._item_page == visible_page, self.menu.get_children()) + if item: + self.menu.set_selected_item(item) + + def load_menu_items(self): + items = [] + for page in self.screen.pages: + if page != self.page and page.priority > g15screen.PRI_INVISIBLE: + items.append(MenuItem(page, self, "menuitem-%s" % page.id )) + items = sorted(items, key=lambda item: item._item_page.title) + self.menu.set_children(items) + if len(items) > 0: + self.menu.selected = items[0] + else: + self.menu.selected = None + for item in items: + self._load_item_icon(item) + + ''' + Private + ''' + def _load_item_icon(self, item): + if item._item_page.thumbnail_painter != None: + img = cairo.ImageSurface(cairo.FORMAT_ARGB32, self.screen.height, self.screen.height) + thumb_canvas = cairo.Context(img) + try : + if item._item_page.thumbnail_painter(thumb_canvas, self.screen.height, True): + img_data = StringIO() + img.write_to_png(img_data) + item.thumbnail = base64.b64encode(img_data.getvalue()) + + except Exception as e: + logger.warning("Problem with painting thumbnail in %s", + item._item_page.id, + exc_info = e) + + def _reset_delete_timer(self): + if self.delete_timer: + self.delete_timer.cancel() + self.delete_timer = self.screen.delete_after(10.0, self.page) + + def _reload_menu(self): + self.load_menu_items() + self.screen.redraw(self.page) + +class MenuScreenChangeListener(g15screen.ScreenChangeAdapter): + def __init__(self, plugin): + self.plugin = plugin + + def new_page(self, page): + if self.plugin.page != None and page != self.plugin.page and page.priority > g15screen.PRI_INVISIBLE: + items = self.plugin.menu.get_children() + item = MenuItem(page, self.plugin, "menuitem-%s" % page.id ) + self.plugin._load_item_icon(item) + items.append(item) + items = sorted(items, key=lambda item: item._item_page.title) + self.plugin.menu.set_children(items) + self.plugin.page.redraw() + + def title_changed(self, page, title): + if self.plugin.page != None and page != self.plugin.page: + self.plugin.page.redraw() + + def deleted_page(self, page): + if self.plugin.page != None and page != self.plugin.page: + self.plugin.menu.remove_child(self.plugin.menu.get_child_by_id("menuitem-%s" % page.id)) + self.plugin.page.redraw() \ No newline at end of file diff --git a/src/plugins/mounts/Makefile.am b/src/plugins/mounts/Makefile.am new file mode 100644 index 0000000..16d01ce --- /dev/null +++ b/src/plugins/mounts/Makefile.am @@ -0,0 +1,7 @@ +SUBDIRS = default + +plugindir = $(datadir)/gnome15/plugins/mounts +plugin_DATA = mounts.ui mounts.py + +EXTRA_DIST = \ + $(plugin_DATA) \ No newline at end of file diff --git a/src/plugins/mounts/default/Makefile.am b/src/plugins/mounts/default/Makefile.am new file mode 100644 index 0000000..9f33860 --- /dev/null +++ b/src/plugins/mounts/default/Makefile.am @@ -0,0 +1,7 @@ +themedir = $(datadir)/gnome15/plugins/mounts/default +theme_DATA = \ + default-menu-entry.svg \ + g19-menu-entry.svg + +EXTRA_DIST = \ + $(theme_DATA) diff --git a/src/plugins/mounts/default/default-menu-entry.svg b/src/plugins/mounts/default/default-menu-entry.svg new file mode 100644 index 0000000..61326d2 --- /dev/null +++ b/src/plugins/mounts/default/default-menu-entry.svg @@ -0,0 +1,237 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + ${item_name} + ${item_alt} + + + + + + ${item_name} + ${item_alt} + + + + diff --git a/src/plugins/mounts/default/g19-menu-entry.svg b/src/plugins/mounts/default/g19-menu-entry.svg new file mode 100644 index 0000000..2af6329 --- /dev/null +++ b/src/plugins/mounts/default/g19-menu-entry.svg @@ -0,0 +1,588 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + ${item_name} + + + + ${item_alt} + + + + + + ${item_name} + + + + ${item_alt} + + + + diff --git a/src/plugins/mounts/i18n/mounts.en_GB.po b/src/plugins/mounts/i18n/mounts.en_GB.po new file mode 100644 index 0000000..8545ee5 --- /dev/null +++ b/src/plugins/mounts/i18n/mounts.en_GB.po @@ -0,0 +1,46 @@ +# English translations for gnome15-plugins package. +# Copyright (C) 2012 THE gnome15-plugins'S COPYRIGHT HOLDER +# This file is distributed under the same license as the gnome15-plugins package. +# Automatically generated, 2012. +# +msgid "" +msgstr "" +"Project-Id-Version: gnome15-plugins 0.8.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: 2012-01-08 11:23+0000\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: en_GB\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=ASCII\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: i18n/mounts.glade.h:1 +msgid "Mounts Preferences" +msgstr "Mounts Preferences" + +#: i18n/mounts.glade.h:2 +msgid "Raise page when mounts change" +msgstr "Raise page when mounts change" + +#: i18n/mounts.glade.h:3 +msgid "center" +msgstr "center" + +#: i18n/mounts.glade.h:4 +msgid "scale" +msgstr "scale" + +#: i18n/mounts.glade.h:5 +msgid "stretch" +msgstr "stretch" + +#: i18n/mounts.glade.h:6 +msgid "tile" +msgstr "tile" + +#: i18n/mounts.glade.h:7 +msgid "zoom" +msgstr "zoom" diff --git a/src/plugins/mounts/i18n/mounts.glade.h b/src/plugins/mounts/i18n/mounts.glade.h new file mode 100644 index 0000000..a211f39 --- /dev/null +++ b/src/plugins/mounts/i18n/mounts.glade.h @@ -0,0 +1,7 @@ +char *s = N_("Mounts Preferences"); +char *s = N_("Raise page when mounts change"); +char *s = N_("center"); +char *s = N_("scale"); +char *s = N_("stretch"); +char *s = N_("tile"); +char *s = N_("zoom"); diff --git a/src/plugins/mounts/i18n/mounts.pot b/src/plugins/mounts/i18n/mounts.pot new file mode 100644 index 0000000..1cd7115 --- /dev/null +++ b/src/plugins/mounts/i18n/mounts.pot @@ -0,0 +1,46 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: i18n/mounts.glade.h:1 +msgid "Mounts Preferences" +msgstr "" + +#: i18n/mounts.glade.h:2 +msgid "Raise page when mounts change" +msgstr "" + +#: i18n/mounts.glade.h:3 +msgid "center" +msgstr "" + +#: i18n/mounts.glade.h:4 +msgid "scale" +msgstr "" + +#: i18n/mounts.glade.h:5 +msgid "stretch" +msgstr "" + +#: i18n/mounts.glade.h:6 +msgid "tile" +msgstr "" + +#: i18n/mounts.glade.h:7 +msgid "zoom" +msgstr "" diff --git a/src/plugins/mounts/mounts.py b/src/plugins/mounts/mounts.py new file mode 100644 index 0000000..e289a08 --- /dev/null +++ b/src/plugins/mounts/mounts.py @@ -0,0 +1,345 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2011 Brett Smith +# +# 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 . + +import gnome15.g15locale as g15locale +_ = g15locale.get_translation("mounts", modfile = __file__).ugettext + +import gnome15.g15plugin as g15plugin +import gnome15.util.g15uigconf as g15uigconf +import gnome15.util.g15scheduler as g15scheduler +import gnome15.util.g15gconf as g15gconf +import gnome15.util.g15icontools as g15icontools +import gnome15.g15theme as g15theme +import gnome15.g15driver as g15driver +import gnome15.g15screen as g15screen +import gio +import gtk +import os.path +import gobject + +# Logging +import logging +logger = logging.getLogger(__name__) + +# Plugin details - All of these must be provided +id="mounts" +name=_("Mounts") +description=_("Shows mount points, allows mounting, unmounting \ +and ejecting of removable media. Also displays \ +free, used or total disk space on mounted media.") +author="Brett Smith " +copyright=_("Copyright (C)2011 Brett Smith") +site="http://www.russo79.com/gnome15" +has_preferences=True +unsupported_models = [ g15driver.MODEL_G110, g15driver.MODEL_G11, g15driver.MODEL_G930, g15driver.MODEL_G35 ] +actions={ + g15driver.PREVIOUS_SELECTION : _("Previous mount"), + g15driver.NEXT_SELECTION : _("Next mount"), + g15driver.NEXT_PAGE : _("Next page"), + g15driver.PREVIOUS_PAGE : _("Previous page"), + g15driver.SELECT : _("Mount, unmount or eject"), + g15driver.VIEW : _("Toggle between free,\navailable and used"), + } + + +def show_preferences(parent, driver, gconf_client, gconf_key): + widget_tree = gtk.Builder() + widget_tree.add_from_file(os.path.join(os.path.dirname(__file__), "mounts.ui")) + dialog = widget_tree.get_object("MountsDialog") + dialog.set_transient_for(parent) + g15uigconf.configure_checkbox_from_gconf(gconf_client, "%s/raise" % gconf_key, "RaisePageCheckbox", True, widget_tree) + dialog.run() + dialog.hide() + +def create(gconf_key, gconf_client, screen): + return G15Places(gconf_client, gconf_key, screen) + +POSSIBLE_ICON_NAMES = [ "folder" ] + +MODES = { "free" : _("Free"), "used" : _("Used"), "size" : _("Size") } +MODE_LIST = list(MODES.keys()) + +""" +Represents a mount as a single item in a menu +""" +class MountMenuItem(g15theme.MenuItem): + def __init__(self, id, mount, plugin): + g15theme.MenuItem.__init__(self, id) + self.mount = mount + self._plugin = plugin + self._refresh() + + def _refresh(self): + self.disk_size = 0 + self.disk_free = 0 + self.disk_used = 0 + self.disk_used_pc = 0 + root = self.mount.get_root() + try: + self.fs_attr = root.query_filesystem_info("*") + self.disk_size = float(max(1, self.fs_attr.get_attribute_uint64(gio.FILE_ATTRIBUTE_FILESYSTEM_SIZE))) + self.disk_free = float(self.fs_attr.get_attribute_uint64(gio.FILE_ATTRIBUTE_FILESYSTEM_FREE)) + self.disk_used = float(self.disk_size - self.disk_free) + self.disk_used_pc = int ( ( self.disk_used / self.disk_size ) * 100.0 ) + except Exception as e: + logger.debug("Error refreshing", exc_info = e) + pass + + def get_theme_properties(self): + item_properties = g15theme.MenuItem.get_theme_properties(self) + item_properties["item_name"] = self.mount.get_name() + item_properties["item_type"] = "" + icon_names = [] + icon = self.mount.get_icon() + if isinstance(icon, gio.FileIcon): + icon_names.append(icon.get_file().get_path()) + else: + icon_names += icon.get_names() + + icon_names += "gnome-dev-harddisk" + item_properties["item_icon"] = g15icontools.get_icon_path(icon_names) + item_properties["disk_usage"] = self.disk_used_pc + item_properties["sel_disk_usage"] = self.disk_used_pc + item_properties["disk_used_mb"] = "%4.2f" % (self.disk_used / 1024.0 / 1024.0 ) + item_properties["disk_free_mb"] = "%4.2f" % (self.disk_free / 1024.0 / 1024.0 ) + item_properties["disk_size_mb"] = "%4.2f" % (self.disk_size / 1024.0 / 1024.0 ) + item_properties["disk_used_gb"] = "%4.1f" % (self.disk_used / 1024.0 / 1024.0 / 1024.0 ) + item_properties["disk_free_gb"] = "%4.1f" % (self.disk_free / 1024.0 / 1024.0 / 1024.0 ) + item_properties["disk_size_gb"] = "%4.1f" % (self.disk_size / 1024.0 / 1024.0 / 1024.0 ) + suffix = "G" if self.disk_size >= ( 1 * 1024.0 * 1024.0 * 1024.0 ) else "M" + item_properties["disk_used"] = "%s %s" % ( item_properties["disk_used_gb"], suffix ) + item_properties["disk_free"] = "%s %s" % ( item_properties["disk_free_gb"], suffix ) + item_properties["disk_size"] = "%s %s" % ( item_properties["disk_size_gb"], suffix ) + + if self._plugin._mode == "free": + item_properties["item_alt"] = item_properties["disk_free"] + elif self._plugin._mode == "used": + item_properties["item_alt"] = item_properties["disk_used"] + elif self._plugin._mode == "size": + item_properties["item_alt"] = item_properties["disk_size"] + + return item_properties + + def activate(self): + if self.mount.can_eject(): + self.mount.eject(self._ejected, flags = gio.MOUNT_UNMOUNT_NONE) + else: + if self.mount.can_unmount(): + self.mount.unmount(self._unmounted, flags = gio.MOUNT_UNMOUNT_NONE) + return True + + def _ejected(self, arg1, arg2): + logger.info("Ejected %s %s %s", self.mount.get_name(), str(arg1), str(arg2)) + + def _unmounted(self, arg1, arg2): + logger.info("Unmounted %s %s %s", self.mount.get_name(), str(arg1), str(arg2)) + +""" +Represents a volumne as a single item in a menu +""" +class VolumeMenuItem(g15theme.MenuItem): + def __init__(self, id, volume): + g15theme.MenuItem.__init__(self, id) + self.volume = volume + + def get_theme_properties(self): + item_properties = g15theme.MenuItem.get_theme_properties(self) + item_properties["item_name"] = self.volume.get_name() + item_properties["item_alt"] = "" + item_properties["item_type"] = "" + + item_properties["item_icon"] = g15icontools.get_icon_path([ self.volume.get_icon().get_names()[0], "gnome-dev-harddisk" ]) + return item_properties + + def activate(self): + if self.volume.can_mount(): + self.volume.mount(None, self._mounted, flags = gio.MOUNT_UNMOUNT_NONE) + return True + + def _mounted(self, arg1, arg2): + logger.info("Mounted %s %s %s", self.volume.get_name(), str(arg1), str(arg2)) + + +""" +Places plugin class +""" +class G15Places(g15plugin.G15MenuPlugin): + + def __init__(self, gconf_client, gconf_key, screen): + g15plugin.G15MenuPlugin.__init__(self, gconf_client, gconf_key, screen, POSSIBLE_ICON_NAMES, id, name) + self._signal_handles = [] + self._handle = None + self._modes = [ "free", "used", "size" ] + self._mode = "free" + + def activate(self): + g15plugin.G15MenuPlugin.activate(self) + self.screen.key_handler.action_listeners.append(self) + + # Get the initial list of volumes and mounts + gobject.idle_add(self._do_activate) + + def _do_activate(self): + self.volume_monitor = gio.VolumeMonitor() + for mount in self.volume_monitor.get_mounts(): + if not mount.is_shadowed(): + self._add_mount(mount) + if len(self.menu.get_children()) > 0: + self.menu.add_separator() + for volume in self.volume_monitor.get_volumes(): + if volume.get_mount() == None: + self._add_volume(volume) + + # Watch for changes + self.volume_monitor.connect("mount_added", self._on_mount_added) + self.volume_monitor.connect("mount_removed", self._on_mount_removed) + + # Refresh disk etc space every minute + self._handle = g15scheduler.schedule("DiskRefresh", 60.0, self._refresh) + + def deactivate(self): + g15plugin.G15MenuPlugin.deactivate(self) + self.screen.key_handler.action_listeners.remove(self) + for handle in self._signal_handles: + self.session_bus.remove_signal_receiver(handle) + if self._handle: + self._handle.cancel() + self._handle = None + + def get_theme_properties(self): + properties = g15plugin.G15MenuPlugin.get_theme_properties(self) + properties["alt_title"] = MODES[self._mode] + idx = MODE_LIST.index(self._mode) + 1 + properties["list"] = MODES[MODE_LIST[0] if idx == len(MODE_LIST) else MODE_LIST[idx]] + if isinstance(self.menu.selected, VolumeMenuItem): + properties["sel"] = _("Mount") + elif isinstance(self.menu.selected, MountMenuItem): + properties["sel"] = _("Eject") if self.menu.selected.mount.can_eject() else _("Unmo.") + else: + properties["sel"] = "" + return properties + + def action_performed(self, binding): + if binding.action == g15driver.VIEW: + idx = MODE_LIST.index(self._mode) + 1 + self._mode = MODE_LIST[0] if idx == len(MODE_LIST) else MODE_LIST[idx] + self.screen.redraw(self.page) + + + """ + Private functions + """ + + def _refresh(self): + """ + Refresh the free space etc for all items + """ + for item in self.menu.get_children(): + if isinstance(item, MountMenuItem): + item._refresh() + self.screen.redraw(self.page) + + def _on_mount_added(self, monitor, mount, *args): + + # Remove the volume for this remove + for item in self.menu.get_children(): + if isinstance(item, VolumeMenuItem) and self._get_key(item.volume) == self._get_key(mount): + self._remove_volume(item.volume) + + + """ + Invoked when new mount is available + """ + self._remove_mount(mount) + self._add_mount(mount) + + self._popup() + + def _on_mount_removed(self, monitor, mount, *args): + """ + Invoked when a mount is removed + """ + self._remove_mount(mount) + + # Look for new volumes + for volume in self.volume_monitor.get_volumes(): + if not self._get_item_for_volume(volume) and volume.get_mount() == None: + self._add_volume(volume) + + self._popup() + + def _popup(self): + if not self.page.is_visible() and g15gconf.get_bool_or_default(self.gconf_client,"%s/raise" % self.gconf_key, True): + self._raise_timer = self.screen.set_priority(self.page, g15screen.PRI_HIGH, revert_after = 4.0) + self.screen.redraw(self.page) + + def _get_key(self, item): + """ + Get a unique key for volume / mount + """ + return "%s-%s" % ( str(item.get_uuid()), str(item.get_name())) + + def _remove_volume(self, volume): + """ + Remove a volume from the menu + """ + logger.info("Removing volume %s", str(volume)) + self.menu.remove_child(self._get_item_for_volume(volume)) + self.screen.redraw(self.page) + + def _remove_mount(self, mount): + """ + Remove a mount from the menu + """ + logger.info("Removing mount %s", str(mount)) + mnt = self._get_item_for_mount(mount) + if mnt: + self.menu.remove_child(mnt) + self.screen.redraw(self.page) + + def _get_item_for_mount(self, mount): + """ + Get the menu item for the given mount + """ + for item in self.menu.get_children(): + if isinstance(item, MountMenuItem) and self._get_key(mount) == self._get_key(item.mount): + return item + + def _get_item_for_volume(self, volume): + """ + Get the menu item for the given volume + """ + for item in self.menu.get_children(): + if isinstance(item, VolumeMenuItem) and self._get_key(volume) == self._get_key(item.volume): + return item + + def _add_volume(self, volume): + """ + Add a new volume to the menu + """ + logger.info("Adding volume %s", str(volume)) + item = VolumeMenuItem("volumeitem-%s" % self._get_key(volume), volume) + self.menu.add_child(item) + self.screen.redraw(self.page) + + def _add_mount(self, mount): + """ + Add a new mount to the menu + """ + logger.info("Adding mount %s", str(mount)) + item = MountMenuItem("mountitem-%s" % self._get_key(mount), mount, self) + self.menu.add_child(item, 0) + self.screen.redraw(self.page) \ No newline at end of file diff --git a/src/plugins/mounts/mounts.ui b/src/plugins/mounts/mounts.ui new file mode 100644 index 0000000..e195fee --- /dev/null +++ b/src/plugins/mounts/mounts.ui @@ -0,0 +1,100 @@ + + + + + + + False + 5 + Mounts Preferences + False + True + dialog + + + True + False + 2 + + + True + False + 4 + + + Raise page when mounts change + True + True + False + True + + + True + True + 0 + + + + + False + False + 0 + + + + + True + False + end + + + gtk-close + True + True + True + True + + + False + False + 0 + + + + + False + True + end + 1 + + + + + + button9 + + + + + + + + + + zoom + + + tile + + + center + + + scale + + + stretch + + + + diff --git a/src/plugins/mpris/Makefile.am b/src/plugins/mpris/Makefile.am new file mode 100644 index 0000000..9533a95 --- /dev/null +++ b/src/plugins/mpris/Makefile.am @@ -0,0 +1,6 @@ +SUBDIRS = default bigcover +plugindir = $(datadir)/gnome15/plugins/mpris +plugin_DATA = mpris.py + +EXTRA_DIST = \ + $(plugin_DATA) \ No newline at end of file diff --git a/src/plugins/mpris/bigcover/Makefile.am b/src/plugins/mpris/bigcover/Makefile.am new file mode 100644 index 0000000..732b679 --- /dev/null +++ b/src/plugins/mpris/bigcover/Makefile.am @@ -0,0 +1,6 @@ +themedir = $(datadir)/gnome15/plugins/mpris/bigcover +theme_DATA = bigcover.theme \ + g19.svg + +EXTRA_DIST = \ + $(theme_DATA) diff --git a/src/plugins/mpris/bigcover/bigcover.theme b/src/plugins/mpris/bigcover/bigcover.theme new file mode 100644 index 0000000..21572be --- /dev/null +++ b/src/plugins/mpris/bigcover/bigcover.theme @@ -0,0 +1,4 @@ +[theme] +name=Big Cover +description=Album artwork takes most of screen. +supported_models=g19 \ No newline at end of file diff --git a/src/plugins/mpris/bigcover/g19.svg b/src/plugins/mpris/bigcover/g19.svg new file mode 100644 index 0000000..3f7a7b9 --- /dev/null +++ b/src/plugins/mpris/bigcover/g19.svg @@ -0,0 +1,427 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + ${time_text} + + diff --git a/src/plugins/mpris/default/Makefile.am b/src/plugins/mpris/default/Makefile.am new file mode 100644 index 0000000..36729e1 --- /dev/null +++ b/src/plugins/mpris/default/Makefile.am @@ -0,0 +1,9 @@ +themedir = $(datadir)/gnome15/plugins/mpris/default +theme_DATA = g19.svg \ + default.svg \ + mx5500.svg \ + pause.gif \ + play.gif + +EXTRA_DIST = \ + $(theme_DATA) diff --git a/src/plugins/mpris/default/default.svg b/src/plugins/mpris/default/default.svg new file mode 100644 index 0000000..03a1245 --- /dev/null +++ b/src/plugins/mpris/default/default.svg @@ -0,0 +1,254 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + ${time_text} + ${title} + ${album} + ${artist} + + diff --git a/src/plugins/mpris/default/g19.svg b/src/plugins/mpris/default/g19.svg new file mode 100644 index 0000000..a964ecb --- /dev/null +++ b/src/plugins/mpris/default/g19.svg @@ -0,0 +1,542 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + ${album} + ${artist} + ${title} + ${time_text} + ${genre}${bitrate}bps + + + + + + + + + + + + + + + diff --git a/src/plugins/mpris/default/mx5500.svg b/src/plugins/mpris/default/mx5500.svg new file mode 100644 index 0000000..33e67ea --- /dev/null +++ b/src/plugins/mpris/default/mx5500.svg @@ -0,0 +1,241 @@ + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + ${time_text} + ${title} + ${album} + ${artist} + + diff --git a/src/plugins/mpris/default/pause.gif b/src/plugins/mpris/default/pause.gif new file mode 100644 index 0000000..7301bd1 Binary files /dev/null and b/src/plugins/mpris/default/pause.gif differ diff --git a/src/plugins/mpris/default/play.gif b/src/plugins/mpris/default/play.gif new file mode 100644 index 0000000..1a495ae Binary files /dev/null and b/src/plugins/mpris/default/play.gif differ diff --git a/src/plugins/mpris/i18n/mpris.en_GB.po b/src/plugins/mpris/i18n/mpris.en_GB.po new file mode 100644 index 0000000..464a3ba --- /dev/null +++ b/src/plugins/mpris/i18n/mpris.en_GB.po @@ -0,0 +1,42 @@ +# English translations for gnome15-plugins package. +# Copyright (C) 2012 THE gnome15-plugins'S COPYRIGHT HOLDER +# This file is distributed under the same license as the gnome15-plugins package. +# Automatically generated, 2012. +# +msgid "" +msgstr "" +"Project-Id-Version: gnome15-plugins 0.8.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: 2012-01-08 11:23+0000\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: en_GB\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=ASCII\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: mpris.py:61 +msgid "Media Player" +msgstr "Media Player" + +#: mpris.py:62 +msgid "" +"Displays information about currently playing media. Requires a player that " +"supports the MPRIS (version 1 or 2) specification. This includes Rhythmbox, " +"Banshee (with a plugin), Audacious, VLC and others. Supports multiple media " +"players running at the same time, each gets their own screen." +msgstr "" +"Displays information about currently playing media. Requires a player that " +"supports the MPRIS (version 1 or 2) specification. This includes Rhythmbox, " +"Banshee (with a plugin), Audacious, VLC and others. Supports multiple media " +"players running at the same time, each gets their own screen." + +#: mpris.py:68 +msgid "Copyright (C)2010 Brett Smith" +msgstr "Copyright (C)2010 Brett Smith" + +#: mpris.py:282 +msgid "No track playing" +msgstr "No track playing" diff --git a/src/plugins/mpris/i18n/mpris.pot b/src/plugins/mpris/i18n/mpris.pot new file mode 100644 index 0000000..106df20 --- /dev/null +++ b/src/plugins/mpris/i18n/mpris.pot @@ -0,0 +1,38 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: mpris.py:61 +msgid "Media Player" +msgstr "" + +#: mpris.py:62 +msgid "" +"Displays information about currently playing media. Requires a player that " +"supports the MPRIS (version 1 or 2) specification. This includes Rhythmbox, " +"Banshee (with a plugin), Audacious, VLC and others. Supports multiple media " +"players running at the same time, each gets their own screen." +msgstr "" + +#: mpris.py:68 +msgid "Copyright (C)2010 Brett Smith" +msgstr "" + +#: mpris.py:282 +msgid "No track playing" +msgstr "" diff --git a/src/plugins/mpris/mpris.py b/src/plugins/mpris/mpris.py new file mode 100644 index 0000000..b31bc1d --- /dev/null +++ b/src/plugins/mpris/mpris.py @@ -0,0 +1,768 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2010 Brett Smith +# Copyright (C) 2013 Brett Smith +# Nuno Araujo +# +# 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 . + +import gnome15.g15locale as g15locale +_ = g15locale.get_translation("mpris", modfile = __file__).ugettext + +import gnome15.g15screen as g15screen +import gnome15.g15driver as g15driver +import gnome15.util.g15convert as g15convert +import gnome15.util.g15scheduler as g15scheduler +import gnome15.util.g15pythonlang as g15pythonlang +import gnome15.util.g15cairo as g15cairo +import gnome15.util.g15icontools as g15icontools +import gnome15.g15theme as g15theme +import gnome15.g15plugin as g15plugin +import gnome15.g15devices as g15devices +import gnome15.g15actions as g15actions +import dbus +import os +import time + +import xdg.Mime as mime +import xdg.BaseDirectory +import urllib + +# Logging +import logging +from dbus.exceptions import DBusException +logger = logging.getLogger(__name__) + +# Custom actions +NEXT_TRACK = "mpris-next-track" +PREV_TRACK = "mpris-previous-track" +PLAY_TRACK = "mpris-play-track" +STOP_TRACK = "mpris-stop-track" + +# Register the action with all supported models +g15devices.g15_action_keys[NEXT_TRACK] = g15actions.ActionBinding(NEXT_TRACK, [ g15driver.G_KEY_NEXT ], g15driver.KEY_STATE_UP) +g15devices.g19_action_keys[NEXT_TRACK] = g15actions.ActionBinding(NEXT_TRACK, [ g15driver.G_KEY_NEXT ], g15driver.KEY_STATE_UP) +g15devices.g15_action_keys[PREV_TRACK] = g15actions.ActionBinding(PREV_TRACK, [ g15driver.G_KEY_PREV ], g15driver.KEY_STATE_UP) +g15devices.g19_action_keys[PREV_TRACK] = g15actions.ActionBinding(PREV_TRACK, [ g15driver.G_KEY_PREV ], g15driver.KEY_STATE_UP) +g15devices.g15_action_keys[STOP_TRACK] = g15actions.ActionBinding(STOP_TRACK, [ g15driver.G_KEY_STOP ], g15driver.KEY_STATE_UP) +g15devices.g19_action_keys[STOP_TRACK] = g15actions.ActionBinding(STOP_TRACK, [ g15driver.G_KEY_STOP ], g15driver.KEY_STATE_UP) +g15devices.g15_action_keys[PLAY_TRACK] = g15actions.ActionBinding(PLAY_TRACK, [ g15driver.G_KEY_PLAY ], g15driver.KEY_STATE_UP) +g15devices.g19_action_keys[PLAY_TRACK] = g15actions.ActionBinding(PLAY_TRACK, [ g15driver.G_KEY_PLAY ], g15driver.KEY_STATE_UP) + +# Plugin details - All of these must be provided +id="mpris" +name=_("Now Playing") +description=_("Displays information about currently playing media. Requires \ +a player that supports the MPRIS (version 1 or 2) specification. This \ +includes Rhythmbox, Banshee (with a plugin), Audacious, VLC and others. \ +Supports multiple media players running at the same time, each gets \ +their own screen.") +author="Brett Smith " +copyright=_("Copyright (C)2010 Brett Smith") +site="http://www.russo79.com/gnome15" +has_preferences=False +unsupported_models = [ g15driver.MODEL_G110, g15driver.MODEL_G11, g15driver.MODEL_G930, g15driver.MODEL_G35 ] +actions={ + NEXT_TRACK : "Skip to the next track", + PREV_TRACK : "Skip to the previous track", + STOP_TRACK : "Stop the current track", + PLAY_TRACK : "Play / Pause the current track", + } + +# Players that are not supported +mpris_blacklist = [ "org.mpris.xbmc", "org.mpris.audacious" ] + +def create(gconf_key, gconf_client, screen): + return G15MPRIS(gconf_client,gconf_key, screen) + +class AbstractMPRISPlayer(): + + def __init__(self, gconf_client, screen, players, interface_name, session_bus, title, theme): + logger.info("Starting player %s", interface_name) + self.stopped = False + self.elapsed = 0 + self.volume = 0 + self.hidden = True + self.title = title + self.session_bus = session_bus + self.page = None + self.duration = 0 + self.screen = screen + self.interface_name = interface_name + self.players = players + self.playing_uri = None + self.playback_started = 0 + self.start_elapsed = 0 + self.gconf_client = gconf_client + self.cover_image = None + self.thumb_image = None + self.cover_uri = None + self.song_properties = {} + self.status = "Stopped" + self.redraw_timer = None + self.theme = theme + + def action_performed(self, binding): + if binding.action == NEXT_TRACK: + self.next_track() + return True + elif binding.action == PREV_TRACK: + self.prev_track() + return True + elif binding.action == PLAY_TRACK: + self.play_pause_track() + return True + elif binding.action == STOP_TRACK: + self.stop_track() + return True + + def next_track(self): + logger.warning("Next track not implemented") + + def prev_track(self): + logger.warning("Previous track not implemented") + + def play_pause_track(self): + logger.warning("Play pause track not implemented") + + def stop_track(self): + logger.warning("Stop track not implemented") + + def check_status(self): + new_status = self.get_new_status() + self.volume = self.get_volume() + if new_status != self.status: + self.set_status(new_status) + else: + if self.status == "Playing": + self.recalc_progress() + self.screen.redraw(self.page) + + def reset_elapsed(self): + logger.debug("Reset track elapsed time") + self.start_elapsed = self.get_progress() + self.playback_started = time.time() + + def set_status(self, new_status): + if new_status != self.status: + logger.info("Playback status changed to %s", new_status) + self.status = new_status + if self.status == "Playing": + g15scheduler.schedule("playbackStarted", 1.0, self._playback_started) + elif self.status == "Paused": + self.cancel_redraw() + if self.page != None: + logger.info("Paused.") + self.load_song_details() + self.screen.set_priority(self.page, g15screen.PRI_HIGH, revert_after = 3.0) + else: + self.show_page() + elif self.status == "Stopped": + self.cancel_redraw() + self.hide_page() + + def _playback_started(self): + self.reset_elapsed() + logger.info("Now playing, showing page") + self.show_page() + self.schedule_redraw() + + def stop(self): + logger.info("Stopping player %s", self.interface_name) + self.stopped = True + self.on_stop() + if self.redraw_timer != None: + self.redraw_timer.cancel() + if self.page != None: + self.hide_page() + del self.players[self.interface_name] + + def show_page(self): + self.load_song_details() + self.page = self.screen.get_page(page_id="MPRIS%s" % self.title) + if self.page == None: + self.page = g15theme.G15Page("MPRIS%s" % self.title, self.screen, on_shown=self.on_shown, \ + on_hidden=self.on_hidden, theme_properties_callback = self._get_properties, \ + panel_painter = self.paint_panel, thumbnail_painter = self.paint_thumbnail, \ + theme = self.theme, title = self.title, + originating_plugin = self) + self.screen.add_page(self.page) + self.screen.redraw(self.page) + else: + self.screen.set_priority(self.page, g15screen.PRI_HIGH, revert_after = 3.0) + + def hide_page(self): + self.screen.del_page(self.page) + self.page = None + + def redraw(self): + if self.status == "Playing": + self.elapsed = time.time() - self.playback_started + self.start_elapsed + self.recalc_progress() + self.screen.redraw(self.page) + self.schedule_redraw() + + def schedule_redraw(self): + self.cancel_redraw() + self.redraw_timer = g15scheduler.queue("mprisDataQueue-%s" % self.screen.device.uid, "MPRIS2Redraw", 1.0, self.redraw) + + def on_shown(self): + self.hidden = False + if self.status == "Playing": + self.schedule_redraw() + + def on_hidden(self): + self.hidden = True + self.cancel_redraw() + + def cancel_redraw(self): + if self.redraw_timer != None: + self.redraw_timer.cancel() + self.redraw_timer = None + + def paint_thumbnail(self, canvas, allocated_size, horizontal): + if self.page != None and self.thumb_image != None: + size = g15cairo.paint_thumbnail_image(allocated_size, self.thumb_image, canvas) + return size + + def paint_panel(self, canvas, allocated_size, horizontal): + if self.page != None and self.thumb_image != None and self.status == "Playing": + size = g15cairo.paint_thumbnail_image(allocated_size, self.thumb_image, canvas) + return size + + def process_properties(self): + + logger.debug("Processing properties") + + self.recalc_progress() + # Find the best icon for the media + + + if "art_uri" in self.song_properties and self.song_properties["art_uri"] != "": + new_cover_uri = self.song_properties["art_uri"] + else: + cover_art = os.path.join(xdg.BaseDirectory.xdg_cache_home, + "rhythmbox", + "covers", + "%s - %s.jpg" % (self.song_properties["artist"], + self.song_properties["album"])) + new_cover_uri = None + if cover_art != None and os.path.exists(cover_art): + new_cover_uri = cover_art + + if new_cover_uri == None: + new_cover_uri = self.get_default_cover() + + if new_cover_uri != self.cover_uri: + self.cover_uri = new_cover_uri + logger.info("Getting cover art from %s", self.cover_uri) + self.cover_image = None + self.thumb_image = None + if self.cover_uri != None: + cover_image = g15cairo.load_surface_from_file(self.cover_uri, self.screen.driver.get_size()[0]) + if cover_image: + self.cover_image = cover_image + + # If the cover URI was from HTTP, then we cached it. Use that as the URI + if self.cover_uri.startswith("http:") or self.cover_uri.startswith("http:"): + self.cover_uri = g15cairo.get_image_cache_file(self.cover_uri, self.screen.driver.get_size()[0]) + else: + cover_image = self.get_default_cover() + logger.warning("Failed to loaded preferred cover art, " \ + "falling back to default of %s", cover_image) + if cover_image: + self.cover_uri = cover_image + self.cover_image = g15cairo.load_surface_from_file(self.cover_uri, self.screen.driver.get_size()[0]) + + # Track status + if self.status == "Stopped": + self.song_properties["stopped"] = True + self.song_properties["icon"] = g15icontools.get_icon_path(["media-stop", "media-playback-stop", "gtk-media-stop", "player_stop" ], self.screen.height) + self.song_properties["title"] = _("No track playing") + self.song_properties["time_text"] = "" + else: + if self.status == "Playing": + if self.screen.driver.get_bpp() == 1: + self.thumb_image = g15cairo.load_surface_from_file(os.path.join(os.path.join(os.path.dirname(__file__), "default"), "play.gif")) + else: + self.thumb_image = self.cover_image + self.song_properties["playing"] = True + else: + if self.screen.driver.get_bpp() == 1: + self.thumb_image = g15cairo.load_surface_from_file(os.path.join(os.path.join(os.path.dirname(__file__), "default"), "pause.gif")) + else: + self.thumb_image = self.cover_image + self.song_properties["paused"] = True + self.song_properties["icon"] = self.cover_uri + + def get_default_cover(self): + mime_type = mime.get_type(self.playing_uri) + new_cover_uri = None + if mime_type != None: + mime_icon = g15icontools.get_icon_path(str(mime_type).replace("/","-"), size=self.screen.height) + if mime_icon != None: + new_cover_uri = mime_icon + if new_cover_uri != None: + try : + new_cover_uri = "file://" + urllib.pathname2url(new_cover_uri) + except Exception as e: + logger.debug("Error getting default cover, using None", exc_info = e) + new_cover_uri = None + + if new_cover_uri == None: + new_cover_uri = g15icontools.get_icon_path(["audio-player", "applications-multimedia" ], size=self.screen.height) + + return new_cover_uri + + def recalc_progress(self): + logger.debug("Recalculating progress") + if not self.duration or self.duration < 1: + self.song_properties["track_progress_pc"] = "0" + self.song_properties["time_text"] = self.get_formatted_time(self.elapsed) + else: + pc = 100 / float(self.duration) + val = int(pc * self.elapsed) + self.song_properties["track_progress_pc"] = str(val) + self.song_properties["time_text"] = self.get_formatted_time(self.elapsed) + " of " + self.get_formatted_time(self.duration) + + # Volume Icon + vol_icon = "audio-volume-muted" + if self.volume > 0.0 and self.volume < 34.0: + vol_icon = "audio-volume-low" + elif self.volume >= 34.0 and self.volume < 67.0: + vol_icon = "audio-volume-medium" + elif self.volume >= 67.0: + vol_icon = "audio-volume-high" + self.song_properties["vol_icon"] = g15icontools.get_icon_path(vol_icon, self.screen.height) + + # For the bars on the G15 (the icon is too small, bars are better) + for i in range(0, int( self.volume / 10 ) + 1, 1): + self.song_properties["bar" + str(i)] = True + + + def get_formatted_time(self, seconds): + return "%0d.%02d" % ( int (seconds / 60), int( seconds % 60 ) ) + + def get_new_status(self): + raise Exception("Not implemented.") + + def load_song_details(self): + raise Exception("Not implemented.") + + def get_progress(self): + raise Exception("Not implemented.") + + def get_volume(self): + raise Exception("Not implemented.") + + def on_stop(self): + raise Exception("Not implemented.") + + def _get_properties(self): + return dict(self.song_properties) + +class MPRIS1Player(AbstractMPRISPlayer): + + def __init__(self, gconf_client, screen, players, bus_name, session_bus, theme): + self.timer = None + root_obj = session_bus.get_object(bus_name, '/') + root = dbus.Interface(root_obj, 'org.freedesktop.MediaPlayer') + AbstractMPRISPlayer.__init__(self, gconf_client, screen, players, bus_name, session_bus, root.Identity(), theme) + + # There is no seek / position changed event in MPRIS1, so we poll + player_obj = session_bus.get_object(bus_name, '/Player') + self.player = dbus.Interface(player_obj, 'org.freedesktop.MediaPlayer') + + # Set the initial status + self._get_elapsed() + self.check_status() + + # Start polling for status, position and track changes + self.timer = g15scheduler.queue("mprisDataQueue-%s" % self.screen.device.uid, "UpdateTrackData", 1.0, self.update_track) + session_bus.add_signal_receiver(self.track_changed_handler, dbus_interface = "org.freedesktop.MediaPlayer", signal_name = "TrackChange") + + def next_track(self): + self.player.Next() + + def prev_track(self): + self.player.Prev() + + def play_pause_track(self): + status = self.player.GetStatus() + if status[0] == 0: + self.player.Pause() + else: + self.player.Play() + + def stop_track(self): + self.player.Stop() + + def update_track(self): + self._get_elapsed() + self.playback_started = time.time() + self.check_status() + if self.status == "Playing": + self.timer = g15scheduler.queue("mprisDataQueue-%s" % self.screen.device.uid, "UpdateTrackData", 1.0, self.update_track) + else: + self.timer = g15scheduler.queue("mprisDataQueue-%s" % self.screen.device.uid, "UpdateTrackData", 5.0, self.update_track) + + def on_stop(self): + if self.timer != None: + self.timer.cancel() + self.session_bus.remove_signal_receiver(self.track_changed_handler, dbus_interface = "org.freedesktop.MediaPlayer", signal_name = "TrackChange") + + def track_changed_handler(self, detail): + g15scheduler.queue("mprisDataQueue-%s" % self.screen.device.uid, "LoadTrackDetails", 1.0, self.load_and_draw) + + def load_and_draw(self): + self.load_song_details() + self.screen.redraw() + + def get_volume(self): + return 50 + + def get_new_status(self): + logger.debug("Getting status") + status = self.player.GetStatus() + if status[0] == 0: + return "Playing" + elif status[0] == 1: + return "Paused" + else: + return "Stopped" + + def load_song_details(self): + meta_data = self.player.GetMetadata() + + # Format properties that need formatting + bitrate = g15pythonlang.value_or_default(meta_data,"audio-bitrate", 0) + if str(bitrate) == "0": + bitrate = "" + self.playing_uri = g15pythonlang.value_or_blank(meta_data,"location") + self.duration = g15pythonlang.value_or_default(meta_data,"time", 0) + if self.duration == 0: + self.duration = g15pythonlang.value_or_default(meta_data,"mtime", 0) / 1000 + + # General properties + self.song_properties = { + "status": self.status, + "uri": self.playing_uri, + "art_uri": g15pythonlang.value_or_blank(meta_data,"arturl"), + "title": g15pythonlang.value_or_blank(meta_data,"title"), + "genre": g15pythonlang.value_or_blank(meta_data,"genre"), + "track_no": g15pythonlang.value_or_blank(meta_data,"tracknumber"), + "artist": g15pythonlang.value_or_blank(meta_data,"artist"), + "album": g15pythonlang.value_or_blank(meta_data,"album"), + "bitrate": bitrate, + "rating": g15pythonlang.value_or_default(meta_data,"rating", 0.0), + "album_artist": g15pythonlang.value_or_blank(meta_data,"mb album artist"), + } + + self.process_properties() + + def get_progress(self): + return float(self.player.PositionGet()) / 1000.0 + + def _get_elapsed(self): + self.start_elapsed = float(self.player.PositionGet()) / float(1000) + + +class MPRIS2Player(AbstractMPRISPlayer): + + def __init__(self, gconf_client, screen, players, bus_name, session_bus, theme): + self.last_properties = None + self.tracks = [] + + # Connect to DBUS + player_obj = session_bus.get_object(bus_name, '/org/mpris/MediaPlayer2') + self.player = dbus.Interface(player_obj, 'org.mpris.MediaPlayer2.Player') + self.player_properties = dbus.Interface(player_obj, 'org.freedesktop.DBus.Properties') + try: + identity = self.player_properties.Get("org.mpris.MediaPlayer2", "Identity") + except DBusException as e: + logger.debug("Error getting identify of player. Using default indentify.", exc_info = e) + # Set a default identity if we cannot get players identity + Identity = "MPRIS2" + + # Connect to DBUS + self.track_list = None + self.track_list_properties = None + try: + self.track_list = dbus.Interface(player_obj, 'org.mpris.MediaPlayer2.TrackList') + self.track_list_properties = dbus.Interface(self.track_list, 'org.freedesktop.DBus.Properties') + self.load_track_list() + except ( dbus.DBusException, KeyError ) as e: + logger.debug("Cound not load track list", exc_info = e) + pass + + if self.track_list is None: + logger.info("No TrackList interface") + + # Configure the initial state + AbstractMPRISPlayer.__init__(self, gconf_client, screen, players, bus_name, session_bus, identity, theme) + + session_bus.add_signal_receiver(self.properties_changed_handler, dbus_interface = "org.freedesktop.DBus.Properties", signal_name = "PropertiesChanged") + session_bus.add_signal_receiver(self.seeked, dbus_interface = "org.mpris.MediaPlayer2.Player", signal_name = "Seeked") + + # Set the initial status + self.check_status() + + # Workarounds + self.timer = None + + # xnoise doesn't send seeked signals, so we need to refresh + if "xnoise" in bus_name: + self.timer = g15scheduler.queue("mprisDataQueue-%s" % self.screen.device.uid, "UpdateTrackData", 1.0, self.update_track) + + def next_track(self): + self.player.Next() + + def prev_track(self): + self.player.Previous() + + def play_pause_track(self): + self.player.PlayPause() + + def stop_track(self): + self.player.Stop() + + def load_track_list(self): + logger.info("Loading tracklist") + track_list_props = self.track_list_properties.GetAll("org.mpris.MediaPlayer2.TrackList") + self.tracks = [] + for track in track_list_props["Tracks"]: + logger.info(" Track %s", track) + + def on_stop(self): + if self.timer: + self.timer.cancel() + self.session_bus.remove_signal_receiver(self.properties_changed_handler, dbus_interface = "org.freedesktop.DBus.Properties", signal_name = "PropertiesChanged") + self.session_bus.remove_signal_receiver(self.seeked, dbus_interface = "org.mpris.MediaPlayer2.Player", signal_name = "Seeked") + + def get_new_status(self): + logger.debug("Getting status") + status = self.player_properties.Get("org.mpris.MediaPlayer2.Player", "PlaybackStatus") + logger.debug("Finished geting status") + return status + + def seeked(self, seek_time): + + """ + This looks like some kind of timing problem with Banshee. It + sometimes sends 0 as the seektime, and rreading it immediately + also sometimes returns 0. Introducing a short delay fixes + the problem + """ + if seek_time == 0: + logger.warning("Received no progress in seeked event, working around problem") + time.sleep(0.5) + seek_time = self.get_progress() * 1000 * 1000 + + self.start_elapsed = seek_time / 1000 / 1000 + +# self.start_elapsed = self.get_progress() + logger.info("Seek changed to %f (%d)", self.start_elapsed, seek_time) + self.playback_started = time.time() + self.recalc_progress() + self.screen.redraw() + + def properties_changed_handler(self, something, properties, list): + logger.info("Properties changed, '%s' scheduling a reload", str(properties)) + + if "PlaybackStatus" in properties: + self.set_status(properties["PlaybackStatus"]) + + # Check if the track has changed + meta = {} + if "Metadata" in properties: + meta = properties["Metadata"] + if ( "xesam:url" in meta and meta["xesam:url"] != self.playing_uri ) or \ + ( "mpris:trackid" in meta and meta["mpris:trackid"] != self.playing_track_id ) or \ + ( "xesam:title" in meta and meta["xesam:title"] != self.playing_track_id ) or \ + ( "xesam:artist" in meta and meta["xesam:artist"] != self.playing_track_id ) or \ + ( "xesam:album" in meta and meta["xesam:album"] != self.playing_track_id ): + + """ + This doesn't seem right, but it stops the hanging problem when notify-lcd is enabled and + tracks change + """ + g15scheduler.schedule("loadMeta", 1.0, self.reset_elapsed) + + if "Volume" in properties: + self.volume = int(properties["Volume"] * 100) + + if self.last_properties == None: + self.last_properties = dict(properties) + else: + for key in properties: + self.last_properties[key] = properties[key] + + if "Metadata" in self.last_properties: + """ + This doesn't seem right, but it stops the hanging problem when notify-lcd is enabled and + tracks change + """ + g15scheduler.schedule("loadMeta", 1.0, self.load_meta) + + def _load_and_redraw(self): + self.load_meta() + self.schedule_redraw() + + def load_song_details(self): + if not self.stopped: + logger.info("Getting all song properties") + properties = self.player_properties.Get("org.mpris.MediaPlayer2.Player", "Metadata") + logger.info("Got all song properties") + self.last_properties = {"Metadata":properties} + self.load_meta() + + def load_meta(self): + logger.debug("Loading MPRIS2 meta data") + meta_data = self.last_properties["Metadata"] + + # Format properties that need formatting + bitrate = g15pythonlang.value_or_default(meta_data,"xesam:audioBitrate", 0) + if bitrate == 0: + bitrate = "" + else: + bitrate = str(bitrate / 1024) + + self.playing_uri = g15pythonlang.value_or_blank(meta_data,"xesam:url") + self.playing_track_id = g15pythonlang.value_or_blank(meta_data,"mpris:trackid") + self.playing_title = g15pythonlang.value_or_blank(meta_data,"xesam:title") + self.playing_artist = g15pythonlang.value_or_blank(meta_data,"xesam:artist") + self.playing_album = g15pythonlang.value_or_blank(meta_data,"xesam:album") + + # General properties + self.song_properties = { + "status": self.status, + "tracklist": len(self.tracks) > 0, + "uri": self.playing_uri, + "track_id": self.playing_track_id, + "title": g15pythonlang.value_or_blank(meta_data,"xesam:title"), + "art_uri": g15pythonlang.value_or_blank(meta_data,"mpris:artUrl"), + "genre": ",".join(list(g15pythonlang.value_or_empty(meta_data,"xesam:genre"))), + "track_no": g15pythonlang.value_or_blank(meta_data,"xesam:trackNumber"), + "artist": ",".join(list(g15pythonlang.value_or_empty(meta_data,"xesam:artist"))), + "album": g15pythonlang.value_or_blank(meta_data,"xesam:album"), + "bitrate": bitrate, + "rating": g15pythonlang.value_or_default(meta_data,"xesam:userRating", 0.0), + "album_artist": ",".join(list(g15pythonlang.value_or_empty(meta_data,"xesam:albumArtist"))), + } + + self.duration = g15pythonlang.value_or_default(meta_data, "mpris:length", 0) / 1000 / 1000 + self.process_properties() + + def get_volume(self): + try: + return int(self.player_properties.Get("org.mpris.MediaPlayer2.Player", "Volume") * 100) + except DBusException as d: + logger.debug("Could not read volume from player. Setting to 100", exc_info = d) + # Nuvola doesn't support the Volume property + return 100 + + def get_progress(self): + if self.status == "Playing": + try : + # This call seems to be where it usually hangs, although not always????? + return self.player_properties.Get("org.mpris.MediaPlayer2.Player", "Position") / 1000 / 1000 + except Exception as e: + logger.debug("Could not read player position.", exc_info = e) + pass + return 0 + + def update_track(self): + logger.debug("Updating elapsed time") + self.reset_elapsed() + self.timer = g15scheduler.queue("mprisDataQueue-%s" % self.screen.device.uid, "UpdateTrackData", 1.0 if self.status == "Playing" else 5.0, self.update_track) + +class G15MPRIS(g15plugin.G15Plugin): + + def __init__(self, gconf_client, gconf_key, screen): + g15plugin.G15Plugin.__init__(self, gconf_client, gconf_key, screen) + self.session_bus = None + + def activate(self): + self.players = {} + g15plugin.G15Plugin.activate(self) + if self.session_bus == None: + self.session_bus = dbus.SessionBus() + self.session_bus.call_on_disconnection(self._dbus_disconnected) + + + self.screen.key_handler.action_listeners.append(self) + self._discover() + + # Watch for players appearing and disappearing + self.session_bus.add_signal_receiver(self._name_owner_changed, + dbus_interface='org.freedesktop.DBus', + signal_name='NameOwnerChanged') + + def deactivate(self): + g15plugin.G15Plugin.deactivate(self) + self.screen.key_handler.action_listeners.remove(self) + for key in self.players.keys(): + self.players[key].stop() + g15scheduler.stop_queue("mprisDataQueue-%s" % self.screen.device.uid) + self.session_bus.remove_signal_receiver(self._name_owner_changed, + dbus_interface='org.freedesktop.DBus', + signal_name='NameOwnerChanged') + + def destroy(self): + pass + + def action_performed(self, binding): + vis_page = self.screen.get_visible_page() + + # First send to the visible player + for p in self.players.values(): + if vis_page == p.page: + return p.action_performed(binding) + + # Now send to just the first player + if len(self.players) > 0: + return self.players.values()[0].action_performed(binding) + + def _name_owner_changed(self, name, old_owner, new_owner): + logger.debug("Name owner changed for %s from %s to %s", name, old_owner, new_owner) + if name.startswith("org.mpris.MediaPlayer2"): + logger.info("MPRIS2 Name owner changed for %s from %s to %s", name, old_owner, new_owner) + if new_owner == "" and name in self.players: + self.players[name].stop() + elif old_owner == "" and not name in self.players: + self.players[name] = MPRIS2Player(self.gconf_client, self.screen, self.players, name, self.session_bus, self.create_theme()) + elif name.startswith("org.mpris."): + logger.info("MPRIS1 Name owner changed for %s from %s to %s", name, old_owner, new_owner) + if new_owner == "" and name in self.players: + self.players[name].stop() + elif old_owner == "" and not name in self.players: + if not name in mpris_blacklist: + self.players[name] = MPRIS1Player(self.gconf_client, self.screen, self.players, name, self.session_bus, self.create_theme()) + else: + logger.info("%s is a blacklisted player, ignoring", name) + + def _discover(self): + # Find new players + active_list = self.session_bus.list_names() + for name in active_list: + if not name in mpris_blacklist: + # MPRIS 2 + if not name in self.players and name.startswith("org.mpris.MediaPlayer2"): + self.players[name] = MPRIS2Player(self.gconf_client, self.screen, self.players, name, self.session_bus, self.create_theme()) + # MPRIS 1 + elif not name in self.players and name.startswith("org.mpris."): + self.players[name] = MPRIS1Player(self.gconf_client, self.screen, self.players, name, self.session_bus, self.create_theme()) + + def _dbus_disconnected(self, connection): + logger.debug("DBUS Disconnected") + self.session_bus = None diff --git a/src/plugins/mpris/mpris.ui b/src/plugins/mpris/mpris.ui new file mode 100644 index 0000000..81d5473 --- /dev/null +++ b/src/plugins/mpris/mpris.ui @@ -0,0 +1,79 @@ + + + + + + False + 5 + Now Playing Preferences + False + True + center-on-parent + dialog + + + True + False + 2 + + + True + False + end + + + gtk-close + True + True + True + True + + + False + False + 0 + + + + + False + True + end + 0 + + + + + True + False + + + Start visualisation when media is playing + True + True + False + True + + + True + True + 0 + + + + + + + + False + False + 1 + + + + + + button9 + + + diff --git a/src/plugins/nm/Makefile.am b/src/plugins/nm/Makefile.am new file mode 100644 index 0000000..8964a21 --- /dev/null +++ b/src/plugins/nm/Makefile.am @@ -0,0 +1,6 @@ +SUBDIRS = default +plugindir = $(datadir)/gnome15/plugins/nm +plugin_DATA = nm.py + +EXTRA_DIST = \ + $(plugin_DATA) diff --git a/src/plugins/nm/default/Makefile.am b/src/plugins/nm/default/Makefile.am new file mode 100644 index 0000000..a6fb5ae --- /dev/null +++ b/src/plugins/nm/default/Makefile.am @@ -0,0 +1,7 @@ +themedir = $(datadir)/gnome15/plugins/nm/default +theme_DATA = \ + default.svg \ + g19.svg + +EXTRA_DIST = \ + $(theme_DATA) diff --git a/src/plugins/nm/default/default.svg b/src/plugins/nm/default/default.svg new file mode 100644 index 0000000..94dec73 --- /dev/null +++ b/src/plugins/nm/default/default.svg @@ -0,0 +1,91 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + ${title} + + + diff --git a/src/plugins/nm/default/g19.svg b/src/plugins/nm/default/g19.svg new file mode 100644 index 0000000..ecf80b3 --- /dev/null +++ b/src/plugins/nm/default/g19.svg @@ -0,0 +1,166 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + ${title} + + diff --git a/src/plugins/nm/nm.py b/src/plugins/nm/nm.py new file mode 100644 index 0000000..bcd7be5 --- /dev/null +++ b/src/plugins/nm/nm.py @@ -0,0 +1,67 @@ +#!/usr/bin/env python + +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2010 Brett Smith +# +# 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 . + +import gnome15.g15theme as g15theme +import gnome15.g15screen as g15screen +import gnome15.g15driver as g15driver +import os + +# Plugin details - All of these must be provided +id="nm" +name="Network Manager" +description="Displays current status of your network connections." +author="Brett Smith " +copyright="Copyright (C)2010 Brett Smith" +site="http://www.gnome15.org/" +has_preferences=False +unsupported_models = [ g15driver.MODEL_G110 ] + +def create(gconf_key, gconf_client, screen): + return G15NM(gconf_client, gconf_key, screen) + +class MenuItem(): + + def __init__(self, page): + self.page = page + self.thumbnail = None + +class G15NM(): + + def __init__(self, gconf_client, gconf_key, screen): + self.screen = screen + self.gconf_client = gconf_client + self.gconf_key = gconf_key + + def activate(self): + self._reload_theme() + self.page = self.screen.new_page(self.paint, id=id, priority = g15screen.PRI_EXCLUSIVE) + self.screen.redraw(self.page) + + def deactivate(self): + if self.page != None: + self.screen.del_page(self.page) + self.page = None + + def destroy(self): + pass + + def paint(self, canvas): + self.theme.draw(canvas, {}) + + def _reload_theme(self): + self.theme = g15theme.G15Theme(os.path.join(os.path.dirname(__file__), "default"), self.screen) \ No newline at end of file diff --git a/src/plugins/notify-lcd/Makefile.am b/src/plugins/notify-lcd/Makefile.am new file mode 100644 index 0000000..a2b5aa7 --- /dev/null +++ b/src/plugins/notify-lcd/Makefile.am @@ -0,0 +1,7 @@ +SUBDIRS = default +plugindir = $(datadir)/gnome15/plugins/notify-lcd +plugin_DATA = notify-lcd.py \ + notify-lcd.ui + +EXTRA_DIST = \ + $(plugin_DATA) \ No newline at end of file diff --git a/src/plugins/notify-lcd/default/Makefile.am b/src/plugins/notify-lcd/default/Makefile.am new file mode 100644 index 0000000..637f1cc --- /dev/null +++ b/src/plugins/notify-lcd/default/Makefile.am @@ -0,0 +1,10 @@ +themedir = $(datadir)/gnome15/plugins/notify-lcd/default +theme_DATA = g19.svg \ + g19-nobody.svg \ + default.svg \ + default-nobody.svg \ + mx5500.svg \ + mx5500-nobody.svg + +EXTRA_DIST = \ + $(theme_DATA) diff --git a/src/plugins/notify-lcd/default/default-nobody.svg b/src/plugins/notify-lcd/default/default-nobody.svg new file mode 100644 index 0000000..958ce0e --- /dev/null +++ b/src/plugins/notify-lcd/default/default-nobody.svg @@ -0,0 +1,168 @@ + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + L3 ${action1} + L4 Next + ${title} + + diff --git a/src/plugins/notify-lcd/default/default.svg b/src/plugins/notify-lcd/default/default.svg new file mode 100644 index 0000000..41a2a36 --- /dev/null +++ b/src/plugins/notify-lcd/default/default.svg @@ -0,0 +1,196 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + ${message} + ${title} + + + L3 ${action1} + L4 Next + + diff --git a/src/plugins/notify-lcd/default/g19-nobody.svg b/src/plugins/notify-lcd/default/g19-nobody.svg new file mode 100644 index 0000000..c45cb71 --- /dev/null +++ b/src/plugins/notify-lcd/default/g19-nobody.svg @@ -0,0 +1,274 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + ${title} + + + + Next + + + + + Clear + + + + ${action1} + Ok + + + + + diff --git a/src/plugins/notify-lcd/default/g19.svg b/src/plugins/notify-lcd/default/g19.svg new file mode 100644 index 0000000..fded005 --- /dev/null +++ b/src/plugins/notify-lcd/default/g19.svg @@ -0,0 +1,316 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + ${title} + + ${message} + + + + Next + + + + + Clear + + + + ${action1} + Ok + + + + + diff --git a/src/plugins/notify-lcd/default/mx5500-nobody.svg b/src/plugins/notify-lcd/default/mx5500-nobody.svg new file mode 100644 index 0000000..43ccd23 --- /dev/null +++ b/src/plugins/notify-lcd/default/mx5500-nobody.svg @@ -0,0 +1,156 @@ + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + ${title} + > Next + < ${action1} + + + diff --git a/src/plugins/notify-lcd/default/mx5500.svg b/src/plugins/notify-lcd/default/mx5500.svg new file mode 100644 index 0000000..9722e3a --- /dev/null +++ b/src/plugins/notify-lcd/default/mx5500.svg @@ -0,0 +1,184 @@ + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + ${message} + ${title} + + > Next + < ${action1} + + + diff --git a/src/plugins/notify-lcd/i18n/notify-lcd.en_GB.po b/src/plugins/notify-lcd/i18n/notify-lcd.en_GB.po new file mode 100644 index 0000000..6054c09 --- /dev/null +++ b/src/plugins/notify-lcd/i18n/notify-lcd.en_GB.po @@ -0,0 +1,74 @@ +# English translations for gnome15-plugins package. +# Copyright (C) 2012 THE gnome15-plugins'S COPYRIGHT HOLDER +# This file is distributed under the same license as the gnome15-plugins package. +# Automatically generated, 2012. +# +msgid "" +msgstr "" +"Project-Id-Version: gnome15-plugins 0.8.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: 2012-01-08 11:23+0000\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: en_GB\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=ASCII\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: i18n/notify-lcd.glade.h:1 +msgid "Notification" +msgstr "Notification" + +#: i18n/notify-lcd.glade.h:2 +msgid "Options" +msgstr "Options" + +#: i18n/notify-lcd.glade.h:3 +msgid "Allow actions" +msgstr "Allow actions" + +#: i18n/notify-lcd.glade.h:4 +msgid "Allow application to cancel notification" +msgstr "Allow application to cancel notification" + +#: i18n/notify-lcd.glade.h:5 +msgid "Blink keyboard backlight" +msgstr "Blink keyboard backlight" + +#: i18n/notify-lcd.glade.h:6 +msgid "Blink memory bank lights" +msgstr "Blink memory bank lights" + +#: i18n/notify-lcd.glade.h:7 +msgid "Change keyboard backlight colour" +msgstr "Change keyboard backlight colour" + +#: i18n/notify-lcd.glade.h:8 +msgid "Color" +msgstr "Color" + +#: i18n/notify-lcd.glade.h:9 +msgid "Delay" +msgstr "Delay" + +#: i18n/notify-lcd.glade.h:10 +msgid "Enable sounds" +msgstr "Enable sounds" + +#: i18n/notify-lcd.glade.h:11 +msgid "Message on desktop (as normal)" +msgstr "Message on desktop (as normal)" + +#: i18n/notify-lcd.glade.h:12 +msgid "Message on keyboard's screen" +msgstr "Message on keyboard's screen" + +#: i18n/notify-lcd.glade.h:13 +msgid "Notify Preferences" +msgstr "Notify Preferences" + +#: i18n/notify-lcd.glade.h:14 +msgid "Respect requested timeout" +msgstr "Respect requested timeout" diff --git a/src/plugins/notify-lcd/i18n/notify-lcd.glade.h b/src/plugins/notify-lcd/i18n/notify-lcd.glade.h new file mode 100644 index 0000000..8507b6b --- /dev/null +++ b/src/plugins/notify-lcd/i18n/notify-lcd.glade.h @@ -0,0 +1,14 @@ +char *s = N_("Notification"); +char *s = N_("Options"); +char *s = N_("Allow actions"); +char *s = N_("Allow application to cancel notification"); +char *s = N_("Blink keyboard backlight"); +char *s = N_("Blink memory bank lights"); +char *s = N_("Change keyboard backlight colour"); +char *s = N_("Color"); +char *s = N_("Delay"); +char *s = N_("Enable sounds"); +char *s = N_("Message on desktop (as normal)"); +char *s = N_("Message on keyboard's screen"); +char *s = N_("Notify Preferences"); +char *s = N_("Respect requested timeout"); diff --git a/src/plugins/notify-lcd/i18n/notify-lcd.pot b/src/plugins/notify-lcd/i18n/notify-lcd.pot new file mode 100644 index 0000000..f318d4b --- /dev/null +++ b/src/plugins/notify-lcd/i18n/notify-lcd.pot @@ -0,0 +1,74 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: i18n/notify-lcd.glade.h:1 +msgid "Notification" +msgstr "" + +#: i18n/notify-lcd.glade.h:2 +msgid "Options" +msgstr "" + +#: i18n/notify-lcd.glade.h:3 +msgid "Allow actions" +msgstr "" + +#: i18n/notify-lcd.glade.h:4 +msgid "Allow application to cancel notification" +msgstr "" + +#: i18n/notify-lcd.glade.h:5 +msgid "Blink keyboard backlight" +msgstr "" + +#: i18n/notify-lcd.glade.h:6 +msgid "Blink memory bank lights" +msgstr "" + +#: i18n/notify-lcd.glade.h:7 +msgid "Change keyboard backlight colour" +msgstr "" + +#: i18n/notify-lcd.glade.h:8 +msgid "Color" +msgstr "" + +#: i18n/notify-lcd.glade.h:9 +msgid "Delay" +msgstr "" + +#: i18n/notify-lcd.glade.h:10 +msgid "Enable sounds" +msgstr "" + +#: i18n/notify-lcd.glade.h:11 +msgid "Message on desktop (as normal)" +msgstr "" + +#: i18n/notify-lcd.glade.h:12 +msgid "Message on keyboard's screen" +msgstr "" + +#: i18n/notify-lcd.glade.h:13 +msgid "Notify Preferences" +msgstr "" + +#: i18n/notify-lcd.glade.h:14 +msgid "Respect requested timeout" +msgstr "" diff --git a/src/plugins/notify-lcd/notify-lcd.py b/src/plugins/notify-lcd/notify-lcd.py new file mode 100644 index 0000000..515085e --- /dev/null +++ b/src/plugins/notify-lcd/notify-lcd.py @@ -0,0 +1,645 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2010 Brett Smith +# +# 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 . + +import gnome15.g15locale as g15locale +_ = g15locale.get_translation("notify-lcd", modfile = __file__).ugettext + +import gnome15.g15screen as g15screen +import gnome15.util.g15scheduler as g15scheduler +import gnome15.util.g15uigconf as g15uigconf +import gnome15.util.g15gconf as g15gconf +import gnome15.util.g15icontools as g15icontools +import gnome15.util.g15markup as g15markup +import gnome15.g15globals as g15globals +import gnome15.g15theme as g15theme +import gnome15.g15driver as g15driver +import gnome15.g15desktop as g15desktop +import gconf +import time +import dbus +import dbus.service +import dbus.exceptions +import os +import gtk +import gtk.gdk +from PIL import Image +import subprocess +import tempfile +import lxml.html +import Queue +import gobject + +from threading import Timer +from threading import Thread +from threading import RLock +from dbus.exceptions import NameExistsException + +# Logging +import logging +logger = logging.getLogger(__name__) + +# Plugin details - All of these must be provided +id="notify-lcd" +name=_("Notify") +description=_("Displays desktop notification messages on the keyboard's screen (when available), and provides \ +various other methods of notification, such as blinking the keyboard backlight, \ +blinking the M-Key lights, or changing the backlight colour. On some desktops, \ +Gnome15 can completely take over the notification service and display messages \ +on the keyboard only.") +author="Brett Smith " +copyright=_("Copyright (C)2010 Brett Smith") +site="http://www.russo79.com/gnome15" +has_preferences=True +single_instance=True +unsupported_models = [ g15driver.MODEL_G930, g15driver.MODEL_G35 ] +actions={ + g15driver.CLEAR : _("Clear all queued messages"), + g15driver.NEXT_SELECTION : _("Next message"), + g15driver.SELECT : _("Perform action (if appropriate)") + } + +IF_NAME="org.freedesktop.Notifications" +BUS_NAME="/org/freedesktop/Notifications" + +# Match string to use for passive mode +PASSIVE_MATCH_STRING="type='method_call',interface='org.freedesktop.Notifications',member='Notify'" +EAVESDROP_MATCH_STRING="eavesdrop='true',%s" % PASSIVE_MATCH_STRING + +# List of processes to try and kill so the notification DBUS server can be replaced +OTHER_NOTIFY_DAEMON_PROCESS_NAMES = [ 'notify-osd', 'notification-daemon', 'knotify4' ] + +# NotificationClosed reasons +NOTIFICATION_EXPIRED = 1 +NOTIFICATION_DISMISSED = 2 +NOTIFICATION_CLOSED = 3 +NOTIFICATION_UNDEFINED = 4 + +def create(gconf_key, gconf_client, screen): + return G15NotifyLCD(gconf_client, gconf_key, screen) + +def show_preferences(parent, driver, gconf_client, gconf_key): + widget_tree = gtk.Builder() + widget_tree.add_from_file(os.path.join(os.path.dirname(__file__), "notify-lcd.ui")) + dialog = widget_tree.get_object("NotifyLCDDialog") + dialog.set_transient_for(parent) + g15uigconf.configure_checkbox_from_gconf(gconf_client, gconf_key + "/respect_timeout", "RespectTimeout", False, widget_tree, True) + g15uigconf.configure_checkbox_from_gconf(gconf_client, gconf_key + "/allow_actions", "AllowActions", False, widget_tree, True) + g15uigconf.configure_checkbox_from_gconf(gconf_client, gconf_key + "/allow_cancel", "AllowCancel", True, widget_tree, True) + g15uigconf.configure_checkbox_from_gconf(gconf_client, gconf_key + "/on_keyboard_screen", "OnKeyboardScreen", True, widget_tree, True) + g15uigconf.configure_checkbox_from_gconf(gconf_client, gconf_key + "/on_desktop", "OnDesktop", True, widget_tree, True) + g15uigconf.configure_checkbox_from_gconf(gconf_client, gconf_key + "/blink_keyboard_backlight", "BlinkKeyboardBacklight", True, widget_tree, True) + g15uigconf.configure_checkbox_from_gconf(gconf_client, gconf_key + "/blink_memory_bank", "BlinkMemoryBank", True, widget_tree, True) + g15uigconf.configure_checkbox_from_gconf(gconf_client, gconf_key + "/change_keyboard_backlight_color", "ChangeKeyboardBacklightColor", False, widget_tree, True) + g15uigconf.configure_adjustment_from_gconf(gconf_client, gconf_key + "/blink_delay", "DelayAdjustment", 500, widget_tree) + g15uigconf.configure_checkbox_from_gconf(gconf_client, gconf_key + "/enable_sounds", "EnableSounds", True, widget_tree, True) + g15uigconf.configure_colorchooser_from_gconf(gconf_client, gconf_key + "/keyboard_backlight_color", "KeyboardBacklightColor", ( 128, 128, 128 ), widget_tree, None) + + set_available(None, widget_tree) + widget_tree.get_object("ChangeKeyboardBacklightColor").connect("toggled", set_available, widget_tree) + widget_tree.get_object("BlinkKeyboardBacklight").connect("toggled", set_available, widget_tree) + + dialog.run() + dialog.hide() + +def set_available(widget, widget_tree): + widget_tree.get_object("KeyboardBacklightColor").set_sensitive(widget_tree.get_object("ChangeKeyboardBacklightColor").get_active()) + widget_tree.get_object("BlinkDelay").set_sensitive(widget_tree.get_object("BlinkKeyboardBacklight").get_active()) + + +''' +Queued notification message +''' +class G15Message(): + + def __init__(self, id, icon, summary, body, timeout, actions, hints): + self.id = id + self.set_details(icon, summary, body, timeout, actions, hints) + self.original_body = body + self.original_summary = summary + + def set_details(self, icon, summary, body, timeout, actions, hints): + self.icon = icon + self.summary = "None" if summary == None else summary + if body != None and len(body) > 0: + try: + self.body = lxml.html.fromstring(body).text_content() + except Exception as e: + logger.debug("Could not parse body as html", exc_info = e) + self.body = body + else: + self.body = body + self.timeout = timeout +# if timeout <= 0.0: +# timeout = 10.0 + self.timeout = 10.0 + self.actions = [] + i = 0 + if actions != None: + for j in range(0, len(actions), 2): + self.actions.append((actions[j], actions[j + 1])) + self.hints = hints + self.embedded_image = None + + if "image_path" in self.hints: + self.icon = self.hints["image_path"] + + if "image_data" in self.hints: + image_struct = self.hints["image_data"] + img_width = image_struct[0] + img_height = image_struct[1] + img_stride = image_struct[2] + has_alpha = image_struct[3] + bits_per_sample = image_struct[4] + channels = image_struct[5] + buf = "" + for b in image_struct[6]: + buf += chr(b) + + try : + pixbuf = gtk.gdk.pixbuf_new_from_data(buf, gtk.gdk.COLORSPACE_RGB, has_alpha, bits_per_sample, img_width, img_height, img_stride) + fh, self.embedded_image = tempfile.mkstemp(suffix=".png",prefix="notify-lcd") + file = os.fdopen(fh) + file.close() + pixbuf.save(self.embedded_image, "png") + self.icon = None + except Exception as e: + # Sometimes the image data seems to be bad + logger.warning("Failed to decode notification image", exc_info = e) + + if self.embedded_image == None and ( self.icon == None or self.icon == "" ): + self.icon = g15icontools.get_icon_path("dialog-information", 1024) + + def close(self): + if self.embedded_image != None: + os.remove(self.embedded_image) + +''' +DBus service implementing the freedesktop notification specification +''' +class G15NotifyService(dbus.service.Object): + + def __init__(self, gconf_client, gconf_key, screen, bus_name, plugin): + dbus.service.Object.__init__(self, bus_name, BUS_NAME) + self._gconf_client = gconf_client + self._gconf_key = gconf_key + self._screen = screen + self._plugin = plugin + + @dbus.service.method(IF_NAME, in_signature='', out_signature='ssss') + def GetServerInformation(self): + return (g15globals.name, "TT", g15globals.version, "1.1") + + @dbus.service.method(IF_NAME, in_signature='', out_signature='as') + def GetCapabilities(self): + logger.debug("Getting capabilities") + caps = [ "body", "body-images", "icon-static" ] + if self._gconf_client.get_bool(self._gconf_key + "/allow_actions"): + caps.append("actions") + if self._plugin._get_enable_sounds(): + caps.append("sounds") + + logger.debug("Got capabilities %s", str(caps)) + return caps + + @dbus.service.method(IF_NAME, in_signature='susssasa{sv}i', out_signature='u') + def Notify(self, app_name, id, icon, summary, body, actions, hints, timeout): + return self._plugin.notify(app_name, id, icon, summary, body, actions, hints, timeout) + + @dbus.service.method(IF_NAME, in_signature='u', out_signature='') + def CloseNotification(self, id): + logger.info("Close notification %d", id) + self._plugin.close_notification(id) + + @dbus.service.signal(dbus_interface=IF_NAME, + signature='us') + + def ActionInvoked(self, id, action_key): + logger.debug("Sending ActionInvoked for %d, %s", id, action_key) + + @dbus.service.signal(dbus_interface=IF_NAME, + signature='uu') + def NotificationClosed(self, id, reason): + logger.debug("Sending NotificationClosed for %d, %s", id, reason) + +''' +Gnome15 notification plugin +''' +class G15NotifyLCD(): + + def __init__(self, gconf_client,gconf_key, screen): + self._screen = screen; + self._gconf_key = gconf_key + self._session_bus = dbus.SessionBus() + self._gconf_client = gconf_client + self._lock = RLock() + self.id = 1 + + def _load_configuration(self): + self.respect_timeout = g15gconf.get_bool_or_default(self._gconf_client, self._gconf_key + "/respect_timeout", False) + self.allow_actions = g15gconf.get_bool_or_default(self._gconf_client, self._gconf_key + "/allow_actions", False) + self.allow_cancel = g15gconf.get_bool_or_default(self._gconf_client, self._gconf_key + "/allow_cancel", True) + self.on_keyboard_screen = g15gconf.get_bool_or_default(self._gconf_client, self._gconf_key + "/on_keyboard_screen", True) + self.on_desktop = g15gconf.get_bool_or_default(self._gconf_client, self._gconf_key + "/on_desktop", True) + self.blink_keyboard_backlight = g15gconf.get_bool_or_default(self._gconf_client, self._gconf_key + "/blink_keyboard_backlight", True) + self.blink_memory_bank = g15gconf.get_bool_or_default(self._gconf_client, self._gconf_key + "/blink_memory_bank", True) + self.change_keyboard_backlight_color = g15gconf.get_bool_or_default(self._gconf_client, self._gconf_key + "/change_keyboard_backlight_color", False) + self.enable_sounds = g15gconf.get_bool_or_default(self._gconf_client, self._gconf_key + "/enable_sounds", True) + self.blink_delay = g15gconf.get_int_or_default(self._gconf_client, self._gconf_key + "/blink_delay", 500) + self.keyboard_backlight_color = g15gconf.get_rgb_or_default(self._gconf_client, self._gconf_key + "/keyboard_backlight_color", ( 128, 128, 128 )) + + def _get_busname(self, retry=False): + try: + return dbus.service.BusName(IF_NAME, + bus=self._bus, + replace_existing=True, + allow_replacement=True, + do_not_queue=True) + except NameExistsException as E: # name is taken. Look up who owns it + if not retry: return None + try: + proxy = self._bus.get_object('org.freedesktop.DBus', + '/org/freedesktop/DBus') + creds = proxy.GetConnectionCredentials(IF_NAME) + if 'ProcessID' in creds: + pid = creds['ProcessID'] + with open("/proc/%s/cmdline" % (pid,)) as F: + pn = F.read().split('\x00')[0] + if pn in OTHER_NOTIFY_DAEMON_PROCESS_NAMES: + process = subprocess.Popen(['killall', '--quiet', pn]) + if process.wait(): + logger.debug("Process still exists, Waiting one more second") + time.sleep(1.0) + return self._get_busname(retry=False) + else: + logger.debug("BusName is owned by unfamiliar process %s", pn) + except dbus.DBusException as E: + logger.debug("Failed to determine process owning %s", + IF_NAME, exc_info=E) + except IOError as E: + logger.debug("Error trying to retrieve notify daemon process name", + exc_info=E) + except OSError as E: + logger.debug("Error while trying to kill notify daemon", exc_info=E) + return None + + def activate(self): + self._last_variant = None + self._displayed_notification = 0 + self._active = True + self._timer = None + self._redraw_timer = None + self._blink_thread = None + self._control_values = [] + self._message_queue = [] + self._message_map = {} + self._current_message = None + self._service = None + self._load_configuration() + self._notify_handle = None + self._page = None + + # DBUS session instance must be private or monitoring will not work properly + self._bus = dbus.SessionBus(private=True) + + if not self.on_desktop: + # Already running + self._bus_name = self._get_busname() + try: + if self._bus_name: + self._service = G15NotifyService(self._gconf_client, + self._gconf_key, + self._screen, + self._bus_name, + self) + else: + logger.warning("Couldn't obtain BusName. Falling back to snooping.") + except KeyError as e: + logger.error("DBUS notify service failed to start. May already be started.", + exc_info = e) +# + if not self._service: + # Just monitor raw DBUS events + self._match_string = EAVESDROP_MATCH_STRING + try: + self._bus.add_match_string(self._match_string) + logger.info("Using eavesdrop for monitoring DBUS") + except Exception as e: + self._match_string = PASSIVE_MATCH_STRING + self._bus.add_match_string(self._match_string) + logger.info("Not using eavesdrop for monitoring DBUS", exc_info = e) + self._bus.add_message_filter(self.msg_cb) + + + self._screen.key_handler.action_listeners.append(self) + self._notify_handle = self._gconf_client.notify_add(self._gconf_key, self._configuration_changed) + + def msg_cb(self, bus, msg): + # Only interested in method calls + if isinstance(msg, dbus.lowlevel.MethodCallMessage): + if msg.get_member() == "Notify": + self.notify(*msg.get_args_list()) + + def deactivate(self): + # TODO How do we properly 'unexport' a service? This seems to kind of work, in + # that notify-osd can take over again, but trying to re-activate the plugin + # doesn't reclaim the bus name (I think because it is cached) + self.clear() + if self._notify_handle: + self._gconf_client.notify_remove(self._notify_handle) + self._screen.key_handler.action_listeners.remove(self) + if self._service: + if not self._screen.service.shutting_down: + logger.warning("Deactivated notify service. Currently the service cannot be reactivated once deactivated. You must completely restart Gnome15") + self._service.active = False + self._service.remove_from_connection() + self._bus_name.__del__() + del self._bus_name + else: + # Stop monitoring DBUS + self._bus.remove_match_string(self._match_string) + self._bus.remove_message_filter(self.msg_cb) + + def destroy(self): + pass + + def action_performed(self, binding): + if self._page != None and self._page.is_visible(): + if binding.action == g15driver.CLEAR: + self.clear() + elif binding.action == g15driver.NEXT_PAGE: + self.next() + elif binding.action == g15driver.SELECT: + self.action() + + def notify(self, app_name, id, icon, summary, body, actions, hints, timeout): + logger.debug("Notify app=%s id=%s '%s' {%s}", app_name, id, summary, hints) + try : + if self._active: + timeout = float(timeout) / 1000.0 + if not self.respect_timeout: + timeout = 10.0 + if not self._service or not self.allow_actions: + actions = None + + # Check if this notification should be ignored, currently we ignore + # volume change notifications + # TODO should implement volume style notifications properly and deprecate alsa monitor + if "x-canonical-private-synchronous" in hints \ + and ( hints["x-canonical-private-synchronous"] == "volume" or \ + hints["x-canonical-private-synchronous"] == "indicator-sound" ): + return + + # Strip markup + if body: + body = g15markup.strip_tags(body) + if summary: + summary = g15markup.strip_tags(summary) + + if id != 0 and not id in self._message_map: + if len(self._message_queue) > 0: + new_id = self._message_queue[0].id + logger.warning("Got request to replace message %d, " \ + "but we do not know about it. " \ + "Just replacing visible message %d", id, new_id) + id = new_id + else: + id = 0 + + # If a message with this ID is already queued, replace it's details + if id == 0: + # Queue a new message + logger.debug("Queuing new message") + id = self.id + message = G15Message(self.id, icon, summary, body, timeout, actions, hints) + self._message_queue.append(message) + self._message_map[self.id] = message + self.id += 1 + + if len(self._message_queue) == 1: + self._notify() + else: + logger.debug("More than one message in queue, just redrawing") + if self._page != None: + self._screen.redraw(self._page) + else: + if id in self._message_map: + logger.debug("Message %s is already in queue, replacing its details", + str(id)) + message = self._message_map[id] + message.set_details(icon, summary, body, timeout, actions, hints) + + # If this message is the visible one, then reset the timer + if message == self._message_queue[0]: + logger.debug("It is the visible message") + self._start_timer(message) + else: + if self._page != None: + self._screen.redraw(self._page) + + logger.info("Notify message has ID of %s", str(id)) + return id + except Exception as blah: + logger.warning("Could not create notification", exc_info = blah) + + def close_notification(self, id): + logger.info("Closing notification %d. Message queue has %d items, allow cancel is %s", + id, + len(self._message_queue), + str(self.allow_cancel)) + self._lock.acquire() + try : + if self.allow_cancel and len(self._message_queue) > 0: + message = self._message_queue[0] + if message.id == id: + self._cancel_timer() + self._move_to_next(NOTIFICATION_CLOSED) + else: + del self._message_map[id] + for m in self._message_queue: + if m.id == id: + self._message_queue.remove(m) + if self._service: + gobject.idle_add(self._service.NotificationClosed, id, NOTIFICATION_CLOSED) + break + finally : + self._lock.release() + + def clear(self): + self._lock.acquire() + try : + for message in self._message_queue: + message.close() + self._message_queue = [] + self._message_map = {} + self._cancel_timer() + if self._page != None: + self._screen.del_page(self._page) + finally: + self._lock.release() + + def next(self): + logger.debug("User is selected next") + self._cancel_timer() + self._move_to_next() + + def action(self): + self._cancel_timer() + if len(self._message_queue) > 0: + message = self._message_queue[0] + if len(message.actions) > 0: + action = message.actions[0] + if self._service: + logger.debug("Action invoked") + self._service.ActionInvoked(message.id, action[0]) + self._move_to_next() + + ''' + Private + ''' + def _configuration_changed(self, client, connection_id, entry, args): + self._load_configuration() + + def _get_theme_properties(self): + width_available = self._screen.width + properties = {} + properties["title"] = self._current_message.summary + properties["message"] = self._current_message.body + if self._current_message.icon != None and len(self._current_message.icon) > 0: + icon_path = g15icontools.get_icon_path(self._current_message.icon) + + # Workaround on Natty missing new email notification icon (from Evolution)? + if icon_path == None and self._current_message.icon == "notification-message-email": + icon_path = g15icontools.get_icon_path([ "applications-email-pane", "mail_new", "mail-inbox", "mail-folder-inbox", "evolution-mail" ]) + + properties["icon"] = icon_path + elif self._current_message.embedded_image != None: + properties["icon"] = self._current_message.embedded_image + if not "icon" in properties or properties["icon"] == None: + properties["icon"] = g15icontools.get_icon_path(["dialog-info", "stock_dialog-info", "messagebox_info" ]) + + properties["next"] = len(self._message_queue) > 1 + action = 1 + for a in self._current_message.actions: + properties["action%d" % action] = a[1] + action += 1 + if len(self._current_message.actions) > 0: + properties["action"] = True + + time_displayed = time.time() - self._displayed_notification + remaining = self._current_message.timeout - time_displayed + remaining_pc = ( remaining / self._current_message.timeout ) * 100.0 + properties["remaining"] = int(remaining_pc) + return properties + + def _page_deleted(self): + self._page = None + + def _notify(self): + if len(self._message_queue) != 0: + logger.debug("Displaying first message in queue of %d", len(self._message_queue)) + message = self._message_queue[0] + + + # Which theme variant should we use + self._last_variant = "" + if message.body == None or message.body == "": + self._last_variant = "nobody" + + self._current_message = message + + # Get the page + + if self._page == None: + logger.debug("Creating new notification message page") + self._control_values = [] + for c in self._screen.driver.get_controls(): + if c.hint & g15driver.HINT_DIMMABLE != 0: + self._control_values.append(c.value) + + if self._screen.driver.get_bpp() != 0: + logger.debug("Creating notification message page") + self._page = g15theme.G15Page(id, self._screen, priority=g15screen.PRI_HIGH, title = name, \ + theme_properties_callback = self._get_theme_properties, \ + theme = g15theme.G15Theme(self, self._last_variant), + originating_plugin = self) + self._page.on_deleted = self._page_deleted + self._screen.add_page(self._page) + else: + logger.debug("Raising notification message page") + self._page.set_theme(g15theme.G15Theme(self, self._last_variant)) + self._screen.raise_page(self._page) + + self._start_timer(message) + self._do_redraw() + + # Play sound + if self.enable_sounds and "sound-file" in message.hints and ( not "suppress-sound" in message.hints or not message.hints["suppress-sound"]): + logger.debug("Will play sound",message.hints["sound-file"]) + os.system("aplay '%s' &" % message.hints["sound-file"]) + + control = self._screen.driver.get_control_for_hint(g15driver.HINT_DIMMABLE) + if control and self.blink_keyboard_backlight: + acquired_control = self._screen.driver.acquire_control(control, release_after = 3.0, val = self.keyboard_backlight_color if self.change_keyboard_backlight_color else control.value) + acquired_control.blink(delay = self.blink_delay / 1000.0) + elif control and self.change_keyboard_backlight_color: + acquired_control = self._screen.driver.acquire_control(control, release_after = 3.0, val = self.keyboard_backlight_color) + + if self.blink_memory_bank: + acquired_control = self._screen.driver.acquire_control_with_hint(g15driver.HINT_MKEYS, release_after = 3.0, val = g15driver.MKEY_LIGHT_1 | g15driver.MKEY_LIGHT_2 | g15driver.MKEY_LIGHT_3 | g15driver.MKEY_LIGHT_MR) + acquired_control.blink(delay = self.blink_delay / 1000.0) + + def _do_redraw(self): + if self._page != None: + self._screen.redraw(self._page) + self._redraw_timer = g15scheduler.schedule("Notification", self._screen.service.animation_delay, self._do_redraw) + + def _cancel_redraw(self): + if self._redraw_timer != None: + self._redraw_timer.cancel() + + def _cancel_timer(self): + if self._timer != None: + self._timer.cancel() + + def _move_to_next(self, reason = NOTIFICATION_DISMISSED): + logger.debug("Dismissing current message. Reason code %d", reason) + self._lock.acquire() + try : + if len(self._message_queue) > 0: + message = self._message_queue[0] + message.close() + del self._message_queue[0] + del self._message_map[message.id] + if self._service: + self._service.NotificationClosed(message.id, reason) + if len(self._message_queue) != 0: + self._notify() + else: + self._screen.del_page(self._page) + self._page = None + finally: + self._lock.release() + + def _hide_notification(self): + logger.debug("Hiding notification") + self._move_to_next(NOTIFICATION_EXPIRED) + + def _start_timer(self, message): + logger.debug("Starting hide timeout") + self._cancel_timer() + self._displayed_notification = time.time() + self._timer = g15scheduler.schedule("Notification", message.timeout, self._hide_notification) + diff --git a/src/plugins/notify-lcd/notify-lcd.ui b/src/plugins/notify-lcd/notify-lcd.ui new file mode 100644 index 0000000..63ea59e --- /dev/null +++ b/src/plugins/notify-lcd/notify-lcd.ui @@ -0,0 +1,352 @@ + + + + + + 5000 + 10 + 100 + 100 + + + 320 + False + 5 + Notify Preferences + False + True + center-on-parent + dialog + + + True + False + 2 + + + True + False + end + + + gtk-close + True + True + True + True + + + False + False + 0 + + + + + False + True + end + 0 + + + + + True + False + 8 + + + True + False + 0 + none + + + True + False + 8 + 12 + + + True + False + 4 + + + Respect requested timeout + True + True + False + True + + + True + True + 0 + + + + + Allow actions + True + True + False + True + + + True + True + 1 + + + + + Allow application to cancel notification + True + True + False + True + + + True + True + 2 + + + + + + + + + True + False + <b>Options</b> + True + + + + + True + True + 0 + + + + + True + False + 0 + none + + + True + False + 12 + + + True + False + 4 + + + Message on desktop (as normal) + True + True + False + True + + + True + True + 0 + + + + + Message on keyboard's screen + True + True + False + True + + + True + True + 1 + + + + + Blink memory bank lights + True + True + False + True + + + True + True + 2 + + + + + Blink keyboard backlight + True + True + False + True + + + True + True + 3 + + + + + True + False + + + True + False + Delay + + + False + False + 32 + 0 + + + + + True + True + DelayAdjustment + 0 + right + + + True + True + 1 + + + + + True + True + 4 + + + + + Change keyboard backlight colour + True + True + False + True + + + True + True + 5 + + + + + True + False + + + True + False + Color + + + False + False + 32 + 0 + + + + + True + True + True + #000000000000 + + + True + True + 1 + + + + + True + True + 6 + + + + + Enable sounds + True + True + False + True + + + True + True + 7 + + + + + + + + + True + False + <b>Notification</b> + True + + + + + True + True + 1 + + + + + True + True + 1 + + + + + + button9 + + + + + + + + + + + + + diff --git a/src/plugins/notify-lcd2/Makefile.am b/src/plugins/notify-lcd2/Makefile.am new file mode 100644 index 0000000..1ef8931 --- /dev/null +++ b/src/plugins/notify-lcd2/Makefile.am @@ -0,0 +1,7 @@ +SUBDIRS = default +plugindir = $(datadir)/gnome15/plugins/notify-lcd2 +plugin_DATA = notify-lcd2.py \ + notify-lcd.ui + +EXTRA_DIST = \ + $(plugin_DATA) diff --git a/src/plugins/notify-lcd2/default/Makefile.am b/src/plugins/notify-lcd2/default/Makefile.am new file mode 100644 index 0000000..853f1f2 --- /dev/null +++ b/src/plugins/notify-lcd2/default/Makefile.am @@ -0,0 +1,8 @@ +themedir = $(datadir)/gnome15/plugins/notify-lcd2/default +theme_DATA = g19.svg \ + g19-nobody.svg \ + default.svg \ + default-nobody.svg + +EXTRA_DIST = \ + $(theme_DATA) diff --git a/src/plugins/notify-lcd2/default/default-nobody.svg b/src/plugins/notify-lcd2/default/default-nobody.svg new file mode 100644 index 0000000..d953213 --- /dev/null +++ b/src/plugins/notify-lcd2/default/default-nobody.svg @@ -0,0 +1,160 @@ + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + L2 Clear + L3 ${action1} + L4 Next + ${title} + + diff --git a/src/plugins/notify-lcd2/default/default.svg b/src/plugins/notify-lcd2/default/default.svg new file mode 100644 index 0000000..3ed2af0 --- /dev/null +++ b/src/plugins/notify-lcd2/default/default.svg @@ -0,0 +1,176 @@ + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + ${body} + ${title} + + + L2 Clear + L3 ${action1} + L4 Next + + diff --git a/src/plugins/notify-lcd2/default/g19-nobody.svg b/src/plugins/notify-lcd2/default/g19-nobody.svg new file mode 100644 index 0000000..34644b7 --- /dev/null +++ b/src/plugins/notify-lcd2/default/g19-nobody.svg @@ -0,0 +1,258 @@ + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + ${title} + + + + Next + + + + + Clear + + + + ${action1} + Ok + + + + + diff --git a/src/plugins/notify-lcd2/default/g19.svg b/src/plugins/notify-lcd2/default/g19.svg new file mode 100644 index 0000000..758287c --- /dev/null +++ b/src/plugins/notify-lcd2/default/g19.svg @@ -0,0 +1,281 @@ + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + ${title} + + + ${message} + + + + Next + + + + + Clear + + + + ${action1} + Ok + + + + + diff --git a/src/plugins/notify-lcd2/notify-lcd.ui b/src/plugins/notify-lcd2/notify-lcd.ui new file mode 100644 index 0000000..aad14ef --- /dev/null +++ b/src/plugins/notify-lcd2/notify-lcd.ui @@ -0,0 +1,331 @@ + + + + + + 5000 + 10 + 100 + 100 + + + 320 + False + 5 + Notify Preferences + False + True + center-on-parent + dialog + + + True + False + 2 + + + True + False + end + + + gtk-close + True + True + True + True + + + False + False + 0 + + + + + False + True + end + 0 + + + + + True + False + 8 + + + True + False + 0 + none + + + True + False + 8 + 12 + + + True + False + 4 + + + Respect requested timeout + True + True + False + True + + + True + True + 0 + + + + + Allow actions + True + True + False + True + + + True + True + 1 + + + + + Allow application to cancel notification + True + True + False + True + + + True + True + 2 + + + + + + + + + True + False + <b>Options</b> + True + + + + + True + True + 0 + + + + + True + False + 0 + none + + + True + False + 12 + + + True + False + 4 + + + On screen notification only + True + True + False + True + True + + + True + True + 4 + 0 + + + + + Blink Keyboard + True + True + False + True + True + LCDOnly + + + True + True + 4 + 1 + + + + + True + False + + + True + False + Delay + + + False + False + 32 + 0 + + + + + True + True + DelayAdjustment + 0 + right + + + True + True + 1 + + + + + True + True + 2 + + + + + Change keyboard color + True + True + False + True + True + LCDOnly + + + True + True + 3 + + + + + True + False + + + True + False + Color + + + False + False + 32 + 0 + + + + + True + True + True + #000000000000 + + + True + True + 1 + + + + + True + True + 4 + + + + + Enable sounds + True + True + False + True + + + True + True + 5 + + + + + + + + + True + False + <b>Notification</b> + True + + + + + True + True + 1 + + + + + True + True + 1 + + + + + + button9 + + + + + + + + + + + + + diff --git a/src/plugins/notify-lcd2/notify-lcd2.py b/src/plugins/notify-lcd2/notify-lcd2.py new file mode 100644 index 0000000..99e35ed --- /dev/null +++ b/src/plugins/notify-lcd2/notify-lcd2.py @@ -0,0 +1,528 @@ +#!/usr/bin/env python + +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2011 Brett Smith +# +# 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 . + +import os +import sys +import time +import dbus +import dbus.service +import dbus.exceptions +import gtk +import gtk.gdk +from PIL import Image +import subprocess +import tempfile +import lxml.html +import Queue +import gconf + +from threading import Timer +from threading import Thread +from threading import RLock +from dbus.exceptions import NameExistsException + +# run it in a gtk window +if __name__ == "__main__": + + import gobject + from dbus.mainloop.glib import DBusGMainLoop + from dbus.mainloop.glib import threads_init + + gobject.threads_init() + dbus.mainloop.glib.threads_init() + DBusGMainLoop(set_as_default=True) + loop = gobject.MainLoop() + + # Allow running from local path + path = os.path.join(os.path.abspath(os.path.dirname(sys.argv[0])), "..", "..") + if os.path.exists(path): + print "Adding",path,"to python path" + sys.path.insert(0, path) + +import gnome15.g15screen as g15screen +import gnome15.util.g15convert as g15convert +import gnome15.util.g15scheduler as g15scheduler +import gnome15.util.g15uigconf as g15uigconf +import gnome15.util.g15icontools as g15icontools +import gnome15.g15globals as g15globals +import gnome15.g15theme as g15theme +import gnome15.g15driver as g15driver + +# Logging +import gnome15.g15logging as g15logging +if __name__ == "__main__": + logger = g15logging.get_root_logger() +else: + logger = logging.getLogger(__name__) + +# Plugin details - All of these must be provided +id="notify-lcd2" +name="Notify 2" +description="Take over as the Notification daemon and display messages on the LCD" +author="Brett Smith " +copyright="Copyright (C)2010 Brett Smith" +site="http://www.gnome15.org/" +has_preferences=True +unsupported_models = [ g15driver.MODEL_G110 ] +fork=True + +IF_NAME="org.freedesktop.Notifications" +BUS_NAME="/org/freedesktop/Notifications" + +# List of processes to try and kill so the notification DBUS server can be replaced +OTHER_NOTIFY_DAEMON_PROCESS_NAMES = [ 'notify-osd', 'notification-daemon', 'knotify4' ] + +# NotificationClosed reasons +NOTIFICATION_EXPIRED = 1 +NOTIFICATION_DISMISSED = 2 +NOTIFICATION_CLOSED = 3 +NOTIFICATION_UNDEFINED = 4 + +def create(gconf_key, gconf_client, screen): + dbus = dbus.SessionBus() + return G15NotifyLCD(gconf_client, gconf_key, screen, screen.driver ) + +def show_preferences(parent, gconf_client, gconf_key): + widget_tree = gtk.Builder() + widget_tree.add_from_file(os.path.join(os.path.dirname(__file__), "notify-lcd.ui")) + dialog = widget_tree.get_object("NotifyLCDDialog") + dialog.set_transient_for(parent) + g15uigconf.configure_checkbox_from_gconf(gconf_client, gconf_key + "/respect_timeout", "RespectTimeout", False, widget_tree, True) + g15uigconf.configure_checkbox_from_gconf(gconf_client, gconf_key + "/allow_actions", "AllowActions", False, widget_tree, True) + g15uigconf.configure_checkbox_from_gconf(gconf_client, gconf_key + "/allow_cancel", "AllowCancel", False, widget_tree, True) + g15uigconf.configure_checkbox_from_gconf(gconf_client, gconf_key + "/enable_sounds", "EnableSounds", True, widget_tree, True) + g15uigconf.configure_checkbox_from_gconf(gconf_client, gconf_key + "/lcd_only", "LCDOnly", False, widget_tree, True) + g15uigconf.configure_checkbox_from_gconf(gconf_client, gconf_key + "/blink_keyboard", "BlinkKeyboard", False, widget_tree, True) + g15uigconf.configure_checkbox_from_gconf(gconf_client, gconf_key + "/change_keyboard_color", "ChangeKeyboardColor", False, widget_tree, True) + g15uigconf.configure_adjustment_from_gconf(gconf_client, gconf_key + "/blink_delay", "DelayAdjustment", 500, widget_tree) + g15uigconf.configure_colorchooser_from_gconf(gconf_client, gconf_key + "/color", "Color", ( 128, 128, 128 ), widget_tree, None) + + set_available(None, widget_tree) + widget_tree.get_object("ChangeKeyboardColor").connect("toggled", set_available, widget_tree) + widget_tree.get_object("BlinkKeyboard").connect("toggled", set_available, widget_tree) + + dialog.run() + dialog.hide() + +def set_available(widget, widget_tree): + widget_tree.get_object("Color").set_sensitive(widget_tree.get_object("ChangeKeyboardColor").get_active()) + widget_tree.get_object("Delay").set_sensitive(widget_tree.get_object("BlinkKeyboard").get_active()) + + +''' +Queued notification message +''' +class G15Message(): + + def __init__(self, id, icon, summary, body, timeout, actions, hints): + self.id = id + self.set_details(icon, summary, body, timeout, actions, hints) + + def set_details(self, icon, summary, body, timeout, actions, hints): + self.icon = icon + self.summary = "None" if summary == None else summary + if body != None and len(body) > 0: + self.body = lxml.html.fromstring(body).text_content() + else: + self.body = body + self.timeout = timeout +# if timeout <= 0.0: +# timeout = 10.0 + self.timeout = 10.0 + self.actions = [] + i = 0 + if actions != None: + for j in range(0, len(actions), 2): + self.actions.append((actions[j], actions[j + 1])) + self.hints = hints + self.embedded_image = None + + if self.icon == None or self.icon == "": + if "image_data" in self.hints: + image_struct = self.hints["image_data"] + img_width = image_struct[0] + img_height = image_struct[1] + img_stride = image_struct[2] + has_alpha = image_struct[3] + bits_per_sample = image_struct[4] + channels = image_struct[5] + buf = "" + for b in image_struct[6]: + buf += chr(b) + pixbuf = gtk.gdk.pixbuf_new_from_data(buf, gtk.gdk.COLORSPACE_RGB, has_alpha, bits_per_sample, img_width, img_height, img_stride) + fh, self.embedded_image = tempfile.mkstemp(suffix=".png",prefix="notify-lcd") + file = os.fdopen(fh) + file.close() + pixbuf.save(self.embedded_image, "png") + else: + self.icon = g15icontools.get_icon_path("dialog-info", 1024) + + def close(self): + if self.embedded_image != None: + os.remove(self.embedded_image) + +''' +DBus service implementing the freedesktop notification specification +''' +class G15NotifyService(dbus.service.Object): + + def __init__(self, gconf_client, gconf_key, service, driver, bus): + self.id = 1 + self._gconf_client = gconf_client + self._driver = driver + self._bus = bus + self._gconf_key = gconf_key + self._displayed_notification = 0 + self._active = True + self._service = service + self._timer = None + self._redraw_timer = None + self._blink_thread = None + self._control_values = [] + self._message_queue = [] + self._message_map = {} + self._current_message = None + self._page = None + + @dbus.service.method(IF_NAME, in_signature='', out_signature='ssss') + def GetServerInformation(self): + return (g15globals.name, "TT", g15globals.version, "1.1") + + @dbus.service.method(IF_NAME, in_signature='', out_signature='as') + def GetCapabilities(self): + caps = [ "body", "body-images", "icon-static" ] + if self._gconf_client.get_bool(self._gconf_key + "/allow_actions"): + caps.append("actions") + enable_sounds = self._gconf_client.get(self._gconf_key + "/enable_sounds") + if self._get_enable_sounds(): + caps.append("sounds") + + @dbus.service.method(IF_NAME, in_signature='susssasa{sv}i', out_signature='u') + def Notify(self, app_name, id, icon, summary, body, actions, hints, timeout): + try : + if self._active: + timeout = float(timeout) / 1000.0 + if not self._gconf_client.get_bool(self._gconf_key + "/respect_timeout"): + timeout = 10.0 + if not self._gconf_client.get_bool(self._gconf_key + "/allow_actions"): + actions = None + + # If a message with this ID is already queued, replace it's details + if id in self._message_map: + message = self._message_map[id] + message.set_details(icon, summary, body, timeout, actions, hints) + + # If this message is the visible one, then reset the timer + if message == self._message_queue[0]: + self._start_timer(message) + else: + self._do_draw() + else: + # Otherwise queue a new message + message = G15Message(self.id, icon, summary, body, timeout, actions, hints) + self._message_queue.append(message) + self._message_map[self.id] = message + self.id += 1 + + if len(self._message_queue) == 1: + try : + self._notify() + except Exception as blah: + logger.warning("Could not notify", exc_info = blah) + else: + self._do_draw() + + return message.id + except Exception as blah: + logger.warning("Could not notify", exc_info = blah) + + @dbus.service.method(IF_NAME, in_signature='u', out_signature='') + def CloseNotification(self, id): + if self._gconf_client.get_bool(self._gconf_key + "/allow_cancel") and len(self._message_queue) > 0: + message = self._message_queue[0] + if message.id == id: + self._cancel_timer() + self._move_to_next(NOTIFICATION_CLOSED) + else: + del self._message_map[id] + for m in self._message_queue: + if m.id == id: + self._message_queue.remove(m) + self.NotificationClosed(id, NOTIFICATION_CLOSED) + break + + @dbus.service.signal(dbus_interface=IF_NAME, + signature='us') + def ActionInvoked(self, id, action_key): + pass + + @dbus.service.signal(dbus_interface=IF_NAME, + signature='uu') + def NotificationClosed(self, id, reason): + pass + + def clear(self): + self._driver.CancelBlink() + for message in self._message_queue: + message.close() + self._message_queue = [] + self._message_map = {} + self._cancel_timer() + page = self._screen.get_page("NotifyLCD") + if page != None: + self._screen.del_page(page) + + def next(self): + self._cancel_timer() + self._move_to_next() + + def action(self): + self._cancel_timer() + message = self._message_queue[0] + action = message.actions[0] + self.ActionInvoked(message.id, action[0]) + self._move_to_next() + + ''' + Private + ''' + def _get_enable_sounds(self): + enable_sounds = self._gconf_client.get(self._gconf_key + "/enable_sounds") + return enable_sounds == None or enable_sounds.get_bool() + + def _reload_theme(self): + self._page.LoadTheme(os.path.realpath(os.path.join(os.path.dirname(__file__), "default")), self._last_variant) + + def _get_properties(self): + properties = {} + properties["title"] = self._current_message.summary + properties["message"] = self._current_message.body + if self._current_message.icon != None and len(self._current_message.icon) > 0: + properties["icon"] = g15icontools.get_icon_path(self._current_message.icon) + elif self._current_message.embedded_image != None: + properties["icon"] = self._current_message.embedded_image + + if str(len(self._message_queue) > 1): + properties["next"] = "True" + + action = 1 + for a in self._current_message.actions: + properties["action%d" % action] = a[1] + action += 1 + if len(self._current_message.actions) > 0: + properties["action"] = "True" + + properties["remaining"] = str(self._get_remaining()) + + return properties + + def _get_remaining(self): + time_displayed = time.time() - self._displayed_notification + remaining = self._current_message.timeout - time_displayed + remaining_pc = ( remaining / self._current_message.timeout ) * 100.0 + return remaining_pc + + def _get_page(self, id): + sequence_number = self._service.GetPageSequenceNumber(id) + if sequence_number != 0: + return self._bus.get_object('org.gnome15.Gnome15', '/org/gnome15/Page%d' % sequence_number) + + def _notify(self): + if len(self._message_queue) != 0: + message = self._message_queue[0] + + # Which theme variant should we use + self._last_variant = "" + if message.body == None or message.body == "": + self._last_variant = "nobody" + + self._current_message = message + + # Get the page + + if self._page == None: + page_sequence_number = self._service.CreatePage("NotifyLCD", "Notification Message", g15screen.PRI_HIGH) + self._page = self._bus.get_object('org.gnome15.Gnome15', '/org/gnome15/Page%d' % page_sequence_number) + self._reload_theme() + else: + self._reload_theme() + self._page.Raise() + + self._page.SetThemeProperties(self._get_properties()) + self._start_timer(message) + self._do_redraw() + + # Play sound + if self._get_enable_sounds() and "sound-file" in message.hints and ( not "suppress-sound" in message.hints or not message.hints["suppress-sound"]): + print "WARNING: Will play sound",message.hints["sound-file"] + os.system("aplay '%s' &" % message.hints["sound-file"]) + + if self._gconf_client.get_bool(self._gconf_key + "/blink_keyboard"): + delay = gconf_client.get(gconf_key + "/blink_delay") + blink_delay = delay.get_int() if delay != None else 500 + self._driver.BlinkKeyboard(float(blink_delay) / 1000.0, 3.0, []) + if self._gconf_client.get_bool(self._gconf_key + "/change_keyboard_color"): + color = gconf_client.get_string(gconf_key + "/color") + self._driver.BlinkKeyboard(0.0, 3.0, (128, 128, 128) if color == None else g15convert.to_rgb(color)) + + def _do_redraw(self): + if self._page != None: + self._do_draw() + self._page.SetThemeProperty("remaining", str(self._get_remaining())) + self._redraw_timer = g15scheduler.schedule("Notification", 0.1, self._do_redraw) + + def _do_draw(self): + if self._page != None: + self._page.SetThemeProperties(self._get_properties()) + self._page.Redraw() + + def _cancel_redraw(self): + if self._redraw_timer != None: + self._redraw_timer.cancel() + + def _cancel_timer(self): + if self._timer != None: + self._timer.cancel() + + def _move_to_next(self, reason = NOTIFICATION_DISMISSED): + self._cancel_redraw() + message = self._message_queue[0] + message.close() + del self._message_queue[0] + del self._message_map[message.id] + self.NotificationClosed(message.id, reason) + if len(self._message_queue) != 0: + self._notify() + else: + logger.debug("Closing notification page") + self._page.Destroy() + self._page = None + + def _hide_notification(self): + self._move_to_next(NOTIFICATION_EXPIRED) + + def _start_timer(self, message): + self._cancel_timer() + self._displayed_notification = time.time() + self._timer = g15scheduler.schedule("Notification", message.timeout, self._hide_notification) + +''' +Gnome15 notification plugin +''' +class G15NotifyLCD(): + + def __init__(self, gconf_client, gconf_key, service, driver, bus): + self._service = service; + self._bus = bus + self._last_variant = None + self._driver = driver + + self._gconf_key = gconf_key + self._session_bus = dbus.SessionBus() + self._gconf_client = gconf_client + + def activate(self): + # Already running + for i in range(0, 6): + try : + for pn in OTHER_NOTIFY_DAEMON_PROCESS_NAMES: + logger.debug("Killing %s", pn) + process = subprocess.Popen(['killall', '--quiet', pn]) + process.wait() + self._bus_name = dbus.service.BusName(IF_NAME, bus=self._bus, replace_existing=True, allow_replacement=True, do_not_queue=True) + break + except NameExistsException as e: + logger.debug("Process still exists. Waiting one second and retrying to kill", + exc_info = e) + time.sleep(1.0) + if i == 2: + logger.debug("Process still exists after retry.", exc_info = e) + raise + + # Notification Service + self._notification_service = G15NotifyService(self._gconf_client, self._gconf_key, self._service, self._driver, self._bus) + try : + logger.info("Starting notification service %s", IF_NAME) + dbus.service.Object.__init__(self._notification_service, self._bus_name, BUS_NAME) + logger.info("Started notification service %s", IF_NAME) + except KeyError as ke: + logger.warning("DBUS notify service failed to start. May already be started.", + exc_info = ke) + + def deactivate(self): + # TODO How do we properly 'unexport' a service? This seems to kind of work, in + # that notify-osd can take over again, but trying to re-activate the plugin + # doesn't reclaim the bus name (I think because it is cached) + print "WARNING: Deactivated notify service. Note, currently the service cannot be reactivated once deactivated. You must completely restart Gnome15" + self._notification_service.active = False + self._notification_service.remove_from_connection() + self._bus_name.__del__() + del self._bus_name + + def destroy(self): + pass + + def handle_key(self, keys, state, post): + if not post and state == g15driver.KEY_STATE_UP: + page = self._screen.get_page("NotifyLCD") + if page != None: + if g15driver.G_KEY_BACK in keys or g15driver.G_KEY_L3 in keys: + if self._notification_service != None: + self._notification_service.clear() + return True + if g15driver.G_KEY_RIGHT in keys or g15driver.G_KEY_L4 in keys: + if self._notification_service != None: + self._notification_service.next() + return True + if g15driver.G_KEY_OK in keys or g15driver.G_KEY_L5 in keys: + if self._notification_service != None: + self._notification_service.action() + return True + +# run it in a gtk window +if __name__ == "__main__": + + try : + import setproctitle + setproctitle.setproctitle(os.path.basename(os.path.abspath(sys.argv[0]))) + except Exception as e: + logger.debug("setproctitle doesn't seem to be available", exc_info = e) + pass + + + import optparse + parser = optparse.OptionParser() + parser.add_option("-l", "--log", dest="log_level", metavar="INFO,DEBUG,WARNING,ERROR,CRITICAL", + default="warning" , help="Log level") + (options, args) = parser.parse_args() + + if options.log_level != None: + logger.setLevel(g15logging.get_level(options.log_level)) + + bus = dbus.SessionBus() + try : + screen = bus.get_object('org.gnome15.Gnome15', '/org/gnome15/Service') + except dbus.DBusException as e: + logger.error("g15-desktop-service is not running.", exc_info = e) + sys.exit(0) + + plugin = G15NotifyLCD(gconf.client_get_default(), + "/apps/gnome15/plugins/notify-lcd2", screen, + bus.get_object('org.gnome15.Gnome15', '/org/gnome15/Driver'), bus) + plugin.activate() + loop.run() \ No newline at end of file diff --git a/src/plugins/panel/Makefile.am b/src/plugins/panel/Makefile.am new file mode 100644 index 0000000..a0f3d78 --- /dev/null +++ b/src/plugins/panel/Makefile.am @@ -0,0 +1,6 @@ +plugindir = $(datadir)/gnome15/plugins/panel +plugin_DATA = panel.py \ + panel.ui + +EXTRA_DIST = \ + $(plugin_DATA) \ No newline at end of file diff --git a/src/plugins/panel/i18n/panel.en_GB.po b/src/plugins/panel/i18n/panel.en_GB.po new file mode 100644 index 0000000..652ee6d --- /dev/null +++ b/src/plugins/panel/i18n/panel.en_GB.po @@ -0,0 +1,54 @@ +# English translations for gnome15-plugins package. +# Copyright (C) 2012 THE gnome15-plugins'S COPYRIGHT HOLDER +# This file is distributed under the same license as the gnome15-plugins package. +# Automatically generated, 2012. +# +msgid "" +msgstr "" +"Project-Id-Version: gnome15-plugins 0.8.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: 2012-01-08 11:23+0000\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: en_GB\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=ASCII\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: i18n/panel.glade.h:1 +msgid "Bottom" +msgstr "Bottom" + +#: i18n/panel.glade.h:2 +msgid "Colour and opacity" +msgstr "Colour and opacity" + +#: i18n/panel.glade.h:3 +msgid "Left" +msgstr "Left" + +#: i18n/panel.glade.h:4 +msgid "Panel Preferences" +msgstr "Panel Preferences" + +#: i18n/panel.glade.h:5 +msgid "Position" +msgstr "Position" + +#: i18n/panel.glade.h:6 +msgid "Right" +msgstr "Right" + +#: i18n/panel.glade.h:7 +msgid "Size" +msgstr "Size" + +#: i18n/panel.glade.h:8 +msgid "Stretch screen content" +msgstr "Stretch screen content" + +#: i18n/panel.glade.h:9 +msgid "Top" +msgstr "Top" diff --git a/src/plugins/panel/i18n/panel.glade.h b/src/plugins/panel/i18n/panel.glade.h new file mode 100644 index 0000000..e8a5f85 --- /dev/null +++ b/src/plugins/panel/i18n/panel.glade.h @@ -0,0 +1,9 @@ +char *s = N_("Bottom"); +char *s = N_("Colour and opacity"); +char *s = N_("Left"); +char *s = N_("Panel Preferences"); +char *s = N_("Position"); +char *s = N_("Right"); +char *s = N_("Size"); +char *s = N_("Stretch screen content"); +char *s = N_("Top"); diff --git a/src/plugins/panel/i18n/panel.pot b/src/plugins/panel/i18n/panel.pot new file mode 100644 index 0000000..5c6b797 --- /dev/null +++ b/src/plugins/panel/i18n/panel.pot @@ -0,0 +1,54 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: i18n/panel.glade.h:1 +msgid "Bottom" +msgstr "" + +#: i18n/panel.glade.h:2 +msgid "Colour and opacity" +msgstr "" + +#: i18n/panel.glade.h:3 +msgid "Left" +msgstr "" + +#: i18n/panel.glade.h:4 +msgid "Panel Preferences" +msgstr "" + +#: i18n/panel.glade.h:5 +msgid "Position" +msgstr "" + +#: i18n/panel.glade.h:6 +msgid "Right" +msgstr "" + +#: i18n/panel.glade.h:7 +msgid "Size" +msgstr "" + +#: i18n/panel.glade.h:8 +msgid "Stretch screen content" +msgstr "" + +#: i18n/panel.glade.h:9 +msgid "Top" +msgstr "" diff --git a/src/plugins/panel/panel.py b/src/plugins/panel/panel.py new file mode 100644 index 0000000..8e1d10f --- /dev/null +++ b/src/plugins/panel/panel.py @@ -0,0 +1,220 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2010 Brett Smith +# +# 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 . + +import gnome15.g15locale as g15locale +_ = g15locale.get_translation("panel", modfile = __file__).ugettext + +import gnome15.g15screen as g15screen +import gnome15.g15driver as g15driver +import gnome15.util.g15uigconf as g15uigconf +import gnome15.util.g15gconf as g15gconf +import os +import gtk +import cairo + +# Plugin details - All of these must be provided +id="panel" +name=_("Panel") +description=_("Adds a small area at the bottom of the screen for other plugins to add permanent components to.") +author="Brett Smith " +copyright=_("Copyright (C)2010 Brett Smith") +site="http://www.russo79.com/gnome15" +has_preferences=True +unsupported_models = [ g15driver.MODEL_G110, g15driver.MODEL_G11, g15driver.MODEL_MX5500, g15driver.MODEL_G930, g15driver.MODEL_G35 ] + +def create(gconf_key, gconf_client, screen): + return G15Panel(gconf_key, gconf_client, screen) + +def show_preferences(parent, driver, gconf_client, gconf_key): + widget_tree = gtk.Builder() + widget_tree.add_from_file(os.path.join(os.path.dirname(__file__), "panel.ui")) + dialog = widget_tree.get_object("PanelDialog") + dialog.set_transient_for(parent) + g15uigconf.configure_adjustment_from_gconf(gconf_client, gconf_key + "/size", "SizeAdjustment", 24, widget_tree) + g15uigconf.configure_combo_from_gconf(gconf_client, gconf_key + "/position", "PositionCombo", "bottom", widget_tree) + g15uigconf.configure_checkbox_from_gconf(gconf_client, gconf_key + "/stretch", "Stretch", False, widget_tree) + g15uigconf.configure_colorchooser_from_gconf(gconf_client, gconf_key + "/color", "Color", ( 128, 128, 128 ), widget_tree, default_alpha = 128) + dialog.run() + dialog.hide() + +class G15PanelPainter(g15screen.Painter): + + def __init__(self, screen, gconf_client, gconf_key): + g15screen.Painter.__init__(self, g15screen.FOREGROUND_PAINTER, 1000) + self.gconf_client = gconf_client + self.gconf_key = gconf_key + self.screen = screen + + def paint(self, canvas): + panel_height = self._get_panel_size() + position = self._get_panel_position() + + # Panel is in one position on the 1 bit display + if self.screen.driver.get_bpp() == 1: + gap = 1 + inset = 1 + widget_size = panel_height + bg = None + position = "top" + align = "end" + else: + inset = 0 + align = "start" + gap = panel_height / 10.0 + bg = g15gconf.get_cairo_rgba_or_default(self.gconf_client, self.gconf_key + "/color", ( 128, 128, 128, 128 )) + widget_size = panel_height - ( gap * 2 ) + + # Paint the panel in memory first so it can be aligned easily + if position == "top" or position == "bottom": + panel_img = cairo.ImageSurface(cairo.FORMAT_ARGB32, self.screen.width, panel_height) + else: + panel_img = cairo.ImageSurface(cairo.FORMAT_ARGB32, panel_height, self.screen.height) + panel_canvas = cairo.Context (panel_img) + self.screen.configure_canvas(panel_canvas) + + actual_size = 0 + if position == "top" or position == "bottom": + panel_canvas.translate(0, gap) + for page in self.screen.pages: + if page != self.screen.get_visible_page() and page.panel_painter != None: + if actual_size > 0: + panel_canvas.translate(inset + gap, 0) + actual_size += inset + gap + panel_canvas.save() + panel_canvas.set_source_rgb(*self.screen.driver.get_color_as_ratios(g15driver.HINT_FOREGROUND, ( 0, 0, 0 ))) + taken_up = page.panel_painter(panel_canvas, widget_size, True) + panel_canvas.restore() + if taken_up != None: + panel_canvas.translate(taken_up, 0) + actual_size += taken_up + else: + panel_canvas.translate(gap, 0) + for page in self.screen.pages: + if page != self.screen.get_visible_page() and page.panel_painter != None: + if actual_size > 0: + panel_canvas.translate(0, inset + gap) + actual_size += inset + gap + panel_canvas.save() + panel_canvas.set_source_rgb(*self.screen.driver.get_color_as_ratios(g15driver.HINT_FOREGROUND, ( 0, 0, 0 ))) + taken_up = page.panel_painter(panel_canvas, widget_size, False) + panel_canvas.restore() + if taken_up != None: + panel_canvas.translate(0, taken_up) + actual_size += taken_up + + # Position the panel + canvas.save() + + if position == "bottom": + canvas.translate(0 if align == "start" else self.screen.width - actual_size - gap, self.screen.height - panel_height) + elif position == "right": + canvas.translate(self.screen.width - panel_height, 0 if align == "start" else self.screen.height - actual_size - gap) + elif position == "top": + canvas.translate(0 if align == "start" else self.screen.width - actual_size - gap, 0) + elif position == "left": + canvas.translate(0, 0 if align == "start" else self.screen.height - actual_size - gap) + + # Paint background + if bg != None: + canvas.set_source_rgba(*bg) + if position == "top" or position == "bottom": + canvas.rectangle(0, 0, self.screen.width, panel_height) + else: + canvas.rectangle(0, 0, panel_height, self.screen.height) + canvas.fill() + + # Now actually paint the panel + canvas.set_source_surface(panel_img) + canvas.paint() + canvas.restore() + + """ + Private + """ + + def _get_panel_size(self): + # Panel is fixed size on the 1 bit display + if self.screen.driver.get_bpp() == 1: + return 8 + + panel_size = self.gconf_client.get_int(self.gconf_key + "/size") + if panel_size == 0: + panel_size = 24 + return panel_size + + def _get_panel_position(self): + panel_pos = self.gconf_client.get_string(self.gconf_key + "/position") + if panel_pos == None or panel_pos == "": + panel_pos = "bottom" + return panel_pos + + +class G15Panel(): + def __init__(self, gconf_key, gconf_client, screen): + self.screen = screen + self.gconf_client = gconf_client + self.gconf_key = gconf_key + + def activate(self): + self.painter = G15PanelPainter(self.screen, self.gconf_client, self.gconf_key) + self.screen.painters.append(self.painter) + self.notify_handle = self.gconf_client.notify_add(self.gconf_key, self._config_changed); + self._set_available_screen_size() + self.screen.redraw() + + def deactivate(self): + self.screen.painters.remove(self.painter) + self.gconf_client.notify_remove(self.notify_handle); + self.screen.set_available_size((0, 0, self.screen.width, self.screen.height)) + self.screen.redraw() + + def destroy(self): + pass + + """ + Private + """ + + def _config_changed(self, client, connection_id, entry, args): + self._set_available_screen_size() + self.screen.redraw() + + def _set_available_screen_size(self): + # Scaling of any sort on the 1 bit display is a bit pointless + if self.screen.driver.get_bpp() == 1: + return + + x = 0 + y = 0 + pos = self.painter._get_panel_position() + panel_height = self.painter._get_panel_size() + stretch = self.gconf_client.get_bool(self.gconf_key + "/stretch") + + if pos == "bottom" or pos == "top": + scale = float( self.screen.height - panel_height ) / float(self.screen.height) + if not stretch: + x = ( float(self.screen.width) - float(self.screen.width * scale ) ) / 2.0 + if pos == "top": + y = panel_height + self.screen.set_available_size((x, y, self.screen.width, self.screen.height - panel_height)) + + if pos == "left" or pos == "right": + scale = float( self.screen.width - panel_height ) / float(self.screen.width) + if not stretch: + y = ( float(self.screen.height) - float(self.screen.height * scale ) ) / 2.0 + if pos == "left": + x = panel_height + self.screen.set_available_size((x, y, self.screen.width - panel_height, self.screen.height)) \ No newline at end of file diff --git a/src/plugins/panel/panel.ui b/src/plugins/panel/panel.ui new file mode 100644 index 0000000..73e79be --- /dev/null +++ b/src/plugins/panel/panel.ui @@ -0,0 +1,213 @@ + + + + + + 100 + 1 + 10 + 10 + + + + + + + + + + + top + Top + + + bottom + Bottom + + + left + Left + + + right + Right + + + + + False + 5 + Panel Preferences + False + True + center-on-parent + dialog + + + True + False + 2 + + + True + False + end + + + gtk-close + True + True + True + True + + + False + False + 0 + + + + + False + True + end + 0 + + + + + True + False + + + True + False + 3 + 2 + 8 + 4 + + + True + False + 0 + Colour and opacity + + + + + True + False + 0 + Size + + + 1 + 2 + + + + + True + True + + False + False + True + True + SizeAdjustment + + + 1 + 2 + 1 + 2 + GTK_FILL + + + + + True + False + 0 + Position + + + 2 + 3 + + + + + True + False + PositionModel + + + + 1 + + + + + 1 + 2 + 2 + 3 + GTK_FILL + + + + + True + True + True + #000000000000 + + + 1 + 2 + GTK_FILL + + + + + True + True + 0 + + + + + Stretch screen content + True + True + False + True + + + False + False + 12 + 1 + + + + + False + False + 1 + + + + + + button9 + + + + 200 + 1 + 10 + + diff --git a/src/plugins/pommodoro/Machovka_tomato.png b/src/plugins/pommodoro/Machovka_tomato.png new file mode 100644 index 0000000..2526f57 Binary files /dev/null and b/src/plugins/pommodoro/Machovka_tomato.png differ diff --git a/src/plugins/pommodoro/Machovka_tomato_green.png b/src/plugins/pommodoro/Machovka_tomato_green.png new file mode 100644 index 0000000..7a2b23e Binary files /dev/null and b/src/plugins/pommodoro/Machovka_tomato_green.png differ diff --git a/src/plugins/pommodoro/Makefile.am b/src/plugins/pommodoro/Makefile.am new file mode 100644 index 0000000..bdd2741 --- /dev/null +++ b/src/plugins/pommodoro/Makefile.am @@ -0,0 +1,13 @@ +SUBDIRS = default + +plugindir = $(datadir)/gnome15/plugins/pommodoro +plugin_DATA = \ + Machovka_tomato.png \ + Machovka_tomato_green.png \ + pommodoro.py \ + pommodoro.ui \ + tomato_1bpp.png \ + tomato_empty_1bpp.png + +EXTRA_DIST = \ + $(plugin_DATA) \ No newline at end of file diff --git a/src/plugins/pommodoro/default/Makefile.am b/src/plugins/pommodoro/default/Makefile.am new file mode 100644 index 0000000..1d7bc46 --- /dev/null +++ b/src/plugins/pommodoro/default/Makefile.am @@ -0,0 +1,28 @@ +themedir = $(datadir)/gnome15/plugins/pommodoro/default +theme_DATA = \ + default.svg \ + default-timerover.svg \ + g19.svg \ + g19-timerover.svg + +EXTRA_DIST = \ + $(theme_DATA) + +all-local: + for M_LOCALE in @ENABLED_LOCALES@; do \ + mkdir -p i18n/$$M_LOCALE/LC_MESSAGES ; \ + if [ `ls i18n/*.po 2>/dev/null|wc -l` -gt 0 ]; then \ + for M_PO in i18n/*.po; do \ + BN=`basename $$M_PO .po`; \ + LL=`basename $$BN .$$M_LOCALE`.mo; \ + echo "$$M_PO -> $$LL"; \ + msgfmt $$M_PO --output-file i18n/$$M_LOCALE/LC_MESSAGES/$$LL; \ + done; \ + fi; \ + done + +install-exec-hook: + for M_LOCALE in @ENABLED_LOCALES@; do \ + mkdir -p $(DESTDIR)$(datadir)/gnome15/plugins/pommodoro/default/i18n; \ + cp -pR i18n/$$M_LOCALE $(DESTDIR)$(datadir)/gnome15/plugins/pommodoro/default/i18n; \ + done \ No newline at end of file diff --git a/src/plugins/pommodoro/default/default-timerover.svg b/src/plugins/pommodoro/default/default-timerover.svg new file mode 100644 index 0000000..76da1be --- /dev/null +++ b/src/plugins/pommodoro/default/default-timerover.svg @@ -0,0 +1,161 @@ + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + ${message} + ${timer} + L2 + L5 + Continue + Stop + + diff --git a/src/plugins/pommodoro/default/default.svg b/src/plugins/pommodoro/default/default.svg new file mode 100644 index 0000000..fd8fdb7 --- /dev/null +++ b/src/plugins/pommodoro/default/default.svg @@ -0,0 +1,202 @@ + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + ${action} + ${timer} + L2 + ${count} + L5 + Start/Stop + Done + Reset + + + + diff --git a/src/plugins/pommodoro/default/g19-timerover.svg b/src/plugins/pommodoro/default/g19-timerover.svg new file mode 100644 index 0000000..009f665 --- /dev/null +++ b/src/plugins/pommodoro/default/g19-timerover.svg @@ -0,0 +1,368 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + Continue + + + OK + + Stop + ${timer} + ${message} + + + + + + + + + diff --git a/src/plugins/pommodoro/default/g19.svg b/src/plugins/pommodoro/default/g19.svg new file mode 100644 index 0000000..8e5cb77 --- /dev/null +++ b/src/plugins/pommodoro/default/g19.svg @@ -0,0 +1,449 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + ${timer} + Start / Stop + + + OK + + + + + + + + + Reset + ${count} + x + + + + + + + + + + ${action} + Done + + diff --git a/src/plugins/pommodoro/pommodoro.py b/src/plugins/pommodoro/pommodoro.py new file mode 100644 index 0000000..79a4398 --- /dev/null +++ b/src/plugins/pommodoro/pommodoro.py @@ -0,0 +1,525 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2013 Nuno Aruajo +# +# 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 . + +""" +Pommodoro timer plugin for Gnome15. +This plugin allows a user to apply the Pommodoro Technique to manage their time. +""" + +import gnome15.g15locale as g15locale +_ = g15locale.get_translation("pommodoro", modfile = __file__).ugettext + +import gnome15.g15screen as g15screen +import gnome15.g15theme as g15theme +import gnome15.util.g15scheduler as g15scheduler +import gnome15.util.g15pythonlang as g15pythonlang +import gnome15.util.g15gconf as g15gconf +import gnome15.util.g15uigconf as g15uigconf +import gnome15.g15driver as g15driver +import gnome15.g15globals as g15globals +import gnome15.g15text as g15text +import gnome15.g15plugin as g15plugin +import gnome15.util.g15cairo as g15cairo +import gnome15.util.g15scheduler as g15scheduler +import datetime +import gtk +import pango +import os +import locale + +import logging +logger = logging.getLogger(__name__) + +# Plugin details - All of these must be provided +id="pommodoro" +name=_("Pommodoro Timer") +description=_("A Pommodoro Timer.\n" \ + "The Pomodoro Technique is an " \ + "amazing way to get the most out of your work day - breaking up your time into " \ + "manageable sections lets you focus more on the task, and accomplish more!") +author="Nuno Araujo " +copyright=_("Copyright (C) 2013 Nuno Araujo") +site="http://www.russo79.com/gnome15" +has_preferences=True +unsupported_models = [g15driver.MODEL_G110, + g15driver.MODEL_G11, + g15driver.MODEL_G930, + g15driver.MODEL_G35] +actions={ + g15driver.SELECT : _("Start / Stop Pommodoro"), + g15driver.CLEAR : _("Reset finished pommodoro counter"), + g15driver.VIEW : _("Cancel Pommodoro") + } +actions_g19={ + g15driver.SELECT : _("Start / Stop Pommodoro"), + g15driver.CLEAR : _("Reset finished pommodoro counter"), + g15driver.VIEW : _("Cancel Pommodoro") + } + + +DEFAULT_WORK_DURATION = 25 # [min] +DEFAULT_SHORTBREAK_DURATION = 5 # [min] +DEFAULT_LONGBREAK_DURATION = 15 # [min] + +def create(gconf_key, gconf_client, screen): + return G15PommodoroPlugin(gconf_key, gconf_client, screen) + +def show_preferences(parent, driver, gconf_client, gconf_key): + widget_tree = gtk.Builder() + widget_tree.add_from_file(os.path.join(os.path.dirname(__file__), "pommodoro.ui")) + + dialog = widget_tree.get_object("PommodoroPreferencesDialog") + dialog.set_transient_for(parent) + + g15uigconf.configure_adjustment_from_gconf(gconf_client, + "{0}/work_duration".format(gconf_key), + "WorkDuration", + DEFAULT_WORK_DURATION, + widget_tree) + g15uigconf.configure_adjustment_from_gconf(gconf_client, + "{0}/shortbreak_duration".format(gconf_key), + "ShortBreakDuration", + DEFAULT_SHORTBREAK_DURATION, + widget_tree) + g15uigconf.configure_adjustment_from_gconf(gconf_client, + "{0}/longbreak_duration".format(gconf_key), + "LongBreakDuration", + DEFAULT_LONGBREAK_DURATION, + widget_tree) + dialog.run() + dialog.hide() + + +class PommodoroTimer: + ''' + PommodoroTimer is a state machine with three main states: + STOPPED : No activity is taking place + RUNNING : An activity is taking place + WAITING : The time allocate to the activity is finished and the timer is waiting for a + command either to start the next activity or to stop. + + When in RUNNING state, three different activities are timed: + WORKING : The user is currently running a pommodoro + SHORT_PAUSING : The pommodoro is over and the user is taking a short break + LONG_PAUSING : The user is taking a long break each 4 finished pommodoros + The duration of these activities are configured by three parameters (work_duration, + shortbreak_duration and longbreak_duration). + + Switching to RUNNING and STOPPED states is made by calling respecively the start and stop + methods. + + Switching from the WAITING state to the RUNNING state is made by calling the go_on method. + + Switching from the RUNNING state to the WAITING state happens automatically when a activity + finishes. This is managed by a g15scheduler queue timer. + + If setup, the method assigned to on_state_change method is called each time the state of + PommodoroTimer changes. + + PommodoroTimer also counts the number of times the WORKING activity was finished in a counter. + ''' + + # States + STOPPED = 0 + RUNNING = 1 + WAITING = 2 + + #Activities + WORKING = 1 + SHORT_PAUSING = 2 + LONG_PAUSING = 3 + + NUMBER_OF_POMMODOROS_BEFORE_LONG_PAUSE = 4 + + def __init__(self): + self.work_duration = DEFAULT_WORK_DURATION + self.shortbreak_duration = DEFAULT_SHORTBREAK_DURATION + self.longbreak_duration = DEFAULT_LONGBREAK_DURATION + + self._count = 0 + + self._state = PommodoroTimer.STOPPED + self._activity = PommodoroTimer.WORKING + + self._state_change_timer = None + self._started_at = None + self._timer_value = self._minutes_to_timedelta(self.work_duration) + + self.on_state_change = None + self.on_count_change = None + + def start(self): + ''' + Start the timer + ''' + if self._state == PommodoroTimer.STOPPED: + self._state_next() + + def stop(self): + ''' + Stop the timer + ''' + if self._state in [PommodoroTimer.WAITING, PommodoroTimer.RUNNING]: + self._destroy_state_change_timer() + self._timer_value = self._minutes_to_timedelta(self.work_duration) + self._state = PommodoroTimer.STOPPED + self._activity = PommodoroTimer.WORKING + self._signal_state_change() + self._log_pommodoro_state() + + def go_on(self): + ''' + Continue the timer when in WAITING state + ''' + if self._state == PommodoroTimer.WAITING: + self._state_next() + + def init_count_at(self, value): + self._count = value + self._signal_count_change() + + def count_reset(self): + ''' + Resets the finished pommodoros counter + ''' + self._count = 0 + self._signal_count_change() + + def recalculate(self): + ''' + Recalculate the timer schedulers. + This method should be called when changes are made to any of the fields managing the + the activity durations (work_duration, shortbreak_duration or longbreak_duration) + ''' + + # Update the _timer_value to a new value depending on the activity. + if self._state in [PommodoroTimer.RUNNING, PommodoroTimer.STOPPED]: + # We don't set the new values if the timer is finished (WAITING). + if self._activity == PommodoroTimer.WORKING: + self._timer_value = self._minutes_to_timedelta(self.work_duration) + elif self._activity == PommodoroTimer.SHORT_PAUSING: + self._timer_value = self._minutes_to_timedelta(self.shortbreak_duration) + elif self._activity == PommodoroTimer.LONG_PAUSING: + self._timer_value = self._minutes_to_timedelta(self.longbreak_duration) + + # If the timer is running, reschedule the state change timer + if self._state == PommodoroTimer.RUNNING: + next_schedule = max(0, (self._timer_value - self._elapsed_time()).total_seconds()) + self._schedule_next_state(next_schedule) + logger.info("Scheduled next state change in %s", str(next_schedule)) + + @property + def state(self): + ''' + Returns the current state of the pommodoro timer + ''' + return self._state + + @property + def activity(self): + ''' + Returns the current activity + ''' + return self._activity + + @property + def timer_value(self): + ''' + Returns the current timer maximum value + ''' + return self._timer_value + + @property + def value(self): + ''' + Returns the current timer remaining time. + Note, this value can be less than 0 if the timer has elapsed. + ''' + if self._state == PommodoroTimer.STOPPED: + return self._timer_value + else: + return self._timer_value + self._started_at - datetime.datetime.now() + + @property + def started_at(self): + ''' + Returns the time at which the last activity started + ''' + return self._started_at + + @property + def count(self): + ''' + Returns the number of finished pommodoros + ''' + return self._count + + ''' + Private methods + ''' + def _elapsed_time(self): + return datetime.datetime.now() - self._started_at + + def _state_next(self): + ''' + Switch to next state within 'normal' workflow + ''' + logger.debug("Switching to next state") + if self._state == PommodoroTimer.STOPPED: + # Start pommodoro timer + self._schedule_next_state(self._timer_value.total_seconds()) + self._started_at = datetime.datetime.now() + self._state = PommodoroTimer.RUNNING + self._activity = PommodoroTimer.WORKING + + elif self._state == PommodoroTimer.RUNNING: + # Timer over, go to waiting state + self._destroy_state_change_timer() + if self._activity == PommodoroTimer.WORKING: + self._count_increase() + self._state = PommodoroTimer.WAITING + + elif self._state == PommodoroTimer.WAITING: + # User accepted to continue, cycle activity and go to RUNNING state + if self._activity == PommodoroTimer.WORKING: + # Start pause if we were working + if self._count % PommodoroTimer.NUMBER_OF_POMMODOROS_BEFORE_LONG_PAUSE == 0: + # If 4 pommodoros were completed, start long pause + self._timer_value = self._minutes_to_timedelta(self.longbreak_duration) + self._activity = PommodoroTimer.LONG_PAUSING + else: + # Start short pause + self._timer_value = self._minutes_to_timedelta(self.shortbreak_duration) + self._activity = PommodoroTimer.SHORT_PAUSING + else: + # Start working if we were in pause + self._timer_value = self._minutes_to_timedelta(self.work_duration) + self._activity = PommodoroTimer.WORKING + self._schedule_next_state(self._timer_value.total_seconds()) + self._started_at = datetime.datetime.now() + self._state = PommodoroTimer.RUNNING + self._signal_state_change() + self._log_pommodoro_state() + + def _signal_state_change(self): + if self.on_state_change is not None: + self.on_state_change() + + def _log_pommodoro_state(self): + logger.info("Switched to state {0} - {1}".format(self._state, self._activity)) + + def _schedule_next_state(self, when): + self._destroy_state_change_timer() + self._state_change_timer = g15scheduler.schedule("PommodoroTimerStateChange", + when, + self._state_next) + + def _destroy_state_change_timer(self): + if self._state_change_timer is not None: + self._state_change_timer.cancel() + self._state_change_timer = None + + def _minutes_to_timedelta(self, minutes): + return datetime.timedelta(0, 0, 0, 0, minutes) + + def _count_increase(self): + self._count += 1 + self._signal_count_change() + + def _signal_count_change(self): + if self.on_count_change is not None: + self.on_count_change() + + +class G15PommodoroPlugin(g15plugin.G15RefreshingPlugin): + + def __init__(self, gconf_key, gconf_client, screen): + g15plugin.G15RefreshingPlugin.__init__(self, + gconf_client, + gconf_key, + screen, + self._get_icon_path("Machovka_tomato.png"), + id, + name) + self.waiting_image = \ + g15cairo.load_surface_from_file(self._get_icon_path("Machovka_tomato.png")) + self.running_image = \ + g15cairo.load_surface_from_file(self._get_icon_path("Machovka_tomato_green.png")) + self.waiting_image_1bpp = \ + g15cairo.load_surface_from_file(self._get_icon_path("tomato_empty_1bpp.png")) + self.running_image_1bpp = \ + g15cairo.load_surface_from_file(self._get_icon_path("tomato_1bpp.png")) + + self.pommodoro_timer = PommodoroTimer() + self.pommodoro_timer.on_state_change = self.timer_state_changed + self.pommodoro_timer.on_count_change = self.pommodoro_count_save + self._load_configuration() + + def activate(self): + self._load_configuration() + self.pommodoro_timer.stop() + g15plugin.G15RefreshingPlugin.activate(self) + self.screen.key_handler.action_listeners.append(self) + self.watch([self._get_configuration_key("work_duration"), + self._get_configuration_key("shortbreak_duration"), + self._get_configuration_key("longbreak_duration")], self._config_changed) + + def deactivate(self): + self.pommodoro_timer.stop() + self.screen.key_handler.action_listeners.remove(self) + g15plugin.G15RefreshingPlugin.deactivate(self) + + def action_performed(self, binding): + if not (self.page and self.page.is_visible()): + # Return if we are not displayed on screen + return + + if binding.action == g15driver.SELECT: + if self.pommodoro_timer.state == PommodoroTimer.STOPPED: + self.pommodoro_timer.start() + elif self.pommodoro_timer.state == PommodoroTimer.WAITING: + self.pommodoro_timer.go_on() + else: + self.pommodoro_timer.stop() + elif binding.action == g15driver.VIEW: + if self.pommodoro_timer.state == PommodoroTimer.WAITING: + self.pommodoro_timer.stop() + elif binding.action == g15driver.CLEAR: + self.pommodoro_timer.count_reset() + + def _paint_panel(self, canvas, allocated_size, horizontal): + # Nothing to paint if the timer is stopped + if self.pommodoro_timer.state == PommodoroTimer.STOPPED: + return + + # Nothing to paint if the page is visible + if not self.page or (self.page and self.page.is_visible()): + return + + if self.screen.driver.get_bpp() == 1: + if self.pommodoro_timer.state == PommodoroTimer.WAITING: + size = g15cairo.paint_thumbnail_image(allocated_size, + self.waiting_image_1bpp, + canvas) + elif self.pommodoro_timer.state == PommodoroTimer.RUNNING: + size = g15cairo.paint_thumbnail_image(allocated_size, + self.running_image_1bpp, + canvas) + else: + if self.pommodoro_timer.state == PommodoroTimer.WAITING: + size = g15cairo.paint_thumbnail_image(allocated_size, self.waiting_image, canvas) + elif self.pommodoro_timer.state == PommodoroTimer.RUNNING: + size = g15cairo.paint_thumbnail_image(allocated_size, self.running_image, canvas) + + return size + + def get_theme_properties(self): + properties = { } + + properties["timer"] = self._format_timer_value_for_display() + + properties["pommodoro_timer"] = self._get_progress_in_percent() + + if self.pommodoro_timer.activity == PommodoroTimer.WORKING: + properties["action"] = "Work" + elif self.pommodoro_timer.activity == PommodoroTimer.SHORT_PAUSING: + properties["action"] = "Small break" + elif self.pommodoro_timer.activity == PommodoroTimer.LONG_PAUSING: + properties["action"] = "Long break" + + properties["count"] = str(self.pommodoro_timer.count) + + if self.pommodoro_timer.state == PommodoroTimer.WAITING: + if self.pommodoro_timer.activity == PommodoroTimer.WORKING: + properties["message"] = "Time for a break" + else: + properties["message"] = "Break's over!" + else: + properties["message"] = "" + + return properties + + def timer_state_changed(self): + self._reload_theme() + # Raise the page for 10 seconds if a activity has just finished (state went to WAITING) + if self.pommodoro_timer.state == PommodoroTimer.WAITING \ + and self.page is not None \ + and self.page.theme is not None: + self.screen.set_priority(self.page, g15screen.PRI_HIGH, revert_after = 10.0) + + + def pommodoro_count_save(self): + pommodoro_count_conf_key = self._get_configuration_key("pommodoro_count") + self.gconf_client.set_int(pommodoro_count_conf_key, self.pommodoro_timer.count) + + def _format_timer_value_for_display(self): + total_seconds = int(self.pommodoro_timer.value.total_seconds()) + if total_seconds > 0: + return str(datetime.timedelta(0, total_seconds)) + else: + x = int((datetime.datetime.now() \ + - self.pommodoro_timer.started_at \ + - self.pommodoro_timer.timer_value).total_seconds()) + return "- {0}".format(str(datetime.timedelta(0, x))) + + def _get_progress_in_percent(self): + return 100 - int(self.pommodoro_timer.value.total_seconds() \ + / self.pommodoro_timer.timer_value.total_seconds() \ + * 100) + + def _config_changed(self, client, connection_id, entry, args): + self._load_configuration() + self.screen.set_priority(self.page, g15screen.PRI_HIGH, revert_after = 3.0) + + def _load_configuration(self): + work_duration_conf_key = self._get_configuration_key("work_duration") + self.pommodoro_timer.work_duration = g15gconf.get_int_or_default(self.gconf_client, + work_duration_conf_key, + DEFAULT_WORK_DURATION) + + shortbreak_conf_key = self._get_configuration_key("shortbreak_duration") + self.pommodoro_timer.shortbreak_duration = \ + g15gconf.get_int_or_default(self.gconf_client, + shortbreak_conf_key, + DEFAULT_SHORTBREAK_DURATION) + + longbreak_conf_key = self._get_configuration_key("longbreak_duration") + self.pommodoro_timer.longbreak_duration = \ + g15gconf.get_int_or_default(self.gconf_client, + longbreak_conf_key, + DEFAULT_LONGBREAK_DURATION) + + pommodoro_count_conf_key = self._get_configuration_key("pommodoro_count") + self.pommodoro_timer.init_count_at(g15gconf.get_int_or_default(self.gconf_client, + pommodoro_count_conf_key, + 0)) + self.pommodoro_timer.recalculate() + + def _get_configuration_key(self, key_name): + ''' + Returns the full gconf key name for the relative key_name passed as parameter + ''' + return "{0}/{1}".format(self.gconf_key, key_name) + + def _reload_theme(self): + variant = None + if self.pommodoro_timer.state == PommodoroTimer.WAITING: + variant = "timerover" + if self.page is not None and self.page.theme is not None: + self.page.theme.set_variant(variant) + + def _get_icon_path(self, name): + return os.path.join(os.path.dirname(__file__), name) diff --git a/src/plugins/pommodoro/pommodoro.ui b/src/plugins/pommodoro/pommodoro.ui new file mode 100644 index 0000000..345f785 --- /dev/null +++ b/src/plugins/pommodoro/pommodoro.ui @@ -0,0 +1,206 @@ + + + + + + 1 + 999999 + 15 + 1 + 10 + + + 1 + 99999 + 5 + 1 + 10 + + + 1 + 999999 + 25 + 1 + 10 + + + False + 5 + dialog + + + True + False + 2 + + + True + False + end + + + gtk-close + True + True + True + True + + + False + False + 0 + + + + + True + True + 0 + + + + + True + False + 3 + 3 + 4 + 2 + + + True + False + 1 + Work duration + + + + + True + False + 1 + Short break duration + + + 1 + 2 + + + + + True + False + 1 + Long break duration + + + 2 + 3 + + + + + True + False + [min] + + + 2 + 3 + + + + + True + False + [min] + + + 2 + 3 + 1 + 2 + + + + + True + False + [min] + + + 2 + 3 + 2 + 3 + + + + + True + True + + False + False + True + True + WorkDuration + True + + + 1 + 2 + + + + + True + True + + False + False + True + True + ShortBreakDuration + True + + + 1 + 2 + 1 + 2 + + + + + True + True + + False + False + True + True + LongBreakDuration + True + + + 1 + 2 + 2 + 3 + + + + + False + True + 1 + + + + + + button1 + + + diff --git a/src/plugins/pommodoro/tomato_1bpp.png b/src/plugins/pommodoro/tomato_1bpp.png new file mode 100644 index 0000000..8256c2c Binary files /dev/null and b/src/plugins/pommodoro/tomato_1bpp.png differ diff --git a/src/plugins/pommodoro/tomato_empty_1bpp.png b/src/plugins/pommodoro/tomato_empty_1bpp.png new file mode 100644 index 0000000..da24f26 Binary files /dev/null and b/src/plugins/pommodoro/tomato_empty_1bpp.png differ diff --git a/src/plugins/ppastats/Makefile.am b/src/plugins/ppastats/Makefile.am new file mode 100644 index 0000000..52e15ef --- /dev/null +++ b/src/plugins/ppastats/Makefile.am @@ -0,0 +1,7 @@ +SUBDIRS = default +plugindir = $(datadir)/gnome15/plugins/ppastats +plugin_DATA = ppastats.py \ + ppastats.ui + +EXTRA_DIST = \ + $(plugin_DATA) diff --git a/src/plugins/ppastats/default/Makefile.am b/src/plugins/ppastats/default/Makefile.am new file mode 100644 index 0000000..56e71cb --- /dev/null +++ b/src/plugins/ppastats/default/Makefile.am @@ -0,0 +1,8 @@ +themedir = $(datadir)/gnome15/plugins/ppastats/default +theme_DATA = default.svg \ + default-menu-entry.svg \ + g19.svg \ + g19-menu-entry.svg + +EXTRA_DIST = \ + $(theme_DATA) diff --git a/src/plugins/ppastats/default/default-menu-entry.svg b/src/plugins/ppastats/default/default-menu-entry.svg new file mode 100644 index 0000000..39635fb --- /dev/null +++ b/src/plugins/ppastats/default/default-menu-entry.svg @@ -0,0 +1,191 @@ + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + ${ent_month_year} ${ent_time_24} + ${ent_title} + + + + + + ${ent_month_year} ${ent_time_24} + ${ent_title} + + + diff --git a/src/plugins/ppastats/default/default.svg b/src/plugins/ppastats/default/default.svg new file mode 100644 index 0000000..0f08afb --- /dev/null +++ b/src/plugins/ppastats/default/default.svg @@ -0,0 +1,127 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + ${title} + + + + + + + + diff --git a/src/plugins/ppastats/default/g19-menu-entry.svg b/src/plugins/ppastats/default/g19-menu-entry.svg new file mode 100644 index 0000000..4e8ed9a --- /dev/null +++ b/src/plugins/ppastats/default/g19-menu-entry.svg @@ -0,0 +1,160 @@ + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + ${ent_title} + ${ent_month_year} ${ent_time_24} + + diff --git a/src/plugins/ppastats/default/g19.svg b/src/plugins/ppastats/default/g19.svg new file mode 100644 index 0000000..25870dd --- /dev/null +++ b/src/plugins/ppastats/default/g19.svg @@ -0,0 +1,233 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + ${title} + ${subtitle} + ${updated} + + + + + + + + diff --git a/src/plugins/ppastats/ppastats.py b/src/plugins/ppastats/ppastats.py new file mode 100644 index 0000000..8a6cb1c --- /dev/null +++ b/src/plugins/ppastats/ppastats.py @@ -0,0 +1,231 @@ +#!/usr/bin/env python + +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2011 Brett Smith +# +# 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 . + +import gnome15.util.g15convert as g15convert +import gnome15.util.g15scheduler as g15scheduler +import gnome15.util.g15cairo as g15cairo +import gnome15.util.g15icontools as g15icontools +import gnome15.g15theme as g15theme +import gnome15.g15driver as g15driver +import subprocess +import time +import os +import feedparser +import gtk +import gconf +from launchpadlib.launchpad import Launchpad + + +# Plugin details - All of these must be provided +id="ppstats" +name="PPA Statistics" +description="Show download statistics and other information about your Ubuntu PPA." +author="Brett Smith " +copyright="Copyright (C)2010 Brett Smith" +site="http://www.gnome15.org/" +has_preferences=True +unsupported_models = [ g15driver.MODEL_G110 ] + +def create(gconf_key, gconf_client, screen): + return G15PPAStats(gconf_client, gconf_key, screen) + +def show_preferences(parent, gconf_client, gconf_key): + G15PPAStatsPreferences(parent, gconf_client, gconf_key) + +def changed(widget, key, gconf_client): + gconf_client.set_bool(key, widget.get_active()) + +def get_update_time(gconf_client, gconf_key): + val = gconf_client.get_int(gconf_key + "/update_time") + if val == 0: + val = 60 + return val + +class G15PPAStatsPreferences(): + + def __init__(self, parent, gconf_client,gconf_key): + self.gconf_client = gconf_client + self.gconf_key = gconf_key + + widget_tree = gtk.Builder() + widget_tree.add_from_file(os.path.join(os.path.dirname(__file__), "ppastats.ui")) + + # Feeds + self.feed_model = widget_tree.get_object("PPAModel") + self.reload_model() + self.feed_list = widget_tree.get_object("PPAList") + self.url_renderer = widget_tree.get_object("URLRenderer") + + # Updates + self.update_adjustment = widget_tree.get_object("UpdateAdjustment") + self.update_adjustment.set_value(get_update_time(gconf_client, gconf_key)) + self.update_adjustment.set_value(get_update_time(gconf_client, gconf_key)) + + # Connect to events + self.update_adjustment.connect("value-changed", self.update_time_changed) + self.url_renderer.connect("edited", self.url_edited) + widget_tree.get_object("NewPPA").connect("clicked", self.new_url) + widget_tree.get_object("RemovePPA").connect("clicked", self.remove_url) + + # Show dialog + dialog = widget_tree.get_object("PPADialog") + dialog.set_transient_for(parent) + + ah = gconf_client.notify_add(gconf_key + "/urls", self.urls_changed); + dialog.run() + dialog.hide() + gconf_client.notify_remove(ah); + + def update_time_changed(self, widget): + self.gconf_client.set_int(self.gconf_key + "/update_time", int(widget.get_value())) + + def url_edited(self, widget, row_index, value): + row = self.feed_model[row_index] + if value != "": + urls = self.gconf_client.get_list(self.gconf_key + "/urls", gconf.VALUE_STRING) + if row[0] in urls: + urls.remove(row[0]) + urls.append(value) + self.gconf_client.set_list(self.gconf_key + "/urls", gconf.VALUE_STRING, urls) + else: + self.feed_model.remove(self.feed_model.get_iter(row_index)) + + def urls_changed(self, client, connection_id, entry, args): + self.reload_model() + + def reload_model(self): + self.feed_model.clear() + for url in self.gconf_client.get_list(self.gconf_key + "/urls", gconf.VALUE_STRING): + self.feed_model.append([ url, True ]) + + def new_url(self, widget): + self.feed_model.append(["", True]) + self.feed_list.set_cursor_on_cell(str(len(self.feed_model) - 1), focus_column = self.feed_list.get_column(0), focus_cell = self.url_renderer, start_editing = True) + self.feed_list.grab_focus() + + def remove_url(self, widget): + (model, path) = self.feed_list.get_selection().get_selected() + url = model[path][0] + urls = self.gconf_client.get_list(self.gconf_key + "/projects", gconf.VALUE_STRING) + if url in urls: + urls.remove(url) + self.gconf_client.set_list(self.gconf_key + "/projects", gconf.VALUE_STRING, urls) + +class G15PPAPage(): + + def __init__(self, plugin, url): + self.launchpad = plugin.launchpad + self.gconf_client = plugin.gconf_client + self.gconf_key = plugin.gconf_key + self.screen = plugin.screen + self.theme = g15theme.G15Theme(os.path.join(os.path.dirname(__file__), "default"), self.screen) + self.url = url + self.index = -1 + self.selected_entry = None + self.reload() + self.page = self.screen.new_page(self.paint, id="PPA " + str(plugin.page_serial), thumbnail_painter = self.paint_thumbnail) + plugin.page_serial += 1 + self.screen.redraw(self.page) + self.project = self.launchpad.projects[self.url] + print self.project + + def reload(self): + self.icon = g15icontools.get_icon_path("application-rss+xml", self.screen.height ) + self.title = "PPA" + + def paint_thumbnail(self, canvas, allocated_size, horizontal): + return g15cairo.paint_thumbnail_image(allocated_size, g15cairo.load_surface_from_file(self.icon), canvas) + + def paint(self, canvas): + properties = {} + attributes = {} + properties["title"] = self.title + attributes["icon"] = self.icon + self.theme.draw(canvas, properties, attributes) + +class G15PPAStats(): + + def __init__(self, gconf_client,gconf_key, screen): + self.screen = screen; + self.gconf_key = gconf_key + self.gconf_client = gconf_client + self.page_serial = 1 + + def activate(self): + + self.cache_dir = os.path.expanduser("~/.gnome2/gnome15/ppastats") + self.launchpad = Launchpad.login_anonymously("just testing", "production", self.cache_dir) + bug_one = self.launchpad.bugs[1] + print "Bug one",bug_one.title + + self.pages = {} + self.update_time_changed_handle = self.gconf_client.notify_add(self.gconf_key + "/update_time", self._update_time_changed) + self.ppas_changed_handle = self.gconf_client.notify_add(self.gconf_key + "/ppas", self._ppas_changed) + self._load_ppas() + self._schedule_refresh() + + def deactivate(self): + self.gconf_client.notify_remove(self.update_time_changed_handle); + self.gconf_client.notify_remove(self.ppas_changed_handle); + for page in self.pages: + self.screen.del_page(self.pages[page].page) + self.pages = {} + + def destroy(self): + pass + + ''' + Private + ''' + + def _schedule_refresh(self): + schedule_seconds = get_update_time(self.gconf_client, self.gconf_key) * 60.0 + self.refresh_timer = g15scheduler.schedule("PPARefreshTimer", schedule_seconds, self._refresh) + + def _refresh(self): + for page_id in self.pages: + page = self.pages[page_id] + page.reload() + self.screen.redraw(page.page) + self._schedule_refresh() + + def _update_time_changed(self, client, connection_id, entry, args): + self.refresh_timer.cancel() + self._schedule_refresh() + + def _ppas_changed(self, client, connection_id, entry, args): + self._load_ppas() + + def _load_ppas(self): + ppa_list = self.gconf_client.get_list(self.gconf_key + "/ppas", gconf.VALUE_STRING) + + # Add new pages + for url in ppa_list: + if not url in self.pages: + self.pages[url] = G15PPAPage(self, url) + + # Remove pages that no longer exist + to_remove = [] + for page_url in self.pages: + page = self.pages[page_url] + if not page.url in feed_list: + self.screen.del_page(page.page) + to_remove.append(page_url) + for page in to_remove: + del self.pages[page] + diff --git a/src/plugins/ppastats/ppastats.ui b/src/plugins/ppastats/ppastats.ui new file mode 100644 index 0000000..ae11337 --- /dev/null +++ b/src/plugins/ppastats/ppastats.ui @@ -0,0 +1,271 @@ + + + + + + + + + + + + + + 1 + 9999 + 1 + 1 + 1 + + + 320 + False + 5 + PPA Preferences + False + True + center-on-parent + dialog + + + True + False + 2 + + + True + False + end + + + gtk-close + True + True + True + True + + + False + False + 0 + + + + + False + True + end + 0 + + + + + True + False + + + True + False + + + True + False + 0 + none + + + True + False + 12 + + + True + False + + + True + False + + + True + False + toolbutton1 + True + gtk-add + + + False + True + + + + + True + False + toolbutton2 + True + gtk-remove + + + False + True + + + + + False + False + 0 + + + + + True + True + automatic + automatic + in + + + True + True + PPAModel + False + False + 0 + + + URL + + + + 1 + 0 + + + + + + + + + True + True + 1 + + + + + + + + + True + False + <b>PPAS</b> + True + + + + + True + True + 0 + + + + + True + False + 0 + none + + + True + False + 12 + + + True + False + + + True + False + Update every + + + True + True + 0 + + + + + True + True + + False + False + True + True + UpdateAdjustment + + + True + True + 1 + + + + + True + False + minutes + + + True + True + 2 + + + + + + + + + True + False + <b>Options</b> + True + + + + + True + True + 1 + + + + + True + True + 0 + + + + + False + False + 1 + + + + + + button9 + + + diff --git a/src/plugins/processes/Makefile.am b/src/plugins/processes/Makefile.am new file mode 100644 index 0000000..ca380e9 --- /dev/null +++ b/src/plugins/processes/Makefile.am @@ -0,0 +1,5 @@ +plugindir = $(datadir)/gnome15/plugins/processes +plugin_DATA = processes.py + +EXTRA_DIST = \ + $(plugin_DATA) \ No newline at end of file diff --git a/src/plugins/processes/i18n/processes.en_GB.po b/src/plugins/processes/i18n/processes.en_GB.po new file mode 100644 index 0000000..66948f9 --- /dev/null +++ b/src/plugins/processes/i18n/processes.en_GB.po @@ -0,0 +1,99 @@ +# English translations for gnome15-plugins package. +# Copyright (C) 2012 THE gnome15-plugins'S COPYRIGHT HOLDER +# This file is distributed under the same license as the gnome15-plugins package. +# Automatically generated, 2012. +# +msgid "" +msgstr "" +"Project-Id-Version: gnome15-plugins 0.8.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: 2012-01-08 11:23+0000\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: en_GB\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=ASCII\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: processes.py:42 +msgid "Process List" +msgstr "Process List" + +#: processes.py:43 +msgid "" +"Lists all running processes and allows them to be killed. through a menu on " +"the LCD." +msgstr "" +"Lists all running processes and allows them to be killed. through a menu on " +"the LCD." + +#: processes.py:46 +msgid "Copyright (C)2010 Brett Smith" +msgstr "Copyright (C)2010 Brett Smith" + +#: processes.py:52 +msgid "Previous process" +msgstr "Previous process" + +#: processes.py:53 +msgid "Next process" +msgstr "Next process" + +#: processes.py:54 +msgid "Next page" +msgstr "Next page" + +#: processes.py:55 +msgid "Previous page" +msgstr "Previous page" + +#: processes.py:56 +msgid "Kill process" +msgstr "Kill process" + +#: processes.py:57 +msgid "" +"Toggle between applications,\n" +"all and user" +msgstr "" +"Toggle between applications,\n" +"all and user" + +#: processes.py:91 +msgid "Kill Process" +msgstr "Kill Process" + +#: processes.py:91 +#, python-format +msgid "" +"Are you sure you want to kill\n" +"%s" +msgstr "" +"Are you sure you want to kill\n" +"%s" + +#: processes.py:167 +msgid "Applications" +msgstr "Applications" + +#: processes.py:168 +msgid "All" +msgstr "All" + +#: processes.py:170 +msgid "All Processes" +msgstr "All Processes" + +#: processes.py:171 +msgid "Usr" +msgstr "Usr" + +#: processes.py:173 +msgid "User Processes" +msgstr "User Processes" + +#: processes.py:174 +msgid "App" +msgstr "App" diff --git a/src/plugins/processes/i18n/processes.pot b/src/plugins/processes/i18n/processes.pot new file mode 100644 index 0000000..178ca18 --- /dev/null +++ b/src/plugins/processes/i18n/processes.pot @@ -0,0 +1,93 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: processes.py:42 +msgid "Process List" +msgstr "" + +#: processes.py:43 +msgid "" +"Lists all running processes and allows them to be killed. through a menu on " +"the LCD." +msgstr "" + +#: processes.py:46 +msgid "Copyright (C)2010 Brett Smith" +msgstr "" + +#: processes.py:52 +msgid "Previous process" +msgstr "" + +#: processes.py:53 +msgid "Next process" +msgstr "" + +#: processes.py:54 +msgid "Next page" +msgstr "" + +#: processes.py:55 +msgid "Previous page" +msgstr "" + +#: processes.py:56 +msgid "Kill process" +msgstr "" + +#: processes.py:57 +msgid "" +"Toggle between applications,\n" +"all and user" +msgstr "" + +#: processes.py:91 +msgid "Kill Process" +msgstr "" + +#: processes.py:91 +#, python-format +msgid "" +"Are you sure you want to kill\n" +"%s" +msgstr "" + +#: processes.py:167 +msgid "Applications" +msgstr "" + +#: processes.py:168 +msgid "All" +msgstr "" + +#: processes.py:170 +msgid "All Processes" +msgstr "" + +#: processes.py:171 +msgid "Usr" +msgstr "" + +#: processes.py:173 +msgid "User Processes" +msgstr "" + +#: processes.py:174 +msgid "App" +msgstr "" diff --git a/src/plugins/processes/processes.py b/src/plugins/processes/processes.py new file mode 100644 index 0000000..38b1a24 --- /dev/null +++ b/src/plugins/processes/processes.py @@ -0,0 +1,392 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2011 Brett Smith +# +# 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 . + +import gnome15.g15locale as g15locale +_ = g15locale.get_translation("processes", modfile = __file__).ugettext + +import gnome15.util.g15scheduler as g15scheduler +import gnome15.util.g15cairo as g15cairo +import gnome15.util.g15icontools as g15icontools +import gnome15.g15theme as g15theme +import gnome15.g15driver as g15driver +import gnome15.g15plugin as g15plugin +import os +import dbus +import time +import gobject +import logging +logger = logging.getLogger(__name__) + +try: + import gtop +except Exception as e: + logger.debug("Could not import gtop module. Will use g15top instead", exc_info = e) + # API compatible work around for Ubuntu 12.10 + import gnome15.g15top as gtop + +from Xlib import X +import Xlib.protocol.event + +# Plugin details - All of these must be provided +id="processes" +name=_("Process List") +description=_("Lists all running processes and allows them to be \ +killed. through a menu on the LCD.") +author="Brett Smith " +copyright=_("Copyright (C)2010 Brett Smith") +site="http://www.russo79.com/gnome15" +has_preferences=False +unsupported_models = [ g15driver.MODEL_G110, g15driver.MODEL_G11, g15driver.MODEL_G930, g15driver.MODEL_G35 ] +reserved_keys = [ g15driver.G_KEY_SETTINGS ] +actions={ + g15driver.PREVIOUS_SELECTION : _("Previous process"), + g15driver.NEXT_SELECTION : _("Next process"), + g15driver.NEXT_PAGE : _("Next page"), + g15driver.PREVIOUS_PAGE : _("Previous page"), + g15driver.SELECT : _("Kill process"), + g15driver.VIEW : _("Toggle between applications,\nall and user") + } + +def create(gconf_key, gconf_client, screen): + return G15Processes(gconf_client, gconf_key, screen) + +class ProcessMenuItem(g15theme.MenuItem): + """ + MenuItem for individual processes + """ + + def __init__(self, item_id, plugin, process_id, process_name): + g15theme.MenuItem.__init__(self, item_id) + self.icon = None + self.process_id = process_id + self.process_name = process_name + self.plugin = plugin + + def get_default_theme_dir(self): + return os.path.join(os.path.dirname(__file__), "default") + + def get_theme_properties(self): + item_properties = g15theme.MenuItem.get_theme_properties(self) + item_properties["item_name"] = self.process_name if self.process_name is not None and len(self.process_name) > 0 else "Unamed" + if isinstance(self.process_id, int): + item_properties["item_alt"] = self.process_id + else: + item_properties["item_alt"] = "" + item_properties["item_type"] = "" + item_properties["item_icon"] = self.icon + return item_properties + + def activate(self): + kill_name = str(self.process_id) if isinstance(self.process_id, int) else self.process_name + self.plugin.confirm_screen = g15theme.ConfirmationScreen(self.get_screen(), _("Kill Process"), _("Are you sure you want to kill\n%s") % kill_name, + g15icontools.get_icon_path("utilities-system-monitor"), self.plugin._kill_process, self.process_id, + cancel_callback = self.plugin._cancel_kill) + + +class G15Processes(g15plugin.G15MenuPlugin): + + def __init__(self, gconf_client, gconf_key, screen): + g15plugin.G15MenuPlugin.__init__(self, gconf_client, gconf_key, screen, ["utilities-system-monitor"], id, name) + self.item_id = 0 + self.confirm_screen = None + + # Can't work out how to kill an application/window given its XID, so only wnck is used for killing + self.session_bus = dbus.SessionBus() + self.bamf_matcher = None + try : + bamf_object = self.session_bus.get_object('org.ayatana.bamf', '/org/ayatana/bamf/matcher') + self.bamf_matcher = dbus.Interface(bamf_object, 'org.ayatana.bamf.matcher') + except Exception as e: + logger.warning("BAMF not available, falling back to WNCK", exc_info = e) + + def activate(self): + self._modes = [ "applications", "all", "user" ] + self._mode = "applications" + self._timer = None + self._matches = [] + g15plugin.G15MenuPlugin.activate(self) + self.screen.key_handler.action_listeners.append(self) + if self.bamf_matcher is not None: + self._matches.append(self.bamf_matcher.connect_to_signal("ViewOpened", self._view_opened)) + self._matches.append(self.bamf_matcher.connect_to_signal("ViewClosed", self._view_closed)) + + def deactivate(self): + self._cancel_timer() + g15plugin.G15MenuPlugin.deactivate(self) + for m in self._matches: + m.remove() + self.screen.key_handler.action_listeners.remove(self) + if self.confirm_screen is not None: + self.confirm_screen.delete() + self.confirm_screen = None + + def load_menu_items(self): + pass + + def _get_next_id(self): + self.item_id += 1 + return self.item_id + + def action_performed(self, binding): + if self.page != None and self.page.is_visible(): + if binding.action == g15driver.VIEW: + if self._mode == "applications": + self._mode = "all" + elif self._mode == "all": + self._mode = "user" + elif self._mode == "user": + self._mode = "applications" + self._cancel_timer() + self._refresh() + return True + + def create_menu(self): + menu = g15plugin.G15MenuPlugin.create_menu(self) + menu.on_move = self._reschedule + return menu + + def create_page(self): + page = g15plugin.G15MenuPlugin.create_page(self) + page.on_shown = self._page_shown + page.on_hidden = self._page_hidden + return page + + def get_theme_properties(self): + props = g15plugin.G15MenuPlugin.get_theme_properties(self) + + props["mode"] = self._mode + if self._mode == "applications": + props["title"] = _("Applications") + props["list"] = _("All") + elif self._mode == "all": + props["title"] = _("All Processes") + props["list"] = _("Usr") + elif self._mode == "user": + props["title"] = _("User Processes") + props["list"] = _("App") + return props + + ''' + Private + ''' + def _view_opened(self, window_path, path_type): + if path_type == "application": + self._get_item_for_bamf_application(window_path) + + def _view_closed(self, window_path, path_type): + if path_type == "application": + self._remove_item_for_bamf_application(window_path) + + def _send_event(self, win, ctype, data, mask=None): + """ Send a ClientMessage event to the root """ + data = (data+[0]*(5-len(data)))[:5] + ev = Xlib.protocol.event.ClientMessage(window=win, client_type=ctype, data=(32,(data))) + + if not mask: + mask = (X.SubstructureRedirectMask|X.SubstructureNotifyMask) + + display = self.screen.service.macro_handler.get_x_display() + screen = display.screen() + root = screen.root + + root.send_event(ev, event_mask=mask) + display.flush() + + def _cancel_kill(self, process_id): + self.confirmation_screen = None + + def _do_kill(self, process_id): + os.system("kill %d" % process_id) + time.sleep(0.5) + if process_id in gtop.proclist(): + time.sleep(5.0) + if process_id in gtop.proclist(): + os.system("kill -9 %d" % process_id) + + def _kill_process(self, process_id): + if isinstance(process_id, int): + self._do_kill(process_id) + else: + gobject.idle_add(self._kill_window, process_id) + self.confirmation_screen = None + self._reload_menu() + + def _kill_window(self, window_path): + import wnck + import gtk + window_names = self._get_window_names(window_path) + screen = wnck.screen_get_default() + while gtk.events_pending(): + gtk.main_iteration() + windows = screen.get_windows() + for window_name in window_names: + for w in windows: + if w.get_name() == window_name: + self._do_kill(w.get_pid()) + return + + def _get_window_names(self, path, window_names = []): + app = self.session_bus.get_object("org.ayatana.bamf", path) + view = dbus.Interface(app, 'org.ayatana.bamf.view') + window_names.append(view.Name()) + children = view.Children() + for c in children: + self._get_window_names(c, window_names) + return window_names + + def _get_process_name(self, args, cmd): + result = cmd + for i in range(min(2, len(args))): + basename = os.path.basename(args[i]) + if basename.find(cmd) != -1: + result = basename + break + return result + + def _reload_menu(self): + g15scheduler.schedule("ReloadProcesses", 0, self._do_reload_menu) + + def _get_menu_item(self, pid): + item = self.menu.get_child_by_id("process-%s" % pid) + if item == None: + item = ProcessMenuItem("process-%s" % pid, self, pid, None) + self.menu.add_child(item) + return item + + def _get_bamf_application_object(self, window): + app = self.session_bus.get_object("org.ayatana.bamf", window) + view = dbus.Interface(app, 'org.ayatana.bamf.view') + return view + + def _remove_item_for_bamf_application(self, window): + item = self.menu.get_child_by_id("process-%s" % window) + if item is not None: + self.menu.remove_child(item) + + def _get_item_for_bamf_application(self, window): + view = self._get_bamf_application_object(window) + item = self._get_menu_item(window) + try: + item.process_name = view.Name() + except dbus.DBusException as e: + logger.debug("Could not get process_name. Using default", exc_info = e) + item.process_name = "Unknown" + try: + icon_name = view.Icon() + if icon_name and len(icon_name) > 0: + icon_path = g15icontools.get_icon_path(icon_name, warning = False) + if icon_path: + item.icon = g15cairo.load_surface_from_file(icon_path, 32) + except dbus.DBusException as e: + logger.debug("Could not get icon", exc_info = e) + pass + + + return item + + def _do_reload_menu(self): + if not self.active: + return + + this_items = {} + if self._mode == "applications": + if self.bamf_matcher != None: + for window in self.bamf_matcher.RunningApplications(): + try: + item = self._get_item_for_bamf_application(window) + this_items[item.id] = item + except Exception as e: + logger.debug("Could not get info from BAMF", exc_info = e) + pass + else: + import wnck + screen = wnck.screen_get_default() + for window in screen.get_windows(): + pid = window.get_pid() + if pid > 0: + item = self._get_menu_item(pid) + item.process_name = window.get_name() + this_items[item.id] = item + pixbuf = window.get_icon() + if pixbuf: + item.icon = g15cairo.pixbuf_to_surface(pixbuf) + + else: + for process_id in gtop.proclist(): + process_id = "%d" % process_id + try : + pid = int(process_id) + proc_state = gtop.proc_state(pid) + proc_args = gtop.proc_args(pid) + if self._mode == "all" or ( self._mode != "all" and proc_state.uid == os.getuid()): + item = self._get_menu_item(pid) + item.icon = None + item.process_name = self._get_process_name(proc_args, proc_state.cmd) + this_items[item.id] = item + except Exception as e: + logger.debug("Process may have disappeared", exc_info = e) + # In case the process disappears + pass + + # Remove any missing items + for item in self.menu.get_children(): + if not item.id in this_items: + self.menu.remove_child(item) + + # Make sure selected still exists + if self.menu.selected != None and self.menu.get_child_by_id(self.menu.selected.id) is None: + if len(self.menu.get_child_count()) > 0: + self.menu.selected = self.menu.get_children()[0] + else: + self.menu.selected = None + + self.page.mark_dirty() + self.screen.redraw(self.page) + + def _on_move(self): + self._reschedule() + + def _on_selected(self): + self.screen.redraw(self.page) + + def _page_shown(self): + logger.debug("Process list activated") + self._reload_menu() + self._schedule_refresh() + + def _page_hidden(self): + self._cancel_timer() + + def _refresh(self): + self._reload_menu() + self._schedule_refresh() + + def _cancel_timer(self): + if self._timer != None: + logger.debug("Stopping refreshing process list") + self._timer.cancel() + + def _reschedule(self): + self._cancel_timer() + self._schedule_refresh() + + def _schedule_refresh(self): + """ + When viewing applications, we don't refresh, just rely on BAMF + events when BAMF is available + """ + if not self._mode == "applications" or self.bamf_matcher is None: + self._timer = g15scheduler.schedule("ProcessesRefresh", 5.0, self._refresh) diff --git a/src/plugins/profiles/Makefile.am b/src/plugins/profiles/Makefile.am new file mode 100644 index 0000000..2c854e0 --- /dev/null +++ b/src/plugins/profiles/Makefile.am @@ -0,0 +1,5 @@ +plugindir = $(datadir)/gnome15/plugins/profiles +plugin_DATA = profiles.py + +EXTRA_DIST = \ + $(plugin_DATA) \ No newline at end of file diff --git a/src/plugins/profiles/bw-locked-inverted.gif b/src/plugins/profiles/bw-locked-inverted.gif new file mode 100644 index 0000000..391c266 Binary files /dev/null and b/src/plugins/profiles/bw-locked-inverted.gif differ diff --git a/src/plugins/profiles/bw-locked.gif b/src/plugins/profiles/bw-locked.gif new file mode 100644 index 0000000..0efd021 Binary files /dev/null and b/src/plugins/profiles/bw-locked.gif differ diff --git a/src/plugins/profiles/profiles.py b/src/plugins/profiles/profiles.py new file mode 100644 index 0000000..b2c54c7 --- /dev/null +++ b/src/plugins/profiles/profiles.py @@ -0,0 +1,213 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2012 Brett Smith +# +# 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 . + +import gnome15.g15locale as g15locale +_ = g15locale.get_translation("profiles", modfile = __file__).ugettext + +import gnome15.g15profile as g15profile +import gnome15.g15driver as g15driver +import gnome15.g15theme as g15theme +import gnome15.g15plugin as g15plugin +import gnome15.util.g15cairo as g15cairo +import gnome15.util.g15icontools as g15icontools +import gnome15.g15devices as g15devices +import gnome15.g15actions as g15actions +from gnome15.util.g15pythonlang import find +import os +import logging +logger = logging.getLogger(__name__) + +# Custom actions +SELECT_PROFILE = "select-profile" + +# Register the action with all supported models +g15devices.g15_action_keys[SELECT_PROFILE] = g15actions.ActionBinding(SELECT_PROFILE, [ g15driver.G_KEY_L1 ], g15driver.KEY_STATE_HELD) +g15devices.g19_action_keys[SELECT_PROFILE] = g15actions.ActionBinding(SELECT_PROFILE, [ g15driver.G_KEY_BACK ], g15driver.KEY_STATE_HELD) + +# Plugin details - All of these must be provided +id="profiles" +name=_("Profile Selector") +description=_("Allows selection of the currently active profile. You may also \n\ +lock a profile to the device it is running on, preventing\n\ +changes triggered by active window changes and other\n\ +automatic profile selection methods.\n\n\ +You may also use this plugin to set the window title that\n\ +activates the current profile by making the required\n\ +window foreground, and the pressing the key bound to\n\ +'Select current window as activator' (see below).") +author="Brett Smith " +copyright=_("Copyright (C)2012 Brett Smith") +site="http://www.russo79.com/gnome15" +has_preferences=False +default_enabled=True +unsupported_models = [ g15driver.MODEL_G110, g15driver.MODEL_Z10, g15driver.MODEL_G11, g15driver.MODEL_MX5500, g15driver.MODEL_G930, g15driver.MODEL_G35 ] +actions={ + SELECT_PROFILE : _("Show profile selector"), + g15driver.PREVIOUS_SELECTION : _("Previous item"), + g15driver.NEXT_SELECTION : _("Next item"), + g15driver.NEXT_PAGE : _("Next page"), + g15driver.PREVIOUS_PAGE : _("Previous page"), + g15driver.VIEW : _("Lock profile"), + g15driver.SELECT : _("Activate profile"), + g15driver.CLEAR : _("Set current window as activator") + } + +def create(gconf_key, gconf_client, screen): + return G15Profiles(gconf_client, gconf_key, screen) + +""" +Represents a profile as a single item in a menu +""" +class ProfileMenuItem(g15theme.MenuItem): + def __init__(self, profile, plugin, id): + g15theme.MenuItem.__init__(self, id) + self.profile = profile + self._plugin = plugin + self._surface = None + + def get_theme_properties(self): + locked = self.profile.is_active() and g15profile.is_locked(self._plugin.screen.device) + + if self.get_screen().device.bpp > 1: + locked_icon = g15icontools.get_icon_path(["locked","gdu-encrypted-lock", + "status_lock", "stock_lock" ]) + else: + if self.parent.selected == self: + locked_icon = os.path.join(os.path.dirname(__file__), 'bw-locked-inverted.gif') + else: + locked_icon = os.path.join(os.path.dirname(__file__), 'bw-locked.gif') + + item_properties = g15theme.MenuItem.get_theme_properties(self) + item_properties["item_name"] = self.profile.name + item_properties["item_radio"] = True + item_properties["item_radio_selected"] = self.profile.is_active() + item_properties["item_icon"] = self._surface + item_properties["item_alt_icon"] = locked_icon if locked else "" + item_properties["item_alt"] = "" + return item_properties + + def on_configure(self): + g15theme.MenuItem.on_configure(self) + self._surface = g15cairo.load_surface_from_file(self.profile.get_profile_icon_path(16), self.theme.bounds[3]) + + def activate(self): + locked = g15profile.is_locked(self._plugin.screen.device) + if locked: + g15profile.set_locked(self._plugin.screen.device, False) + self.profile.make_active() + if locked: + g15profile.set_locked(self._plugin.screen.device, True) + + # Raise the macros page if it is enabled and not raised + macros_page = self._plugin.screen.get_page("macros") + if macros_page is not None and not macros_page.is_visible(): + self._plugin.screen.raise_page(macros_page) + + +""" +Profiles plugin class +""" +class G15Profiles(g15plugin.G15MenuPlugin): + + def __init__(self, gconf_client, gconf_key, screen): + g15plugin.G15MenuPlugin.__init__(self, gconf_client, gconf_key, screen, [ "user-bookmarks", "bookmarks" ], id, _("Profiles")) + + def activate(self): + g15plugin.G15MenuPlugin.activate(self) + g15profile.profile_listeners.append(self._stored_profiles_changed) + self.delete_timer = None + self.screen.key_handler.action_listeners.append(self) + self._notify_handles = [] + self._notify_handles.append(self.gconf_client.notify_add("/apps/gnome15/%s/active_profile" % self.screen.device.uid, self._profiles_changed)) + self._notify_handles.append(self.gconf_client.notify_add("/apps/gnome15/%s/locked" % self.screen.device.uid, self._profiles_changed)) + + def deactivate(self): + g15plugin.G15MenuPlugin.deactivate(self) + g15profile.profile_listeners.remove(self._stored_profiles_changed) + self.screen.key_handler.action_listeners.remove(self) + for h in self._notify_handles: + self.gconf_client.notify_remove(h) + + def action_performed(self, binding): + if self.page != None: + if binding.action == SELECT_PROFILE: + self.screen.raise_page(self.page) + elif self.page.is_visible(): + if binding.action == g15driver.VIEW: + active = g15profile.get_active_profile(self.screen.device) + if active.id == self.menu.selected.profile.id: + g15profile.set_locked(self.screen.device, not g15profile.is_locked(self.screen.device)) + else: + if g15profile.is_locked(self.screen.device): + g15profile.set_locked(self.screen.device, False) + self.menu.selected.profile.make_active() + g15profile.set_locked(self.screen.device, True) + return True + elif binding.action == g15driver.CLEAR: + profile = self.menu.selected.profile + if self.screen.service.active_application_name is not None: + self._configure_profile_with_window_name(profile, self.screen.service.active_application_name) + profile.save() + elif self.screen.service.active_window_title is not None: + self._configure_profile_with_window_name(profile, self.screen.service.active_window_title) + profile.save() + return True + + + def show_menu(self): + active_profile = g15profile.get_active_profile(self.screen.device) + g15plugin.G15MenuPlugin.show_menu(self) + if active_profile: + item = find(lambda m: m.profile == active_profile, self.menu.get_children()) + if item: + self.menu.set_selected_item(item) + + def load_menu_items(self): + items = [] + profile_list = g15profile.get_profiles(self.screen.device) + for profile in profile_list: + items.append(ProfileMenuItem(profile, self, "profile-%s" % profile.id )) + items = sorted(items, key=lambda item: item.profile.name) + self.menu.set_children(items) + if len(items) > 0: + self.menu.selected = items[0] + else: + self.menu.selected = None + + def get_theme_properties(self): + p = g15plugin.G15MenuPlugin.get_theme_properties(self) + p["profile_locked"] = g15profile.is_locked(self.screen.device) + return p + + ''' + Private + ''' + def _configure_profile_with_window_name(self, profile, window_name): + profile.activate_on_focus = True + profile.activate_on_launch = False + profile.launch_pattern = None + profile.window_name = window_name + + def _profiles_changed(self, arg0 = None, arg1 = None, arg2 = None, arg3 = None): + self.screen.redraw(self.page) + + def _stored_profiles_changed(self, profile_id, device): + self._reload_menu() + + def _reload_menu(self): + self.load_menu_items() + self.screen.redraw(self.page) + \ No newline at end of file diff --git a/src/plugins/rss/Makefile.am b/src/plugins/rss/Makefile.am new file mode 100644 index 0000000..3590ed3 --- /dev/null +++ b/src/plugins/rss/Makefile.am @@ -0,0 +1,7 @@ +SUBDIRS = default +plugindir = $(datadir)/gnome15/plugins/rss +plugin_DATA = rss.py \ + rss.ui + +EXTRA_DIST = \ + $(plugin_DATA) \ No newline at end of file diff --git a/src/plugins/rss/default/Makefile.am b/src/plugins/rss/default/Makefile.am new file mode 100644 index 0000000..98c63ac --- /dev/null +++ b/src/plugins/rss/default/Makefile.am @@ -0,0 +1,9 @@ +themedir = $(datadir)/gnome15/plugins/rss/default +theme_DATA = default-menu-screen.svg \ + default-menu-entry.svg \ + mx5500-menu-entry.svg \ + g19-menu-screen.svg \ + g19-menu-entry.svg + +EXTRA_DIST = \ + $(theme_DATA) diff --git a/src/plugins/rss/default/default-menu-entry.svg b/src/plugins/rss/default/default-menu-entry.svg new file mode 100644 index 0000000..c1b25e7 --- /dev/null +++ b/src/plugins/rss/default/default-menu-entry.svg @@ -0,0 +1,205 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + ${ent_month_year} ${ent_time} + ${ent_title} + + + + ${ent_month_year} ${ent_time} + ${ent_title} + + diff --git a/src/plugins/rss/default/default-menu-screen.svg b/src/plugins/rss/default/default-menu-screen.svg new file mode 100644 index 0000000..32fffea --- /dev/null +++ b/src/plugins/rss/default/default-menu-screen.svg @@ -0,0 +1,131 @@ + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + ${title} + + + + + + + diff --git a/src/plugins/rss/default/g19-menu-entry.svg b/src/plugins/rss/default/g19-menu-entry.svg new file mode 100644 index 0000000..51edfac --- /dev/null +++ b/src/plugins/rss/default/g19-menu-entry.svg @@ -0,0 +1,161 @@ + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + ${ent_title} + ${ent_month_year} ${ent_time} + + diff --git a/src/plugins/rss/default/g19-menu-screen.svg b/src/plugins/rss/default/g19-menu-screen.svg new file mode 100644 index 0000000..aab80cd --- /dev/null +++ b/src/plugins/rss/default/g19-menu-screen.svg @@ -0,0 +1,245 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + ${title} + ${subtitle} + ${updated} + + + + + + + There are no news items in this feed + + diff --git a/src/plugins/rss/default/mx5500-menu-entry.svg b/src/plugins/rss/default/mx5500-menu-entry.svg new file mode 100644 index 0000000..25d1c7d --- /dev/null +++ b/src/plugins/rss/default/mx5500-menu-entry.svg @@ -0,0 +1,215 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + ${ent_month_year} ${ent_time_24} + ${ent_title} + + + + + + ${ent_month_year} ${ent_time_24} + ${ent_title} + + + diff --git a/src/plugins/rss/i18n/rss.en_GB.po b/src/plugins/rss/i18n/rss.en_GB.po new file mode 100644 index 0000000..bdd1e08 --- /dev/null +++ b/src/plugins/rss/i18n/rss.en_GB.po @@ -0,0 +1,46 @@ +# English translations for gnome15-plugins package. +# Copyright (C) 2012 THE gnome15-plugins'S COPYRIGHT HOLDER +# This file is distributed under the same license as the gnome15-plugins package. +# Automatically generated, 2012. +# +msgid "" +msgstr "" +"Project-Id-Version: gnome15-plugins 0.8.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: 2012-01-08 11:23+0000\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: en_GB\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=ASCII\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: i18n/rss.glade.h:1 +msgid "Feeds" +msgstr "Feeds" + +#: i18n/rss.glade.h:2 +msgid "Options" +msgstr "Options" + +#: i18n/rss.glade.h:3 +msgid "RSS Preferences" +msgstr "RSS Preferences" + +#: i18n/rss.glade.h:4 +msgid "Update every" +msgstr "Update every" + +#: i18n/rss.glade.h:5 +msgid "minutes" +msgstr "minutes" + +#: i18n/rss.glade.h:6 +msgid "toolbutton1" +msgstr "toolbutton1" + +#: i18n/rss.glade.h:7 +msgid "toolbutton2" +msgstr "toolbutton2" diff --git a/src/plugins/rss/i18n/rss.glade.h b/src/plugins/rss/i18n/rss.glade.h new file mode 100644 index 0000000..66ea145 --- /dev/null +++ b/src/plugins/rss/i18n/rss.glade.h @@ -0,0 +1,7 @@ +char *s = N_("Feeds"); +char *s = N_("Options"); +char *s = N_("RSS Preferences"); +char *s = N_("Update every"); +char *s = N_("minutes"); +char *s = N_("toolbutton1"); +char *s = N_("toolbutton2"); diff --git a/src/plugins/rss/i18n/rss.pot b/src/plugins/rss/i18n/rss.pot new file mode 100644 index 0000000..1350b73 --- /dev/null +++ b/src/plugins/rss/i18n/rss.pot @@ -0,0 +1,46 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: i18n/rss.glade.h:1 +msgid "Feeds" +msgstr "" + +#: i18n/rss.glade.h:2 +msgid "Options" +msgstr "" + +#: i18n/rss.glade.h:3 +msgid "RSS Preferences" +msgstr "" + +#: i18n/rss.glade.h:4 +msgid "Update every" +msgstr "" + +#: i18n/rss.glade.h:5 +msgid "minutes" +msgstr "" + +#: i18n/rss.glade.h:6 +msgid "toolbutton1" +msgstr "" + +#: i18n/rss.glade.h:7 +msgid "toolbutton2" +msgstr "" diff --git a/src/plugins/rss/rss.py b/src/plugins/rss/rss.py new file mode 100644 index 0000000..ea105d2 --- /dev/null +++ b/src/plugins/rss/rss.py @@ -0,0 +1,379 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2010 Brett Smith +# +# 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 . + +import gnome15.g15locale as g15locale +_ = g15locale.get_translation("rss", modfile = __file__).ugettext + +import gnome15.util.g15convert as g15convert +import gnome15.util.g15pythonlang as g15pythonlang +import gnome15.util.g15scheduler as g15scheduler +import gnome15.util.g15uigconf as g15uigconf +import gnome15.util.g15gconf as g15gconf +import gnome15.util.g15cairo as g15cairo +import gnome15.util.g15icontools as g15icontools +import gnome15.g15theme as g15theme +import gnome15.g15driver as g15driver +import gnome15.g15desktop as g15desktop +import subprocess +import time +import os +import feedparser +import gtk +import gconf +import logging +logger = logging.getLogger(__name__) + +# Plugin details - All of these must be provided +id = "rss" +name = _("RSS") +description = _("A simple RSS reader. Multiple feeds may be added, with a screen being \ +allocated to each one once it has loaded.\n\n\ +\ +Warning: This plugin has a small memory leak. If you experience problems, \ +try reducing the update interval time.") +author = "Brett Smith " +copyright = _("Copyright (C)2010 Brett Smith") +site = "http://www.russo79.com/gnome15" +has_preferences = True +needs_network = True +unsupported_models = [ g15driver.MODEL_G110, g15driver.MODEL_G11, g15driver.MODEL_G930, g15driver.MODEL_G35 ] +actions={ + g15driver.PREVIOUS_SELECTION : _("Previous news item"), + g15driver.NEXT_SELECTION : _("Next news items"), + g15driver.NEXT_PAGE : _("Next page"), + g15driver.PREVIOUS_PAGE : _("Previous page"), + g15driver.SELECT : _("Open item in browser") + } + +def create(gconf_key, gconf_client, screen): + return G15RSS(gconf_client, gconf_key, screen) + +def show_preferences(parent, driver, gconf_client, gconf_key): + G15RSSPreferences(parent, driver, gconf_client, gconf_key) + +def changed(widget, key, gconf_client): + gconf_client.set_bool(key, widget.get_active()) + +class G15RSSPreferences(): + + def __init__(self, parent, driver, gconf_client, gconf_key): + self._gconf_client = gconf_client + self._gconf_key = gconf_key + + widget_tree = gtk.Builder() + widget_tree.add_from_file(os.path.join(os.path.dirname(__file__), "rss.ui")) + + # Feeds + self.feed_model = widget_tree.get_object("FeedModel") + self.reload_model() + self.feed_list = widget_tree.get_object("FeedList") + self.url_renderer = widget_tree.get_object("URLRenderer") + + # Optins + self.update_adjustment = widget_tree.get_object("UpdateAdjustment") + self.update_adjustment.set_value(g15gconf.get_int_or_default(self._gconf_client, "%s/update_time" % self._gconf_key, 60)) + g15uigconf.configure_checkbox_from_gconf(gconf_client, "%s/twenty_four_hour_times" % gconf_key, "TwentyFourHourTimes", True, widget_tree) + + # Connect to events + self.update_adjustment.connect("value-changed", self.update_time_changed) + self.url_renderer.connect("edited", self.url_edited) + widget_tree.get_object("NewURL").connect("clicked", self.new_url) + widget_tree.get_object("RemoveURL").connect("clicked", self.remove_url) + + # Display + + # Show dialog + dialog = widget_tree.get_object("RSSDialog") + dialog.set_transient_for(parent) + + ah = gconf_client.notify_add(gconf_key + "/urls", self.urls_changed); + dialog.run() + dialog.hide() + gconf_client.notify_remove(ah); + + def update_time_changed(self, widget): + self._gconf_client.set_int(self._gconf_key + "/update_time", int(widget.get_value())) + + def url_edited(self, widget, row_index, value): + row = self.feed_model[row_index] + if value != "": + urls = self._gconf_client.get_list(self._gconf_key + "/urls", gconf.VALUE_STRING) + if row[0] in urls: + urls.remove(row[0]) + urls.append(value) + self._gconf_client.set_list(self._gconf_key + "/urls", gconf.VALUE_STRING, urls) + else: + self.feed_model.remove(self.feed_model.get_iter(row_index)) + + def urls_changed(self, client, connection_id, entry, args): + self.reload_model() + + def reload_model(self): + self.feed_model.clear() + for url in self._gconf_client.get_list(self._gconf_key + "/urls", gconf.VALUE_STRING): + self.feed_model.append([ url, True ]) + + def new_url(self, widget): + self.feed_model.append(["", True]) + self.feed_list.set_cursor_on_cell(str(len(self.feed_model) - 1), focus_column=self.feed_list.get_column(0), focus_cell=self.url_renderer, start_editing=True) + self.feed_list.grab_focus() + + def remove_url(self, widget): + (model, path) = self.feed_list.get_selection().get_selected() + url = model[path][0] + urls = self._gconf_client.get_list(self._gconf_key + "/urls", gconf.VALUE_STRING) + if url in urls: + urls.remove(url) + self._gconf_client.set_list(self._gconf_key + "/urls", gconf.VALUE_STRING, urls) + +class G15FeedsMenuItem(g15theme.MenuItem): + def __init__(self, component_id, entry, gconf_client, gconf_key): + g15theme.MenuItem.__init__(self, component_id) + self.entry = entry + self.gconf_client = gconf_client + self.gconf_key = gconf_key + if "icon" in self.entry: + self.icon = self.entry["icon"] + elif "image" in self.entry: + img = self.entry["image"] + if "url" in img: + self.icon = img["url"] + elif "link" in img: + self.icon = img["link"] + else: + self.icon = None + + def on_configure(self): + self.set_theme(g15theme.G15Theme(self.parent.get_theme().dir, "menu-entry")) + + def get_theme_properties(self): + + use_twenty_four_hour = g15gconf.get_bool_or_default(self.gconf_client, "%s/twenty_four_hour_times" % self.gconf_key, True) + + element_properties = g15theme.MenuItem.get_theme_properties(self) + element_properties["ent_title"] = self.entry.title + element_properties["ent_link"] = self.entry.link + if g15pythonlang.attr_exists(self.entry, "description"): + element_properties["ent_description"] = self.entry.description + + if hasattr(self.entry, 'date_parsed'): + dt = self.entry.date_parsed + elif hasattr(self.entry, 'published_parsed'): + logger.debug("Could not get date_parsed attribute. Trying published_parsed") + dt = self.entry.published_parsed + else: + logger.debug("Could not get publish_parsed attribute. Using current time.") + dt = time.localtime() + + element_properties["ent_locale_date_time"] = time.strftime("%x %X", dt) + element_properties["ent_locale_time"] = time.strftime("%X", dt) + element_properties["ent_locale_date"] = time.strftime("%x", dt) + element_properties["ent_time_24"] = time.strftime("%H:%M", dt) + if use_twenty_four_hour: + element_properties["ent_time"] = g15locale.format_time_24hour(time, self.gconf_client, False) + else: + element_properties["ent_time"] = g15locale.format_time(time, self.gconf_client, False) + element_properties["ent_full_time_24"] = time.strftime("%H:%M:%S", dt) + if use_twenty_four_hour: + element_properties["ent_full_time"] = g15locale.format_time_24hour(time, self.gconf_client, True) + else: + element_properties["ent_full_time"] = g15locale.format_time(time, self.gconf_client, True) + element_properties["ent_time_12"] = time.strftime("%I:%M %p", dt) + element_properties["ent_full_time_12"] = time.strftime("%I:%M:%S %p", dt) + element_properties["ent_short_date"] = time.strftime("%a %d %b", dt) + element_properties["ent_full_date"] = time.strftime("%A %d %B", dt) + element_properties["ent_month_year"] = time.strftime("%m/%y", dt) + + return element_properties + + def activate(self): + g15desktop.browse(self.entry.link) + return True + +class G15FeedPage(g15theme.G15Page): + + def __init__(self, plugin, url): + + self._gconf_client = plugin._gconf_client + self._gconf_key = plugin._gconf_key + self._screen = plugin._screen + self._icon_surface = None + self._icon_embedded = None + self._selected_icon_embedded = None + self.url = url + self.index = -1 + self._menu = g15theme.Menu("menu") + self._menu.on_selected = self._on_selected + g15theme.G15Page.__init__(self, "Feed " + str(plugin._page_serial), self._screen, + thumbnail_painter=self._paint_thumbnail, + theme=g15theme.G15Theme(self, "menu-screen"), + theme_properties_callback=self._get_theme_properties, + originating_plugin = plugin) + self.add_child(self._menu) + self.add_child(g15theme.MenuScrollbar("viewScrollbar", self._menu)) + plugin._page_serial += 1 + self._reload() + self._screen.add_page(self) + self._screen.redraw(self) + + """ + Private + """ + def _on_selected(self): + self._selected_icon_embedded = None + if self._menu.selected is not None and self._menu.selected.icon is not None: + try : + icon_surface = g15cairo.load_surface_from_file(self._menu.selected.icon) + self._selected_icon_embedded = g15icontools.get_embedded_image_url(icon_surface) + except Exception as e: + logger.warning("Failed to get icon %s", str(self._menu.selected.icon), exc_info = e) + + def _reload(self): + self.feed = feedparser.parse(self.url) + icon = None + if "icon" in self.feed["feed"]: + icon = self.feed["feed"]["icon"] + elif "image" in self.feed["feed"]: + img = self.feed["feed"]["image"] + if "url" in img: + icon = img["url"] + elif "link" in img: + icon = img["link"] + + title = self.feed["feed"]["title"] if "title" in self.feed["feed"] else self.url + if icon is None and title.endswith("- Twitter Search"): + title = title[:-16] + icon = g15icontools.get_icon_path("gnome15") + if icon is None: + icon = g15icontools.get_icon_path(["application-rss+xml","gnome-mime-application-rss+xml"], self._screen.height) + + if icon == None: + self._icon_surface = None + self._icon_embedded = None + else: + try : + icon_surface = g15cairo.load_surface_from_file(icon) + self._icon_surface = icon_surface + self._icon_embedded = g15icontools.get_embedded_image_url(icon_surface) + except Exception as e: + logger.warning("Failed to get icon %s", str(icon), exc_info = e) + self._icon_surface = None + self._icon_embedded = None + self.set_title(title) + self._subtitle = self.feed["feed"]["subtitle"] if "subtitle" in self.feed["feed"] else "" + self._menu.remove_all_children() + i = 0 + for entry in self.feed.entries: + self._menu.add_child(G15FeedsMenuItem("feeditem-%d" % i, entry, self._gconf_client, self._gconf_key)) + i += 1 + + def _get_theme_properties(self): + properties = {} + properties["title"] = self.title + if self._selected_icon_embedded is not None: + properties["icon"] = self._selected_icon_embedded + else: + properties["icon"] = self._icon_embedded + properties["subtitle"] = self._subtitle + properties["no_news"] = self._menu.get_child_count() == 0 + properties["alt_title"] = "" + try: + update_time = self.feed.updated + if isinstance(self.feed.updated, str): + update_time = self.feed.updated_parsed + + properties["updated"] = "%s %s" % (time.strftime("%H:%M", update_time), time.strftime("%a %d %b", update_time)) + except AttributeError as ae: + logger.debug("Could not get attribute", exc_info = ae) + pass + return properties + + def _paint_thumbnail(self, canvas, allocated_size, horizontal): + if self._icon_surface: + return g15cairo.paint_thumbnail_image(allocated_size, self._icon_surface, canvas) + +class G15RSS(): + + def __init__(self, gconf_client, gconf_key, screen): + self._screen = screen; + self._gconf_key = gconf_key + self._gconf_client = gconf_client + self._page_serial = 1 + self._refresh_timer = None + + def activate(self): + self._pages = {} + self._schedule_refresh() + self._update_time_changed_handle = self._gconf_client.notify_add(self._gconf_key + "/update_time", self._update_time_changed) + self._urls_changed_handle = self._gconf_client.notify_add(self._gconf_key + "/urls", self._urls_changed) + g15scheduler.schedule("LoadFeeds", 0, self._load_feeds) + + def deactivate(self): + self._cancel_refresh() + self._gconf_client.notify_remove(self._update_time_changed_handle); + self._gconf_client.notify_remove(self._urls_changed_handle); + for page in self._pages: + self._screen.del_page(self._pages[page]) + self._pages = {} + + ''' + Private + ''' + + def _schedule_refresh(self): + schedule_seconds = g15gconf.get_int_or_default(self._gconf_client, "%s/update_time" % self._gconf_key, 60) * 60.0 + self._refresh_timer = g15scheduler.schedule("FeedRefreshTimer", schedule_seconds, self._refresh) + + def _refresh(self): + logger.info("Refreshing RSS feeds") + for page_id in list(self._pages): + page = self._pages[page_id] + page._reload() + page.redraw() + self._schedule_refresh() + + def destroy(self): + pass + + def _update_time_changed(self, client, connection_id, entry, args): + self._cancel_refresh() + self._schedule_refresh() + + def _cancel_refresh(self): + if self._refresh_timer: + self._refresh_timer.cancel() + + def _urls_changed(self, client, connection_id, entry, args): + self._load_feeds() + + def _load_feeds(self): + feed_list = self._gconf_client.get_list(self._gconf_key + "/urls", gconf.VALUE_STRING) + + # Add new pages + for url in feed_list: + if not url in self._pages: + self._pages[url] = G15FeedPage(self, url) + + # Remove pages that no longer exist + to_remove = [] + for page_url in self._pages: + page = self._pages[page_url] + if not page.url in feed_list: + self._screen.del_page(page) + to_remove.append(page_url) + for page in to_remove: + del self._pages[page] + diff --git a/src/plugins/rss/rss.ui b/src/plugins/rss/rss.ui new file mode 100644 index 0000000..24d48d5 --- /dev/null +++ b/src/plugins/rss/rss.ui @@ -0,0 +1,299 @@ + + + + + + + + + + + + + + 320 + False + 5 + RSS Preferences + False + True + center-on-parent + dialog + + + True + False + 2 + + + True + False + end + + + gtk-close + True + True + True + True + + + False + False + 0 + + + + + False + True + end + 0 + + + + + True + False + + + True + False + + + True + False + 0 + none + + + True + False + 12 + + + True + False + + + True + False + + + True + False + toolbutton1 + True + gtk-add + + + False + True + + + + + True + False + toolbutton2 + True + gtk-remove + + + False + True + + + + + False + False + 0 + + + + + True + True + automatic + automatic + in + + + 200 + True + True + FeedModel + False + False + 0 + + + URL + + + + 1 + 0 + + + + + + + + + True + True + 1 + + + + + + + + + True + False + <b>Feeds</b> + True + + + + + True + True + 0 + + + + + True + False + 0 + none + + + True + False + 12 + + + True + False + 4 + + + True + False + + + True + False + Update every + + + True + True + 0 + + + + + True + True + + True + False + False + True + True + UpdateAdjustment + + + True + True + 1 + + + + + True + False + minutes + + + True + True + 2 + + + + + True + True + 0 + + + + + Show Time in 24 hour format + True + True + False + True + + + True + True + 1 + + + + + + + + + True + False + <b>Options</b> + True + + + + + True + True + 1 + + + + + True + True + 0 + + + + + False + False + 1 + + + + + + button9 + + + + 1 + 9999 + 1 + 1 + 1 + + diff --git a/src/plugins/runapp/Makefile.am b/src/plugins/runapp/Makefile.am new file mode 100644 index 0000000..ad40f83 --- /dev/null +++ b/src/plugins/runapp/Makefile.am @@ -0,0 +1,5 @@ +plugindir = $(datadir)/gnome15/plugins/runapp +plugin_DATA = runapp.py + +EXTRA_DIST = \ + $(plugin_DATA) diff --git a/src/plugins/runapp/background-160x43.png b/src/plugins/runapp/background-160x43.png new file mode 100644 index 0000000..e965e79 Binary files /dev/null and b/src/plugins/runapp/background-160x43.png differ diff --git a/src/plugins/runapp/background-320x240.png b/src/plugins/runapp/background-320x240.png new file mode 100644 index 0000000..f079f2c Binary files /dev/null and b/src/plugins/runapp/background-320x240.png differ diff --git a/src/plugins/runapp/default/default.svg b/src/plugins/runapp/default/default.svg new file mode 100644 index 0000000..3e1ac38 --- /dev/null +++ b/src/plugins/runapp/default/default.svg @@ -0,0 +1,248 @@ + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + ${temp_c} + + + + + + + ${sensor} + + + + 40 + 70 + 110 + 140 + 0 + + diff --git a/src/plugins/runapp/i18n/background.en_GB.po b/src/plugins/runapp/i18n/background.en_GB.po new file mode 100644 index 0000000..5db1e79 --- /dev/null +++ b/src/plugins/runapp/i18n/background.en_GB.po @@ -0,0 +1,66 @@ +# English translations for gnome15-plugins package. +# Copyright (C) 2012 THE gnome15-plugins'S COPYRIGHT HOLDER +# This file is distributed under the same license as the gnome15-plugins package. +# Automatically generated, 2012. +# +msgid "" +msgstr "" +"Project-Id-Version: gnome15-plugins 0.8.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: 2012-01-08 11:23+0000\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: en_GB\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=ASCII\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: i18n/background.glade.h:1 +msgid "Allow macro profiles to override background" +msgstr "Allow macro profiles to override background" + +#: i18n/background.glade.h:2 +msgid "Background Image" +msgstr "Background Image" + +#: i18n/background.glade.h:3 +msgid "Center" +msgstr "Center" + +#: i18n/background.glade.h:4 +msgid "Scale" +msgstr "Scale" + +#: i18n/background.glade.h:5 +msgid "Select A Background" +msgstr "Select A Background" + +#: i18n/background.glade.h:6 +msgid "Stretch" +msgstr "Stretch" + +#: i18n/background.glade.h:7 +msgid "Style" +msgstr "Style" + +#: i18n/background.glade.h:8 +msgid "Tile" +msgstr "Tile" + +#: i18n/background.glade.h:9 +msgid "Use _image file" +msgstr "Use _image file" + +#: i18n/background.glade.h:10 +msgid "Wallpaper Preferences" +msgstr "Wallpaper Preferences" + +#: i18n/background.glade.h:11 +msgid "Zoom" +msgstr "Zoom" + +#: i18n/background.glade.h:12 +msgid "_Same as desktop background" +msgstr "_Same as desktop background" diff --git a/src/plugins/runapp/i18n/background.glade.h b/src/plugins/runapp/i18n/background.glade.h new file mode 100644 index 0000000..af34a46 --- /dev/null +++ b/src/plugins/runapp/i18n/background.glade.h @@ -0,0 +1,12 @@ +char *s = N_("Allow macro profiles to override background"); +char *s = N_("Background Image"); +char *s = N_("Center"); +char *s = N_("Scale"); +char *s = N_("Select A Background"); +char *s = N_("Stretch"); +char *s = N_("Style"); +char *s = N_("Tile"); +char *s = N_("Use _image file"); +char *s = N_("Wallpaper Preferences"); +char *s = N_("Zoom"); +char *s = N_("_Same as desktop background"); diff --git a/src/plugins/runapp/i18n/background.pot b/src/plugins/runapp/i18n/background.pot new file mode 100644 index 0000000..b945edf --- /dev/null +++ b/src/plugins/runapp/i18n/background.pot @@ -0,0 +1,66 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: i18n/background.glade.h:1 +msgid "Allow macro profiles to override background" +msgstr "" + +#: i18n/background.glade.h:2 +msgid "Background Image" +msgstr "" + +#: i18n/background.glade.h:3 +msgid "Center" +msgstr "" + +#: i18n/background.glade.h:4 +msgid "Scale" +msgstr "" + +#: i18n/background.glade.h:5 +msgid "Select A Background" +msgstr "" + +#: i18n/background.glade.h:6 +msgid "Stretch" +msgstr "" + +#: i18n/background.glade.h:7 +msgid "Style" +msgstr "" + +#: i18n/background.glade.h:8 +msgid "Tile" +msgstr "" + +#: i18n/background.glade.h:9 +msgid "Use _image file" +msgstr "" + +#: i18n/background.glade.h:10 +msgid "Wallpaper Preferences" +msgstr "" + +#: i18n/background.glade.h:11 +msgid "Zoom" +msgstr "" + +#: i18n/background.glade.h:12 +msgid "_Same as desktop background" +msgstr "" diff --git a/src/plugins/runapp/runapp.py b/src/plugins/runapp/runapp.py new file mode 100644 index 0000000..e6d3579 --- /dev/null +++ b/src/plugins/runapp/runapp.py @@ -0,0 +1,149 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2010 Brett Smith +# +# 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 . + +import gnome15.g15locale as g15locale +_ = g15locale.get_translation("rundapp", modfile = __file__).ugettext + +import gnome15.util.g15convert as g15convert +import gnome15.util.g15uigconf as g15uigconf +import gnome15.util.g15gconf as g15gconf +import gnome15.util.g15cairo as g15cairo +import gnome15.g15driver as g15driver +import gnome15.g15screen as g15screen +import gnome15.g15profile as g15profile +import gnome15.g15desktop as g15desktop +import cairo +import gtk +import os +import logging +import gconf +from lxml import etree +logger = logging.getLogger(__name__) + +# Plugin details - All of these must be provided +id="rundapp" +name=_("aaaa") +description=_("Mi app en la pantalla") +author=_("Gustavo Adolfo Mesa Roldan ") +copyright="Copyright (C)2020 Brett Gustavo Adolfo Mesa Roldan" +site="http://hatthieves.es" +has_preferences=False +unsupported_models = [ g15driver.MODEL_G110, g15driver.MODEL_G11, g15driver.MODEL_G930, g15driver.MODEL_G35 ] + +time_2 = 0 +time_2 = time_2 + 1 + +def create(gconf_key, gconf_client, screen): + return G19app(gconf_key, gconf_client, screen) + +class G19appPainter(g15screen.Painter): + def __init__(self, screen): + g15screen.Painter.__init__(self, g15screen.BACKGROUND_PAINTER, -9999) + self.background_image = None + self.brightness = 0 + self._screen = screen + + def paint(self, canvas): + + print("1111111111111") + if self.background_image != None: + canvas.set_source_surface(self.background_image, 0.0, 0.0) + canvas.paint() + if self.brightness > 0: + canvas.set_source_rgba(1.0, 1.0, 1.0, ( self.brightness / 100.0 )) + else: + canvas.set_source_rgba(0.0, 0.0, 0.0, ( abs(self.brightness) / 100.0 )) + size = self._screen.device.lcd_size + canvas.rectangle(0,0,size[0],size[1]) + canvas.fill() + +class G19app(): + def __init__(self, gconf_key, gconf_client, screen): + self.screen = screen + self.gconf_client = gconf_client + self.gconf_key = gconf_key + self.target_surface = None + self.target_context = None + self.gconf_client.add_dir('/desktop/gnome/background', gconf.CLIENT_PRELOAD_NONE) + + def refresh(self): + print("2222222222222222") + self.painter = G19appPainter(self.screen) + + def activate(self): + self.painter = G19appPainter(self.screen) + self.notify_handlers = [] + self.screen.painters.append(self.painter) + self.notify_handlers.append(self.gconf_client.notify_add(self.gconf_key + "/path", self.config_changed)) + self.notify_handlers.append(self.gconf_client.notify_add(self.gconf_key + "/type", self.config_changed)) + self.notify_handlers.append(self.gconf_client.notify_add(self.gconf_key + "/style", self.config_changed)) + self.notify_handlers.append(self.gconf_client.notify_add(self.gconf_key + "/brightness", self.config_changed)) + self.notify_handlers.append(self.gconf_client.notify_add("/apps/gnome15/%s/active_profile" % self.screen.device.uid, self._active_profile_changed)) + + g15profile.profile_listeners.append(self._profiles_changed) + self._do_config_changed() + + def deactivate(self): + g15profile.profile_listeners.remove(self._profiles_changed) + self.screen.painters.remove(self.painter) + self.screen.redraw() + + def config_changed(self, client, connection_id, entry, args): + self._do_config_changed() + + def destroy(self): + pass + + ''' + Private + ''' + def _active_profile_changed(self, client, connection_id, entry, args): + self._do_config_changed() + + def _profiles_changed(self, profile_id, device_uid): + self._do_config_changed() + + def _do_config_changed(self): + print("2222222222222222") + screen_size = self.screen.size + surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, screen_size[0], screen_size[1]) + context = cairo.Context(surface) + context.scale(screen_size[0], screen_size[1]) + + context.select_font_face("Arial", cairo.FONT_SLANT_NORMAL, cairo.FontWeight.BOLD) + context.move_to(0.1, 0.1) + context.show_text("Paner Chupala") + + context.set_line_width(0.04) + context.move_to(0.1, 0.5) + context.curve_to(0.4, 0.9, 0.6, 0.1, 0.9, 0.5) + context.stroke() + context.set_source_rgba(1, 0.2, 0.2, 0.6) + context.set_line_width(0.02) + context.move_to(0.1, 0.5) + context.line_to(0.4, 0.9) + context.move_to(0.6, 0.1) + context.line_to(0.9, 0.5) + context.stroke() + context.save() + context.paint() + context.restore() + self.painter.background_image = surface + self.painter.brightness = self.gconf_client.get_int(self.gconf_key + "/brightness") + self.screen.redraw() + + + diff --git a/src/plugins/screensaver/Makefile.am b/src/plugins/screensaver/Makefile.am new file mode 100644 index 0000000..45d5bf4 --- /dev/null +++ b/src/plugins/screensaver/Makefile.am @@ -0,0 +1,7 @@ +SUBDIRS = default +plugindir = $(datadir)/gnome15/plugins/screensaver +plugin_DATA = screensaver.py \ + screensaver.ui + +EXTRA_DIST = \ + $(plugin_DATA) \ No newline at end of file diff --git a/src/plugins/screensaver/default/Makefile.am b/src/plugins/screensaver/default/Makefile.am new file mode 100644 index 0000000..ce388f6 --- /dev/null +++ b/src/plugins/screensaver/default/Makefile.am @@ -0,0 +1,10 @@ +themedir = $(datadir)/gnome15/plugins/screensaver/default +theme_DATA = default.svg \ + default-nobody.svg \ + g19.svg \ + g19-nobody.svg \ + mx5500.svg \ + mx5500-nobody.svg + +EXTRA_DIST = \ + $(theme_DATA) diff --git a/src/plugins/screensaver/default/default-nobody.svg b/src/plugins/screensaver/default/default-nobody.svg new file mode 100644 index 0000000..d81142c --- /dev/null +++ b/src/plugins/screensaver/default/default-nobody.svg @@ -0,0 +1,94 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + ${title} + + diff --git a/src/plugins/screensaver/default/default.svg b/src/plugins/screensaver/default/default.svg new file mode 100644 index 0000000..4659567 --- /dev/null +++ b/src/plugins/screensaver/default/default.svg @@ -0,0 +1,122 @@ + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + ${body} + ${title} + + + diff --git a/src/plugins/screensaver/default/g19-nobody.svg b/src/plugins/screensaver/default/g19-nobody.svg new file mode 100644 index 0000000..ff40ce9 --- /dev/null +++ b/src/plugins/screensaver/default/g19-nobody.svg @@ -0,0 +1,122 @@ + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + diff --git a/src/plugins/screensaver/default/g19.svg b/src/plugins/screensaver/default/g19.svg new file mode 100644 index 0000000..706461f --- /dev/null +++ b/src/plugins/screensaver/default/g19.svg @@ -0,0 +1,172 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + ${body} + + diff --git a/src/plugins/screensaver/default/mx5500-nobody.svg b/src/plugins/screensaver/default/mx5500-nobody.svg new file mode 100644 index 0000000..86fac04 --- /dev/null +++ b/src/plugins/screensaver/default/mx5500-nobody.svg @@ -0,0 +1,103 @@ + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + ${title} + + diff --git a/src/plugins/screensaver/default/mx5500.svg b/src/plugins/screensaver/default/mx5500.svg new file mode 100644 index 0000000..03f06c9 --- /dev/null +++ b/src/plugins/screensaver/default/mx5500.svg @@ -0,0 +1,144 @@ + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + ${body} + ${title} + + + diff --git a/src/plugins/screensaver/i18n/screensaver.en_GB.po b/src/plugins/screensaver/i18n/screensaver.en_GB.po new file mode 100644 index 0000000..e1fd7c7 --- /dev/null +++ b/src/plugins/screensaver/i18n/screensaver.en_GB.po @@ -0,0 +1,34 @@ +# English translations for gnome15-plugins package. +# Copyright (C) 2012 THE gnome15-plugins'S COPYRIGHT HOLDER +# This file is distributed under the same license as the gnome15-plugins package. +# Automatically generated, 2012. +# +msgid "" +msgstr "" +"Project-Id-Version: gnome15-plugins 0.8.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: 2012-01-08 11:23+0000\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: en_GB\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=ASCII\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: i18n/screensaver.glade.h:1 +msgid "Message" +msgstr "Message" + +#: i18n/screensaver.glade.h:2 +msgid "Options" +msgstr "Options" + +#: i18n/screensaver.glade.h:3 +msgid "Dim Controls" +msgstr "Dim Controls" + +#: i18n/screensaver.glade.h:4 +msgid "Screensaver Preferences" +msgstr "Screensaver Preferences" diff --git a/src/plugins/screensaver/i18n/screensaver.glade.h b/src/plugins/screensaver/i18n/screensaver.glade.h new file mode 100644 index 0000000..b665e5e --- /dev/null +++ b/src/plugins/screensaver/i18n/screensaver.glade.h @@ -0,0 +1,4 @@ +char *s = N_("Message"); +char *s = N_("Options"); +char *s = N_("Dim Controls"); +char *s = N_("Screensaver Preferences"); diff --git a/src/plugins/screensaver/i18n/screensaver.pot b/src/plugins/screensaver/i18n/screensaver.pot new file mode 100644 index 0000000..b70e60d --- /dev/null +++ b/src/plugins/screensaver/i18n/screensaver.pot @@ -0,0 +1,34 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: i18n/screensaver.glade.h:1 +msgid "Message" +msgstr "" + +#: i18n/screensaver.glade.h:2 +msgid "Options" +msgstr "" + +#: i18n/screensaver.glade.h:3 +msgid "Dim Controls" +msgstr "" + +#: i18n/screensaver.glade.h:4 +msgid "Screensaver Preferences" +msgstr "" diff --git a/src/plugins/screensaver/screensaver.py b/src/plugins/screensaver/screensaver.py new file mode 100644 index 0000000..52dc422 --- /dev/null +++ b/src/plugins/screensaver/screensaver.py @@ -0,0 +1,219 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2010 Brett Smith +# +# 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 . + +import gnome15.g15locale as g15locale +_ = g15locale.get_translation("screensaver", modfile = __file__).ugettext + +import gnome15.g15screen as g15screen +import gnome15.g15driver as g15driver +import gnome15.util.g15uigconf as g15uigconf +import gnome15.util.g15gconf as g15gconf +import gnome15.util.g15icontools as g15icontools +import gnome15.g15theme as g15theme +from threading import Timer +import gtk +import dbus +import logging +import os.path +logger = logging.getLogger(__name__) + +# Plugin details - All of these must be provided +id="screensaver" +name=_("Screensaver") +description=_("Dim the keyboard and display a message (on models with an LCD screen) when the desktop screen saver activates.") +author="Brett Smith " +copyright=_("Copyright (C)2010 Brett Smith") +site="http://www.russo79.com/gnome15" +has_preferences=True +unsupported_models = [ g15driver.MODEL_G930, g15driver.MODEL_G35 ] + + +''' +This plugin displays a high priority screen when the screensaver activates +''' + +def create(gconf_key, gconf_client, screen): + return G15ScreenSaver(gconf_key, gconf_client, screen) + +def show_preferences(parent, driver, gconf_client, gconf_key): + widget_tree = gtk.Builder() + widget_tree.add_from_file(os.path.join(os.path.dirname(__file__), "screensaver.ui")) + + dialog = widget_tree.get_object("ScreenSaverDialog") + dialog.set_transient_for(parent) + + g15uigconf.configure_checkbox_from_gconf(gconf_client, "%s/dim_keyboard" % gconf_key,"DimKeyboardCheckbox", True, widget_tree) + + if driver.get_bpp() == 0: + widget_tree.get_object("MessageFrame").hide() + + text_buffer = widget_tree.get_object("TextBuffer") + text = gconf_client.get_string(gconf_key + "/message_text") + if text == None: + text = "" + text_buffer.set_text(text) + text_h = text_buffer.connect("changed", changed, gconf_key + "/message_text", gconf_client) + + dialog.run() + dialog.hide() + text_buffer.disconnect(text_h) + +def changed(widget, key, gconf_client): + if key.endswith("/dim_keyboard"): + gconf_client.set_bool(key, widget.get_active()) + else: + bounds = widget.get_bounds() + gconf_client.set_string(key, widget.get_text(bounds[0],bounds[1])) + pass + +class G15ScreenSaver(): + + def __init__(self, gconf_key, gconf_client, screen): + self._screen = screen + self._session_bus = None + self._in_screensaver = False + self._page = None + self._gconf_client = gconf_client + self._gconf_key = gconf_key + self.dimmed = False + + def activate(self): + self._controls = [] + self._control_values = [] + for control in self._screen.driver.get_controls(): + if control.hint & g15driver.HINT_DIMMABLE != 0 or control.hint & g15driver.HINT_SHADEABLE != 0: + self._controls.append(control) + self._dbus_name = "org.gnome.ScreenSaver" + self._dbus_interface = "org.gnome.ScreenSaver" + self._in_screen_saver = False + + if self._session_bus == None: + screen_saver = None + try: + self._session_bus = dbus.SessionBus() + except Exception as e: + self._session_bus = None + logger.error("Error. Retrying in 10 seconds", exc_info = e) + Timer(10, self.activate, ()).start() + return + + # Paths vary from desktop to desktop + screensavers = [ + ("org.gnome.ScreenSaver", "org.gnome.ScreenSaver", "/"), + ("org.gnome.ScreenSaver", "org.gnome.ScreenSaver", "/org/gnome/ScreenSaver"), + ("org.kde.screensaver", "org.freedesktop.ScreenSaver", "/ScreenSaver"), + ("org.mate.ScreenSaver", "org.mate.ScreenSaver", "/"), + ] + + for dbus_name, interface, path in screensavers: + try : + logger.debug("Searching for screensaver. " \ + "dbus_name: %s, dbus_interface: %s, dbus_object: %s", + dbus_name, + interface, + path) + screen_saver = dbus.Interface(self._session_bus.get_object(dbus_name, path), interface) + self._dbus_interface = interface + self._dbus_name = dbus_name + self._session_bus.add_signal_receiver(self._screensaver_changed_handler, dbus_interface = self._dbus_interface, signal_name = "ActiveChanged") + self._in_screensaver = screen_saver.GetActive() + break + except Exception as e: + logger.debug("Could not find screensaver", exc_info = e) + screen_saver = None + pass + + if screen_saver is None: + raise Exception("No supported DBUS screen saver interface found.") + + self._activated = True + self._check_page() + + def deactivate(self): + if self._in_screensaver: + if self._gconf_client.get_bool(self._gconf_key + "/dim_keyboard"): + self._light_keyboard() + self._remove_page() + self._activated = False + + def destroy(self): + if self._session_bus: + self._session_bus.remove_signal_receiver(self._screensaver_changed_handler, dbus_interface = self._dbus_interface, signal_name = "ActiveChanged") + + def handle_key(self, keys, state, post): + # Sinks all keyboard events when the page is active + return self._page is not None + + ''' Functions specific to plugin + ''' + + def _remove_page(self): + if self._page != None: + self._screen.del_page(self._page) + self._page = None + + def _check_page(self): + if self._in_screensaver: + if self._screen.driver.get_bpp() != 0 and self._page == None: + self._reload_theme() + self._page = g15theme.G15Page(id, self._screen, priority = g15screen.PRI_EXCLUSIVE, \ + title = name, theme = self._theme, + theme_properties_callback = self._get_theme_properties, + originating_plugin = self) + self._page.key_handlers.append(self) + self._screen.add_page(self._page) + self._screen.redraw(self._page) + if not self.dimmed and g15gconf.get_bool_or_default(self._gconf_client, "%s/dim_keyboard" % self._gconf_key, True): + self._dim_keyboard() + else: + if self._screen.driver.get_bpp() != 0: + self._remove_page() + if self.dimmed and g15gconf.get_bool_or_default(self._gconf_client,"%s/dim_keyboard" % self._gconf_key, True): + self._light_keyboard() + + def _screensaver_changed_handler(self, value): + if self._activated: + self._in_screensaver = bool(value) + self._check_page() + + def _dim_keyboard(self): + self._acquisitions = [] + for c in self._controls: + acquisition = self._screen.driver.acquire_control(c, val = c.value) + self._acquisitions.append(acquisition) + acquisition.fade(100.0 if c.hint & g15driver.HINT_DIMMABLE != 0 else 0.5, 3.0) + self.dimmed = True + + def _light_keyboard(self): + for c in self._acquisitions: + self._screen.driver.release_control(c) + self.dimmed = False + + def _reload_theme(self): + text = self._gconf_client.get_string(self._gconf_key + "/message_text") + variant = "" + if text == None or text == "": + variant = "nobody" + self._theme = g15theme.G15Theme(self, variant) + + def _get_theme_properties(self): + + properties = {} + properties["title"] = _("Workstation Locked") + properties["body"] = self._gconf_client.get_string(self._gconf_key + "/message_text") + properties["icon"] = g15icontools.get_icon_path("sleep", self._screen.height) + + return properties diff --git a/src/plugins/screensaver/screensaver.ui b/src/plugins/screensaver/screensaver.ui new file mode 100644 index 0000000..ec9097f --- /dev/null +++ b/src/plugins/screensaver/screensaver.ui @@ -0,0 +1,153 @@ + + + + + + 100 + 1 + 10 + + + + 320 + False + 5 + Screensaver Preferences + False + True + center-on-parent + dialog + + + True + False + 2 + + + True + False + end + + + gtk-close + True + True + True + True + + + False + False + 0 + + + + + False + True + end + 0 + + + + + True + False + + + True + False + 0 + none + + + True + False + 12 + + + Dim Controls + True + True + False + True + + + + + + + True + False + <b>Options</b> + True + + + + + True + True + 0 + + + + + True + False + 0 + none + + + True + False + 12 + + + True + True + automatic + automatic + in + + + 60 + True + True + 1 + word-char + TextBuffer + + + + + + + + + True + False + <b>Message</b> + True + + + + + True + True + 1 + + + + + False + False + 1 + + + + + + button9 + + + diff --git a/src/plugins/sense/Makefile.am b/src/plugins/sense/Makefile.am new file mode 100644 index 0000000..1cab369 --- /dev/null +++ b/src/plugins/sense/Makefile.am @@ -0,0 +1,7 @@ +SUBDIRS = default +plugindir = $(datadir)/gnome15/plugins/sense +plugin_DATA = sense.py \ + sense.ui + +EXTRA_DIST = \ + $(plugin_DATA) \ No newline at end of file diff --git a/src/plugins/sense/default/Makefile.am b/src/plugins/sense/default/Makefile.am new file mode 100644 index 0000000..c59c59f --- /dev/null +++ b/src/plugins/sense/default/Makefile.am @@ -0,0 +1,14 @@ +themedir = $(datadir)/gnome15/plugins/sense/default +theme_DATA = g19.svg \ + g19-menu-entry.svg \ + g19-fan.svg \ + g19-volt.svg \ + g19-none.svg \ + default.svg \ + default-none.svg \ + default-fan.svg \ + default-volt.svg \ + default-menu-entry.svg + +EXTRA_DIST = \ + $(theme_DATA) diff --git a/src/plugins/sense/default/default-fan.svg b/src/plugins/sense/default/default-fan.svg new file mode 100644 index 0000000..63dc829 --- /dev/null +++ b/src/plugins/sense/default/default-fan.svg @@ -0,0 +1,254 @@ + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + ${rpm} RPM + + + + + + + ${sensor} + + + + 2000 + 3500 + 5000 + 7000 + 0 + + + diff --git a/src/plugins/sense/default/default-menu-entry.svg b/src/plugins/sense/default/default-menu-entry.svg new file mode 100644 index 0000000..7b6d7e9 --- /dev/null +++ b/src/plugins/sense/default/default-menu-entry.svg @@ -0,0 +1,272 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + ${item_name} + + + + + ${item_alt} + + + + ${item_name} + + + + + ${item_alt} + + diff --git a/src/plugins/sense/default/default-none.svg b/src/plugins/sense/default/default-none.svg new file mode 100644 index 0000000..acd2a08 --- /dev/null +++ b/src/plugins/sense/default/default-none.svg @@ -0,0 +1,100 @@ + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + _(No sensors enabled) + + diff --git a/src/plugins/sense/default/default-volt.svg b/src/plugins/sense/default/default-volt.svg new file mode 100644 index 0000000..c00ab08 --- /dev/null +++ b/src/plugins/sense/default/default-volt.svg @@ -0,0 +1,250 @@ + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + ${voltage} V + + + + + + + ${sensor} + + + + 4 + 7 + 12 + 14 + 0 + + diff --git a/src/plugins/sense/default/default.svg b/src/plugins/sense/default/default.svg new file mode 100644 index 0000000..3e1ac38 --- /dev/null +++ b/src/plugins/sense/default/default.svg @@ -0,0 +1,248 @@ + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + ${temp_c} + + + + + + + ${sensor} + + + + 40 + 70 + 110 + 140 + 0 + + diff --git a/src/plugins/sense/default/g19-fan.svg b/src/plugins/sense/default/g19-fan.svg new file mode 100644 index 0000000..e6113c7 --- /dev/null +++ b/src/plugins/sense/default/g19-fan.svg @@ -0,0 +1,1268 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0 + 1000 + 2000 + 3000 + + 4000 + 5000 + 6000 + 7000 + ${sensor} + ${rpm} RPM + + + + + + + + + + diff --git a/src/plugins/sense/default/g19-menu-entry.svg b/src/plugins/sense/default/g19-menu-entry.svg new file mode 100644 index 0000000..3f1a326 --- /dev/null +++ b/src/plugins/sense/default/g19-menu-entry.svg @@ -0,0 +1,353 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + ${item_name} + ${item_alt} + ${item_alt2} + Max + + + diff --git a/src/plugins/sense/default/g19-none.svg b/src/plugins/sense/default/g19-none.svg new file mode 100644 index 0000000..6e36f24 --- /dev/null +++ b/src/plugins/sense/default/g19-none.svg @@ -0,0 +1,693 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + _(No sensors enabled) + + diff --git a/src/plugins/sense/default/g19-volt.svg b/src/plugins/sense/default/g19-volt.svg new file mode 100644 index 0000000..97c4167 --- /dev/null +++ b/src/plugins/sense/default/g19-volt.svg @@ -0,0 +1,1269 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0 + 2 + 4 + 6 + + 8 + 10 + 12 + 14 + ${sensor} + ${voltage} V + + + + + + + + + + diff --git a/src/plugins/sense/default/g19.svg b/src/plugins/sense/default/g19.svg new file mode 100644 index 0000000..01395b6 --- /dev/null +++ b/src/plugins/sense/default/g19.svg @@ -0,0 +1,1358 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 10 + 0 + 20 + 30 + 40 + 50 + 60 + 70 + + 80 + 90 + 100 + 110 + 120 + 130 + 140 + ${sensor} + ${temp_c} + + + + + + + + + + diff --git a/src/plugins/sense/default/i18n/default-none.en_GB.po b/src/plugins/sense/default/i18n/default-none.en_GB.po new file mode 100644 index 0000000..e782e4e --- /dev/null +++ b/src/plugins/sense/default/i18n/default-none.en_GB.po @@ -0,0 +1,22 @@ +# English translations for gnome15-plugins package. +# Copyright (C) 2012 THE gnome15-plugins'S COPYRIGHT HOLDER +# This file is distributed under the same license as the gnome15-plugins package. +# Automatically generated, 2012. +# +msgid "" +msgstr "" +"Project-Id-Version: gnome15-plugins 0.8.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: 2012-01-08 11:23+0000\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: en_GB\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=ASCII\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: i18n/default-none.h:1 +msgid "No sensors enabled" +msgstr "No sensors enabled" diff --git a/src/plugins/sense/default/i18n/default-none.h b/src/plugins/sense/default/i18n/default-none.h new file mode 100644 index 0000000..e6440f7 --- /dev/null +++ b/src/plugins/sense/default/i18n/default-none.h @@ -0,0 +1 @@ +char *s = N_("No sensors enabled"); diff --git a/src/plugins/sense/default/i18n/default-none.pot b/src/plugins/sense/default/i18n/default-none.pot new file mode 100644 index 0000000..f9a507d --- /dev/null +++ b/src/plugins/sense/default/i18n/default-none.pot @@ -0,0 +1,22 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: i18n/default-none.h:1 +msgid "No sensors enabled" +msgstr "" diff --git a/src/plugins/sense/default/i18n/g19-none.en_GB.po b/src/plugins/sense/default/i18n/g19-none.en_GB.po new file mode 100644 index 0000000..982148c --- /dev/null +++ b/src/plugins/sense/default/i18n/g19-none.en_GB.po @@ -0,0 +1,22 @@ +# English translations for gnome15-plugins package. +# Copyright (C) 2012 THE gnome15-plugins'S COPYRIGHT HOLDER +# This file is distributed under the same license as the gnome15-plugins package. +# Automatically generated, 2012. +# +msgid "" +msgstr "" +"Project-Id-Version: gnome15-plugins 0.8.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: 2012-01-08 11:23+0000\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: en_GB\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=ASCII\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: i18n/g19-none.h:1 +msgid "No sensors enabled" +msgstr "No sensors enabled" diff --git a/src/plugins/sense/default/i18n/g19-none.h b/src/plugins/sense/default/i18n/g19-none.h new file mode 100644 index 0000000..e6440f7 --- /dev/null +++ b/src/plugins/sense/default/i18n/g19-none.h @@ -0,0 +1 @@ +char *s = N_("No sensors enabled"); diff --git a/src/plugins/sense/default/i18n/g19-none.pot b/src/plugins/sense/default/i18n/g19-none.pot new file mode 100644 index 0000000..eb4b48b --- /dev/null +++ b/src/plugins/sense/default/i18n/g19-none.pot @@ -0,0 +1,22 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: i18n/g19-none.h:1 +msgid "No sensors enabled" +msgstr "" diff --git a/src/plugins/sense/i18n/sense.en_GB.po b/src/plugins/sense/i18n/sense.en_GB.po new file mode 100644 index 0000000..a466e01 --- /dev/null +++ b/src/plugins/sense/i18n/sense.en_GB.po @@ -0,0 +1,46 @@ +# English translations for gnome15-plugins package. +# Copyright (C) 2012 THE gnome15-plugins'S COPYRIGHT HOLDER +# This file is distributed under the same license as the gnome15-plugins package. +# Automatically generated, 2012. +# +msgid "" +msgstr "" +"Project-Id-Version: gnome15-plugins 0.8.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: 2012-01-08 11:23+0000\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: en_GB\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=ASCII\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: i18n/sense.glade.h:1 +msgid "Options" +msgstr "Options" + +#: i18n/sense.glade.h:2 +msgid "Enabled" +msgstr "Enabled" + +#: i18n/sense.glade.h:3 +msgid "Label" +msgstr "Label" + +#: i18n/sense.glade.h:4 +msgid "Refresh interval" +msgstr "Refresh interval" + +#: i18n/sense.glade.h:5 +msgid "Sensors Preferences" +msgstr "Sensors Preferences" + +#: i18n/sense.glade.h:6 +msgid "Type" +msgstr "Type" + +#: i18n/sense.glade.h:7 +msgid "seconds" +msgstr "seconds" diff --git a/src/plugins/sense/i18n/sense.glade.h b/src/plugins/sense/i18n/sense.glade.h new file mode 100644 index 0000000..9b300f5 --- /dev/null +++ b/src/plugins/sense/i18n/sense.glade.h @@ -0,0 +1,7 @@ +char *s = N_("Options"); +char *s = N_("Enabled"); +char *s = N_("Label"); +char *s = N_("Refresh interval"); +char *s = N_("Sensors Preferences"); +char *s = N_("Type"); +char *s = N_("seconds"); diff --git a/src/plugins/sense/i18n/sense.pot b/src/plugins/sense/i18n/sense.pot new file mode 100644 index 0000000..45eee9d --- /dev/null +++ b/src/plugins/sense/i18n/sense.pot @@ -0,0 +1,46 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: i18n/sense.glade.h:1 +msgid "Options" +msgstr "" + +#: i18n/sense.glade.h:2 +msgid "Enabled" +msgstr "" + +#: i18n/sense.glade.h:3 +msgid "Label" +msgstr "" + +#: i18n/sense.glade.h:4 +msgid "Refresh interval" +msgstr "" + +#: i18n/sense.glade.h:5 +msgid "Sensors Preferences" +msgstr "" + +#: i18n/sense.glade.h:6 +msgid "Type" +msgstr "" + +#: i18n/sense.glade.h:7 +msgid "seconds" +msgstr "" diff --git a/src/plugins/sense/sense.py b/src/plugins/sense/sense.py new file mode 100644 index 0000000..6194ae2 --- /dev/null +++ b/src/plugins/sense/sense.py @@ -0,0 +1,566 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2011 Brett Smith +# Copyright (C) 2013 Brett Smith +# Nuno Araujo +# +# 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 . + +import gnome15.g15locale as g15locale +_ = g15locale.get_translation("sensors", modfile = __file__).ugettext + +import gnome15.g15driver as g15driver +import gnome15.g15plugin as g15plugin +import gnome15.g15theme as g15theme +import gnome15.util.g15gconf as g15gconf +import gnome15.util.g15svg as g15svg +import os.path +import dbus +import sensors +import gtk +import gconf +import gobject + +import subprocess +from threading import Lock + +# Logging +import logging +logger = logging.getLogger(__name__) + +id = "sense" +name = _("Sensors") +description = _("Display information from various sensors. The plugin \ +supports Temperatures, Fans and Voltages from various sources. \ +\n\n\ +Sources include libsensors, nvidiactl and UDisks.\ +\n\n\ +NOTE: UDisk may cause a delay in starting up Gnome15. This bug is\ +being investigated.") +author = "Brett Smith " +copyright = _("Copyright (C)2010 Brett Smith") +site = "http://www.gnome15.org" +has_preferences = True +unsupported_models = [ g15driver.MODEL_G110, g15driver.MODEL_G11, g15driver.MODEL_G930, g15driver.MODEL_G35 ] +actions={ + g15driver.PREVIOUS_SELECTION : _("Previous sensor"), + g15driver.NEXT_SELECTION : _("Next sensor"), + g15driver.NEXT_PAGE : _("Next page"), + g15driver.PREVIOUS_PAGE : _("Previous page") + } + +UDISKS_DEVICE_NAME = "org.freedesktop.UDisks.Device" +UDISKS_BUS_NAME= "org.freedesktop.UDisks" +UDISKS2_BUS_NAME= "org.freedesktop.UDisks2" + +''' +Sensor types +''' +VOLTAGE = 0 +TEMPERATURE = 2 +FAN = 1 +UNKNOWN_1 = 3 +INTRUSION = 17 + +TYPE_NAMES = { VOLTAGE: "Voltage", FAN: "Fan", TEMPERATURE : "Temp" } +VARIANT_NAMES = { VOLTAGE : "volt", TEMPERATURE : None, FAN : "fan", UNKNOWN_1 : "volt", INTRUSION: "intrusion" } + +''' +This plugin displays sensor information +''' + +def create(gconf_key, gconf_client, screen): + return G15Sensors(gconf_key, gconf_client, screen) + +def show_preferences(parent, driver, gconf_client, gconf_key): + G15SensorsPreferences(parent, driver, gconf_client, gconf_key) + +def get_sensor_sources(): + available_sensor_sources = [[LibsensorsSource()], \ + [NvidiaSource()], \ + [UDisks2Source(), UDisksSource()]] + sensor_sources = [] + for sensor_source_group in available_sensor_sources: + for candidade in sensor_source_group: + logger.info("Testing if '%s' is a valid sensor source", candidade.name) + try: + if candidade.is_valid(): + logger.info("Adding '%s' as a sensor source", candidade.name) + sensor_sources.append(candidade) + else: + candidade.stop() + break + except Exception as e: + logger.debug("Error when checking '%s'. Skipping", candidate.name, exc_info = e) + pass + + return sensor_sources + +class G15SensorsPreferences(): + + def __init__(self, parent, driver, gconf_client, gconf_key): + self._gconf_client = gconf_client + self._gconf_key = gconf_key + + widget_tree = gtk.Builder() + widget_tree.add_from_file(os.path.join(os.path.dirname(__file__), "sense.ui")) + + # Feeds + self.sensor_model = widget_tree.get_object("SensorModel") + self.reload_model() + self.sensor_list = widget_tree.get_object("SensorList") + self.enabled_renderer = widget_tree.get_object("EnabledRenderer") + self.label_renderer = widget_tree.get_object("LabelRenderer") + + # Lines + self.interval_adjustment = widget_tree.get_object("IntervalAdjustment") + self.interval_adjustment.set_value(g15gconf.get_float_or_default(self._gconf_client, "%s/interval" % self._gconf_key, 10)) + + # Connect to events + self.interval_adjustment.connect("value-changed", self.interval_changed) + self.label_renderer.connect("edited", self.label_edited) + self.enabled_renderer.connect("toggled", self.sensor_toggled) + + # Show dialog + self.dialog = widget_tree.get_object("SenseDialog") + self.dialog.set_transient_for(parent) + + self.dialog.run() + self.dialog.hide() + + def interval_changed(self, widget): + self._gconf_client.set_float(self._gconf_key + "/interval", int(widget.get_value())) + + def label_edited(self, widget, row_index, value): + row_index = int(row_index) + if value != "": + if self.sensor_model[row_index][2] != value: + self.sensor_model.set_value(self.sensor_model.get_iter(row_index), 2, value) + sensor_name = self.sensor_model[row_index][0] + self._gconf_client.set_string("%s/sensors/%s/label" % (self._gconf_key, gconf.escape_key(sensor_name, len(sensor_name))), value) + + def sensor_toggled(self, widget, row_index): + row_index = int(row_index) + now_active = not widget.get_active() + self.sensor_model.set_value(self.sensor_model.get_iter(row_index), 1, now_active) + sensor_name = self.sensor_model[row_index][0] + self._gconf_client.set_bool("%s/sensors/%s/enabled" % (self._gconf_key, gconf.escape_key(sensor_name, len(sensor_name))), now_active) + + def reload_model(self): + self.sensor_model.clear() + ss = get_sensor_sources() + for source in ss: + sa = source.get_sensors() + for sensor in sa: + sense_key = "%s/sensors/%s" % (self._gconf_key, gconf.escape_key(sensor.name, len(sensor.name))) + if sensor.sense_type in TYPE_NAMES: + self.sensor_model.append([ sensor.name, g15gconf.get_bool_or_default(self._gconf_client, "%s/enabled" % (sense_key), True), + g15gconf.get_string_or_default(self._gconf_client, "%s/label" % (sense_key), sensor.name), TYPE_NAMES[sensor.sense_type] ]) + source.stop() + + +class Sensor(): + + def __init__(self, sense_type, name, value, critical = None): + self.sense_type = sense_type + self.name = name + self.value = value + self.critical = critical + + def get_default_crit(self): + # Meaningless really, but more sensible than a single value + + if self.sense_type == FAN: + return 7000 + elif self.sense_type == VOLTAGE: + return 12 + else: + return 100 + + +class UDisksSource(): + + def __init__(self): + self.name = "UDisks" + self.udisks = None + self.system_bus = None + self.sensors = {} + self.lock = Lock() + + def get_sensors(self): + self.sensors = {} + for device in self.udisks.EnumerateDevices(): + udisk_object = self.system_bus.get_object(UDISKS_BUS_NAME, device) + udisk_properties = dbus.Interface(udisk_object, 'org.freedesktop.DBus.Properties') + + if udisk_properties.Get(UDISKS_DEVICE_NAME, "DriveAtaSmartIsAvailable"): + sensor_name = udisk_properties.Get(UDISKS_DEVICE_NAME, "DriveModel") + if sensor_name in self.sensors: + # TODO get something else unique? + n = udisk_properties.Get(UDISKS_DEVICE_NAME, "DeviceFile") + if n: + sensor_name += " (%s)" % n + else: + n = udisk_properties.Get(UDISKS_DEVICE_NAME, "DriveSerial") + if n: + sensor_name += " (%s)" % n + sensor = Sensor(TEMPERATURE, sensor_name, 0.0) + device_file = str(udisk_properties.Get(UDISKS_DEVICE_NAME, "DeviceFile")) + if int(udisk_properties.Get(UDISKS_DEVICE_NAME, "DriveAtaSmartTimeCollected")) > 0: + # Only get the temperature if SMART data is collected to avoid spinning up disk + smart_blob = udisk_properties.Get(UDISKS_DEVICE_NAME, "DriveAtaSmartBlob", byte_arrays=True) + smart_blob_str = str(smart_blob) + process = subprocess.Popen(['skdump', '--temperature', '--load=-'], shell = False, stdout = subprocess.PIPE, stdin = subprocess.PIPE) + result, stderrdata = process.communicate(smart_blob_str) + process.wait() + if len(result) > 0: + try: + kelvin = int(result) + kelvin /= 1000; + temp_c = kelvin - 273.15 + sensor.value = temp_c + except ValueError as ve: + logger.warning("Invalid temperature for device %s, %s.", + sensor_name, + result, + exc_info = ve) + sensor.value = 0 + else: + sensor.value = 0 + + self.sensors[sensor.name] = sensor + return self.sensors.values() + + def is_valid(self): + if self.udisks == None: + self.system_bus = dbus.SystemBus() + udisks_object = self.system_bus.get_object(UDISKS_BUS_NAME, '/org/freedesktop/UDisks') + # Easier way found to ensure that we can communicate with udisks + properties = dbus.Interface(udisks_object, 'org.freedesktop.DBus.Properties') + properties.Get(UDISKS_BUS_NAME, 'DaemonVersion') + self.udisks = dbus.Interface(udisks_object, UDISKS_BUS_NAME) + + return self.udisks is not None + + def stop(self): + pass + +class UDisks2Source(): + + def __init__(self): + self.name = "UDisks2" + + self.udisks = None + self.system_bus = None + self.udisks_data = None + self.sensors = {} + self.lock = Lock() + + def get_sensors(self): + def is_a_drive(device_path): + return device_path.find('/org/freedesktop/UDisks2/drives/') != -1 + def is_a_ata_drive_supporting_SMART(drive): + if 'org.freedesktop.UDisks2.Drive.Ata' in drive: + return drive['org.freedesktop.UDisks2.Drive.Ata']['SmartSupported'] + else: + return False + def find_valid_sensor_name(drive): + model = drive['org.freedesktop.UDisks2.Drive']['Model'] + serial = drive['org.freedesktop.UDisks2.Drive']['Serial'] + if not model in self.sensors: + return model + else: + return "(%s) (%s)" % (model, serial) + def kelvin_to_celsius(kelvin): + return kelvin - 273.15 + def drive_temperature(drive): + if drive['org.freedesktop.UDisks2.Drive.Ata']['SmartUpdated'] > 0: + return kelvin_to_celsius(drive['org.freedesktop.UDisks2.Drive.Ata']['SmartTemperature']) + else: + return 0 + + logger.debug("Refreshing disk drives temperatures") + self.sensors = {} + self.udisks_data = self.udisks.GetManagedObjects() + for device in self.udisks_data: + if not is_a_drive(device): + continue + if not is_a_ata_drive_supporting_SMART(self.udisks_data[device]): + logger.debug('SMART is disabled or unsupported by drive %s.', device) + continue + sensor_name = find_valid_sensor_name(self.udisks_data[device]) + logger.debug('Found sensor %s for drive %s.', sensor_name, device) + sensor = Sensor(TEMPERATURE, sensor_name, 0.0) + try: + sensor.value = drive_temperature(self.udisks_data[device]) + logger.debug('Temperature of drive %s is %f.', sensor_name,sensor.value) + except ValueError as ve: + logger.warning("Invalid temperature for device %s.", sensor_name, exc_info = ve) + sensor.value = 0 + self.sensors[sensor.name] = sensor + + return self.sensors.values() + + def is_valid(self): + if self.udisks == None: + self.system_bus = dbus.SystemBus() + udisks_object = self.system_bus.get_object(UDISKS2_BUS_NAME, '/org/freedesktop/UDisks2') + self.udisks = dbus.Interface(udisks_object, 'org.freedesktop.DBus.ObjectManager') + dbus_peer = dbus.Interface(udisks_object, 'org.freedesktop.DBus.Peer') + dbus_peer.Ping() + + return self.udisks is not None + + def stop(self): + pass + +class LibsensorsSource(): + def __init__(self): + self.name = "Libsensors" + self.started = False + + def get_sensors(self): + sensor_objects = [] + sensor_names = [] + for chip in sensors.iter_detected_chips(): + logger.debug("Found chip %s, adapter %s", chip, chip.adapter_name) + for feature in chip: + sensor_name = feature.label + + # Prevent name conflicts across chips + if not sensor_name in sensor_names: + sensor_names.append(sensor_name) + else: + o = sensor_name + idx = 1 + while sensor_name in sensor_names: + idx += 1 + sensor_name = "%s-%d" % (o, idx) + sensor_names.append(sensor_name) + + logger.debug("' %s: %.2f", sensor_name, feature.get_value()) + sensor = Sensor(feature.type, sensor_name, float(feature.get_value())) + sensor_objects.append(sensor) + + for subfeature in feature: + name = subfeature.name + if name.endswith("_crit"): + sensor.critical = float(subfeature.get_value()) + elif name.endswith("_input"): + sensor.value = float(subfeature.get_value()) + return sensor_objects + + def is_valid(self): + if not self.started: + sensors.init() + self.started = True + return self.started + + def stop(self): + if self.started: + sensors.cleanup() + +class NvidiaSource(): + def __init__(self): + self.name = "NVidia" + + def get_sensors(self): + status, value = self.getstatusoutput("nvidia-settings -q GPUCoreTemp -t") + return [Sensor(TEMPERATURE, "GPUCoreTemp", int(value.split('\n')[0]))] + + def getstatusoutput(self, cmd): + pipe = os.popen('{ ' + cmd + '; } 2>/dev/null', 'r') + text = pipe.read() + sts = pipe.close() + if sts is None: sts = 0 + if text[-1:] == '\n': text = text[:-1] + return sts, text + + def is_valid(self): + if not os.path.exists("/dev/nvidiactl"): + return False + if not os.access("/dev/nvidiactl", os.R_OK): + logger.warning("/dev/nvidiactl exists, but it is not readable by the current user, skipping sensor source.") + return False + return True + + def stop(self): + pass + +class SensorMenuItem(g15theme.MenuItem): + + def __init__(self, item_id, sensor, sensor_label): + g15theme.MenuItem.__init__(self, item_id) + self.sensor = sensor + self.sensor_label = sensor_label + + def get_theme_properties(self): + properties = g15theme.MenuItem.get_theme_properties(self) + properties["item_name"] = self.sensor_label + properties["item_alt"] = self._format_value(self.sensor.value) + properties["item_alt2"] = self._format_value(self.sensor.critical) if self.sensor.critical is not None else "" + max_val = self.sensor.critical if self.sensor.critical is not None else self.sensor.get_default_crit() + properties["temp_percent"] = ( self.sensor.value / max_val ) * 100.0 + return properties + + def _format_value(self, val): + return "%.2f" % val if val < 1000 else "%4d" % int(val) + +class G15Sensors(g15plugin.G15RefreshingPlugin): + + def __init__(self, gconf_key, gconf_client, screen): + g15plugin.G15RefreshingPlugin.__init__(self, gconf_client, gconf_key, screen, [ "system", "applications-system" ], id, name, 5.0) + self.schedule_on_gobject = True + self._sensors_changed_handle = None + self._menu = None + + def activate(self): + gobject.idle_add(self._do_activate) + + def _do_activate(self): + self._sensors_changed_handle = self.gconf_client.notify_add(self.gconf_key + "/sensors", self._sensors_changed) + self.sensor_sources = get_sensor_sources() + self.sensor_dict = {} + g15plugin.G15RefreshingPlugin.activate(self) + + def populate_page(self): + self._menu = g15theme.Menu("menu") + g15plugin.G15RefreshingPlugin.populate_page(self) + + enabled_sensors = [] + for c in self.sensor_sources: + for s in c.get_sensors(): + sense_key = "%s/sensors/%s" % (self.gconf_key, gconf.escape_key(s.name, len(s.name))) + if g15gconf.get_bool_or_default(self.gconf_client, "%s/enabled" % (sense_key), True): + enabled_sensors.append(s) + + + # If there are no sensors enabled, display the 'none' variant + # which shows a message + if len(enabled_sensors) == 0: + self.page.theme.set_variant("none") + else: + self.page.theme.set_variant(None) + def menu_selected(): + self.page.theme.set_variant(VARIANT_NAMES[self._menu.selected.sensor.sense_type]) + + self._menu.on_selected = menu_selected + self.page.add_child(self._menu) + self.page.theme.svg_processor = self._process_svg + self.page.add_child(g15theme.MenuScrollbar("viewScrollbar", self._menu)) + i = 0 + for s in enabled_sensors: + if s.sense_type in TYPE_NAMES: + sense_key = "%s/sensors/%s" % (self.gconf_key, gconf.escape_key(s.name, len(s.name))) + sense_label = g15gconf.get_string_or_default(self.gconf_client, "%s/label" % (sense_key), s.name) + menu_item = SensorMenuItem("menuitem-%d" % i, s, sense_label) + self.sensor_dict[s.name] = menu_item + self._menu.add_child(menu_item) + + # If this is the first child, change the theme variant + if self._menu.get_child_count() == 1: + self.page.theme.set_variant(VARIANT_NAMES[menu_item.sensor.sense_type]) + + i += 1 + + + def deactivate(self): + for c in self.sensor_sources: + c.stop() + g15plugin.G15RefreshingPlugin.deactivate(self) + if self._sensors_changed_handle is not None: + self.gconf_client.notify_remove(self._sensors_changed_handle) + + def refresh(self): + self._get_stats() + + def get_next_tick(self): + return g15gconf.get_float_or_default(self.gconf_client, "%s/interval" % self.gconf_key, 5.0) + + ''' Private + ''' + + def _sensors_changed(self, client, connection_id, entry, args): + self.page.remove_all_children() + self.populate_page() + self.refresh() + + def _process_svg(self, document, properties, attributes): + root = document.getroot() + if self._menu.selected is not None: + needle = self.page.theme.get_element("needle", root = root) + needle_center = self.page.theme.get_element("needle_center", root = root) + val = float(self._menu.selected.sensor.value) + + """ + The title contains the bounds for the gauge, in the format + lower_val,upper_val,middle_val,lower_deg,upper_deg + """ + gauge_data = needle_center.get("title").split(",") + lower_val = float(gauge_data[0]) + upper_val = float(gauge_data[1]) + middle_val = float(gauge_data[2]) + lower_deg = float(gauge_data[3]) + upper_deg = float(gauge_data[4]) + + # Clamp the value + val = min(upper_val, max(lower_val, val)) + + # Ratio of gauge bounds to rotate by + ratio = val / ( upper_val - lower_val ) + + """ + Work out total number of degrees in the bounds + """ + total_deg = upper_deg + ( 360 - lower_deg ) + + + # Work out total number of degress to rotate + rot_degrees = total_deg * ratio + + # + degr = lower_deg + degr += rot_degrees + + """ + This is a bit weak. It doesn't take transformations into account, + so care is needed in the SVG. + """ + center_bounds = g15svg.get_bounds(needle_center) + needle.set("transform", "rotate(%f,%f,%f)" % (degr, center_bounds[0], center_bounds[1]) ) + + def _get_stats(self): + for c in self.sensor_sources: + for s in c.get_sensors(): + if s.name in self.sensor_dict: + self.sensor_dict[s.name].sensor = s + if s.critical is not None: + logger.debug("Sensor %s on %s is %f (critical %f)", + s.name, + c.name, + s.value, + s.critical) + else: + logger.debug("Sensor %s on %s is %f", s.name, c.name, s.value) + + def get_theme_properties(self): + properties = g15plugin.G15RefreshingPlugin.get_theme_properties(self) + if self._menu.selected is not None: + properties["sensor"] = self._menu.selected.sensor.name + if self._menu.selected.sensor.sense_type == FAN: + properties["rpm"] = "%4d" % float(self._menu.selected.sensor.value) + elif self._menu.selected.sensor.sense_type == VOLTAGE: + properties["voltage"] = "%.2f" % float(self._menu.selected.sensor.value) + else: + properties["temp_c"] = "%.2f C" % float(self._menu.selected.sensor.value) + return properties diff --git a/src/plugins/sense/sense.ui b/src/plugins/sense/sense.ui new file mode 100644 index 0000000..d97abe6 --- /dev/null +++ b/src/plugins/sense/sense.ui @@ -0,0 +1,217 @@ + + + + + + 9999 + 1 + 10 + + + + + + + + + + + + + + + 320 + 400 + False + 5 + Sensors Preferences + False + True + center-on-parent + dialog + + + True + False + 2 + + + True + False + end + + + gtk-close + True + True + True + True + + + False + False + 0 + + + + + False + True + end + 0 + + + + + True + False + + + True + True + automatic + automatic + + + True + True + SensorModel + False + 0 + + + Enabled + + + + 1 + + + + + + + fixed + 48 + Type + + + + 3 + + + + + + + Label + + + True + + + 2 + + + + + + + + + True + True + 0 + + + + + True + False + 0 + none + + + True + False + 12 + + + True + False + + + True + False + 0 + Refresh interval + + + True + True + 0 + + + + + True + True + + False + False + True + True + IntervalAdjustment + + + False + True + 1 + + + + + True + False + seconds + + + False + True + 8 + 2 + + + + + + + + + True + False + <b>Options</b> + True + + + + + False + True + 1 + + + + + True + True + 1 + + + + + + button1 + + + diff --git a/src/plugins/stopwatch/Makefile.am b/src/plugins/stopwatch/Makefile.am new file mode 100644 index 0000000..8efafd5 --- /dev/null +++ b/src/plugins/stopwatch/Makefile.am @@ -0,0 +1,11 @@ +SUBDIRS = default + +plugindir = $(datadir)/gnome15/plugins/stopwatch +plugin_DATA = \ + stopwatch.ui \ + stopwatch.py \ + timer.py \ + preferences.py + +EXTRA_DIST = \ + $(plugin_DATA) \ No newline at end of file diff --git a/src/plugins/stopwatch/default/Makefile.am b/src/plugins/stopwatch/default/Makefile.am new file mode 100644 index 0000000..64066a9 --- /dev/null +++ b/src/plugins/stopwatch/default/Makefile.am @@ -0,0 +1,36 @@ +themedir = $(datadir)/gnome15/plugins/stopwatch/default +theme_DATA = \ + g19.svg \ + g19-one_timer.svg \ + g19-two_timers.svg \ + up.gif \ + playpause.gif \ + reset.gif \ + mx5500.svg \ + mx5500-one_timer.svg \ + mx5500-two_timers.svg \ + default.svg \ + default-one_timer.svg \ + default-two_timers.svg + +EXTRA_DIST = \ + $(theme_DATA) + +all-local: + for M_LOCALE in @ENABLED_LOCALES@; do \ + mkdir -p i18n/$$M_LOCALE/LC_MESSAGES ; \ + if [ `ls i18n/*.po 2>/dev/null|wc -l` -gt 0 ]; then \ + for M_PO in i18n/*.po; do \ + BN=`basename $$M_PO .po`; \ + LL=`basename $$BN .$$M_LOCALE`.mo; \ + echo "$$M_PO -> $$LL"; \ + msgfmt $$M_PO --output-file i18n/$$M_LOCALE/LC_MESSAGES/$$LL; \ + done; \ + fi; \ + done + +install-exec-hook: + for M_LOCALE in @ENABLED_LOCALES@; do \ + mkdir -p $(DESTDIR)$(datadir)/gnome15/plugins/stopwatch/default/i18n; \ + cp -pR i18n/$$M_LOCALE $(DESTDIR)$(datadir)/gnome15/plugins/stopwatch/default/i18n; \ + done \ No newline at end of file diff --git a/src/plugins/stopwatch/default/default-one_timer.svg b/src/plugins/stopwatch/default/default-one_timer.svg new file mode 100644 index 0000000..b1273d5 --- /dev/null +++ b/src/plugins/stopwatch/default/default-one_timer.svg @@ -0,0 +1,142 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + ${timer_label} + ${timer} + L3 + L4 + + + + + + + diff --git a/src/plugins/stopwatch/default/default-two_timers.svg b/src/plugins/stopwatch/default/default-two_timers.svg new file mode 100644 index 0000000..cccbb0e --- /dev/null +++ b/src/plugins/stopwatch/default/default-two_timers.svg @@ -0,0 +1,235 @@ + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + ${timer1_label} + ${timer1} + L3 + L4 + ${timer2} + L2 + A/B + ${timer2_label} + + + + + + + diff --git a/src/plugins/stopwatch/default/default.svg b/src/plugins/stopwatch/default/default.svg new file mode 100644 index 0000000..67e324d --- /dev/null +++ b/src/plugins/stopwatch/default/default.svg @@ -0,0 +1,77 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + _(No timer enabled) + + diff --git a/src/plugins/stopwatch/default/g19-one_timer.svg b/src/plugins/stopwatch/default/g19-one_timer.svg new file mode 100644 index 0000000..99e8d0c --- /dev/null +++ b/src/plugins/stopwatch/default/g19-one_timer.svg @@ -0,0 +1,265 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + ${timer} + ${timer_label} + + + + + + + + + + + + + + + + + + + + + diff --git a/src/plugins/stopwatch/default/g19-two_timers.svg b/src/plugins/stopwatch/default/g19-two_timers.svg new file mode 100644 index 0000000..2824164 --- /dev/null +++ b/src/plugins/stopwatch/default/g19-two_timers.svg @@ -0,0 +1,464 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + ${timer1} + ${timer1_label} + ${timer2} + ${timer2_label} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/plugins/stopwatch/default/g19.svg b/src/plugins/stopwatch/default/g19.svg new file mode 100644 index 0000000..75538ae --- /dev/null +++ b/src/plugins/stopwatch/default/g19.svg @@ -0,0 +1,77 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + _(No timer enabled.) + + diff --git a/src/plugins/stopwatch/default/i18n/default.en_GB.po b/src/plugins/stopwatch/default/i18n/default.en_GB.po new file mode 100644 index 0000000..3f6003f --- /dev/null +++ b/src/plugins/stopwatch/default/i18n/default.en_GB.po @@ -0,0 +1,22 @@ +# English translations for gnome15-plugins package. +# Copyright (C) 2012 THE gnome15-plugins'S COPYRIGHT HOLDER +# This file is distributed under the same license as the gnome15-plugins package. +# Automatically generated, 2012. +# +msgid "" +msgstr "" +"Project-Id-Version: gnome15-plugins 0.8.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: 2012-01-08 11:23+0000\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: en_GB\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=ASCII\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: i18n/default.h:1 +msgid "No timer enabled" +msgstr "No timer enabled" diff --git a/src/plugins/stopwatch/default/i18n/default.h b/src/plugins/stopwatch/default/i18n/default.h new file mode 100644 index 0000000..93cf7c2 --- /dev/null +++ b/src/plugins/stopwatch/default/i18n/default.h @@ -0,0 +1 @@ +char *s = N_("No timer enabled"); diff --git a/src/plugins/stopwatch/default/i18n/default.pot b/src/plugins/stopwatch/default/i18n/default.pot new file mode 100644 index 0000000..18fbd05 --- /dev/null +++ b/src/plugins/stopwatch/default/i18n/default.pot @@ -0,0 +1,22 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: i18n/default.h:1 +msgid "No timer enabled" +msgstr "" diff --git a/src/plugins/stopwatch/default/i18n/g19.en_GB.po b/src/plugins/stopwatch/default/i18n/g19.en_GB.po new file mode 100644 index 0000000..9751407 --- /dev/null +++ b/src/plugins/stopwatch/default/i18n/g19.en_GB.po @@ -0,0 +1,22 @@ +# English translations for gnome15-plugins package. +# Copyright (C) 2012 THE gnome15-plugins'S COPYRIGHT HOLDER +# This file is distributed under the same license as the gnome15-plugins package. +# Automatically generated, 2012. +# +msgid "" +msgstr "" +"Project-Id-Version: gnome15-plugins 0.8.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: 2012-01-08 11:23+0000\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: en_GB\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=ASCII\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: i18n/g19.h:1 +msgid "No timer enabled." +msgstr "No timer enabled." diff --git a/src/plugins/stopwatch/default/i18n/g19.h b/src/plugins/stopwatch/default/i18n/g19.h new file mode 100644 index 0000000..aaa6c03 --- /dev/null +++ b/src/plugins/stopwatch/default/i18n/g19.h @@ -0,0 +1 @@ +char *s = N_("No timer enabled."); diff --git a/src/plugins/stopwatch/default/i18n/g19.pot b/src/plugins/stopwatch/default/i18n/g19.pot new file mode 100644 index 0000000..4756340 --- /dev/null +++ b/src/plugins/stopwatch/default/i18n/g19.pot @@ -0,0 +1,22 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: i18n/g19.h:1 +msgid "No timer enabled." +msgstr "" diff --git a/src/plugins/stopwatch/default/i18n/mx5500.en_GB.po b/src/plugins/stopwatch/default/i18n/mx5500.en_GB.po new file mode 100644 index 0000000..9dfef42 --- /dev/null +++ b/src/plugins/stopwatch/default/i18n/mx5500.en_GB.po @@ -0,0 +1,22 @@ +# English translations for gnome15-plugins package. +# Copyright (C) 2012 THE gnome15-plugins'S COPYRIGHT HOLDER +# This file is distributed under the same license as the gnome15-plugins package. +# Automatically generated, 2012. +# +msgid "" +msgstr "" +"Project-Id-Version: gnome15-plugins 0.8.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: 2012-01-08 11:23+0000\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: en_GB\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=ASCII\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: i18n/mx5500.h:1 +msgid "No timer enabled" +msgstr "No timer enabled" diff --git a/src/plugins/stopwatch/default/i18n/mx5500.h b/src/plugins/stopwatch/default/i18n/mx5500.h new file mode 100644 index 0000000..93cf7c2 --- /dev/null +++ b/src/plugins/stopwatch/default/i18n/mx5500.h @@ -0,0 +1 @@ +char *s = N_("No timer enabled"); diff --git a/src/plugins/stopwatch/default/i18n/mx5500.pot b/src/plugins/stopwatch/default/i18n/mx5500.pot new file mode 100644 index 0000000..1623df7 --- /dev/null +++ b/src/plugins/stopwatch/default/i18n/mx5500.pot @@ -0,0 +1,22 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: i18n/mx5500.h:1 +msgid "No timer enabled" +msgstr "" diff --git a/src/plugins/stopwatch/default/mx5500-one_timer.svg b/src/plugins/stopwatch/default/mx5500-one_timer.svg new file mode 100644 index 0000000..2bef5c0 --- /dev/null +++ b/src/plugins/stopwatch/default/mx5500-one_timer.svg @@ -0,0 +1,127 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + ${timer_label} + ${timer} + Go/Stop + + + Reset + + diff --git a/src/plugins/stopwatch/default/mx5500-two_timers.svg b/src/plugins/stopwatch/default/mx5500-two_timers.svg new file mode 100644 index 0000000..fde9953 --- /dev/null +++ b/src/plugins/stopwatch/default/mx5500-two_timers.svg @@ -0,0 +1,177 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + ${timer1_label} + ${timer1} + Go/Stop + Reset + ${timer2_label} + ${timer2} + + + + + A/B + + diff --git a/src/plugins/stopwatch/default/mx5500.svg b/src/plugins/stopwatch/default/mx5500.svg new file mode 100644 index 0000000..4d0e190 --- /dev/null +++ b/src/plugins/stopwatch/default/mx5500.svg @@ -0,0 +1,77 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + _(No timer enabled) + + diff --git a/src/plugins/stopwatch/default/playpause.gif b/src/plugins/stopwatch/default/playpause.gif new file mode 100644 index 0000000..0ae3444 Binary files /dev/null and b/src/plugins/stopwatch/default/playpause.gif differ diff --git a/src/plugins/stopwatch/default/reset.gif b/src/plugins/stopwatch/default/reset.gif new file mode 100644 index 0000000..800acad Binary files /dev/null and b/src/plugins/stopwatch/default/reset.gif differ diff --git a/src/plugins/stopwatch/default/up.gif b/src/plugins/stopwatch/default/up.gif new file mode 100644 index 0000000..362b680 Binary files /dev/null and b/src/plugins/stopwatch/default/up.gif differ diff --git a/src/plugins/stopwatch/i18n/stopwatch.en_GB.po b/src/plugins/stopwatch/i18n/stopwatch.en_GB.po new file mode 100644 index 0000000..61da091 --- /dev/null +++ b/src/plugins/stopwatch/i18n/stopwatch.en_GB.po @@ -0,0 +1,62 @@ +# English translations for gnome15-plugins package. +# Copyright (C) 2012 THE gnome15-plugins'S COPYRIGHT HOLDER +# This file is distributed under the same license as the gnome15-plugins package. +# Automatically generated, 2012. +# +msgid "" +msgstr "" +"Project-Id-Version: gnome15-plugins 0.8.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: 2012-01-08 11:23+0000\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: en_GB\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=ASCII\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: i18n/stopwatch.glade.h:1 +msgid "Mode" +msgstr "Mode" + +#: i18n/stopwatch.glade.h:2 +msgid "Time (hours:minutes:seconds)" +msgstr "Time (hours:minutes:seconds)" + +#: i18n/stopwatch.glade.h:3 +msgid "Countdown" +msgstr "Countdown" + +#: i18n/stopwatch.glade.h:4 +msgid "Enabled" +msgstr "Enabled" + +#: i18n/stopwatch.glade.h:5 +msgid "Keep stopwatch visible while actve" +msgstr "Keep stopwatch visible while actve" + +#: i18n/stopwatch.glade.h:6 +msgid "Label" +msgstr "Label" + +#: i18n/stopwatch.glade.h:7 +msgid "Loop" +msgstr "Loop" + +#: i18n/stopwatch.glade.h:8 +msgid "Stopwatch" +msgstr "Stopwatch" + +#: i18n/stopwatch.glade.h:9 +msgid "Stopwatch Preferences" +msgstr "Stopwatch Preferences" + +#: i18n/stopwatch.glade.h:10 +msgid "Timer 1" +msgstr "Timer 1" + +#: i18n/stopwatch.glade.h:11 +msgid "Timer 2" +msgstr "Timer 2" diff --git a/src/plugins/stopwatch/i18n/stopwatch.glade.h b/src/plugins/stopwatch/i18n/stopwatch.glade.h new file mode 100644 index 0000000..b85fcde --- /dev/null +++ b/src/plugins/stopwatch/i18n/stopwatch.glade.h @@ -0,0 +1,11 @@ +char *s = N_("Mode"); +char *s = N_("Time (hours:minutes:seconds)"); +char *s = N_("Countdown"); +char *s = N_("Enabled"); +char *s = N_("Keep stopwatch visible while actve"); +char *s = N_("Label"); +char *s = N_("Loop"); +char *s = N_("Stopwatch"); +char *s = N_("Stopwatch Preferences"); +char *s = N_("Timer 1"); +char *s = N_("Timer 2"); diff --git a/src/plugins/stopwatch/i18n/stopwatch.pot b/src/plugins/stopwatch/i18n/stopwatch.pot new file mode 100644 index 0000000..59e4212 --- /dev/null +++ b/src/plugins/stopwatch/i18n/stopwatch.pot @@ -0,0 +1,62 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: i18n/stopwatch.glade.h:1 +msgid "Mode" +msgstr "" + +#: i18n/stopwatch.glade.h:2 +msgid "Time (hours:minutes:seconds)" +msgstr "" + +#: i18n/stopwatch.glade.h:3 +msgid "Countdown" +msgstr "" + +#: i18n/stopwatch.glade.h:4 +msgid "Enabled" +msgstr "" + +#: i18n/stopwatch.glade.h:5 +msgid "Keep stopwatch visible while actve" +msgstr "" + +#: i18n/stopwatch.glade.h:6 +msgid "Label" +msgstr "" + +#: i18n/stopwatch.glade.h:7 +msgid "Loop" +msgstr "" + +#: i18n/stopwatch.glade.h:8 +msgid "Stopwatch" +msgstr "" + +#: i18n/stopwatch.glade.h:9 +msgid "Stopwatch Preferences" +msgstr "" + +#: i18n/stopwatch.glade.h:10 +msgid "Timer 1" +msgstr "" + +#: i18n/stopwatch.glade.h:11 +msgid "Timer 2" +msgstr "" diff --git a/src/plugins/stopwatch/preferences.py b/src/plugins/stopwatch/preferences.py new file mode 100644 index 0000000..fcaa638 --- /dev/null +++ b/src/plugins/stopwatch/preferences.py @@ -0,0 +1,97 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2011 Nuno Araujo +# +# 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 . + +import gnome15.util.g15uigconf as g15uigconf +import gtk +import os + +class G15StopwatchPreferences(): + + def __init__(self, parent, driver, gconf_client, gconf_key): + widget_tree = gtk.Builder() + widget_tree.add_from_file(os.path.join(os.path.dirname(__file__), "stopwatch.ui")) + + self.dialog = widget_tree.get_object("StopwatchDialog") + self.dialog.set_transient_for(parent) + + # Timer 1 settings + g15uigconf.configure_checkbox_from_gconf(gconf_client, gconf_key + "/timer1_enabled", "cb_timer1_enabled", False, widget_tree, True) + + timer1_label = widget_tree.get_object("e_timer1_label") + timer1_label.set_text(gconf_client.get_string(gconf_key + "/timer1_label") or "") + timer1_label.connect("changed", self._label_changed, gconf_key + "/timer1_label", gconf_client) + + g15uigconf.configure_checkbox_from_gconf(gconf_client, gconf_key + "/timer1_mode_stopwatch", "rb_timer1_stopwatch_mode", True, widget_tree, True) + rb_timer1_stopwatch = widget_tree.get_object("rb_timer1_stopwatch_mode") + rb_timer1_stopwatch.connect("clicked", self._timer_timer_mode, widget_tree, "1", False) + g15uigconf.configure_checkbox_from_gconf(gconf_client, gconf_key + "/timer1_mode_countdown", "rb_timer1_countdown_mode", False, widget_tree, True) + rb_timer1_countdown = widget_tree.get_object("rb_timer1_countdown_mode") + rb_timer1_countdown.connect("clicked", self._timer_timer_mode, widget_tree, "1", True) + + g15uigconf.configure_spinner_from_gconf(gconf_client, gconf_key + "/timer1_hours", "sb_timer1_hours", 0, widget_tree, False) + g15uigconf.configure_spinner_from_gconf(gconf_client, gconf_key + "/timer1_minutes", "sb_timer1_minutes", 5, widget_tree, False) + g15uigconf.configure_spinner_from_gconf(gconf_client, gconf_key + "/timer1_seconds", "sb_timer1_seconds", 0, widget_tree, False) + + g15uigconf.configure_checkbox_from_gconf(gconf_client, gconf_key + "/timer1_loop", "cb_timer1_loop", False, widget_tree, True) + + # Timer 2 settings + g15uigconf.configure_checkbox_from_gconf(gconf_client, gconf_key + "/timer2_enabled", "cb_timer2_enabled", False, widget_tree, True) + + timer2_label = widget_tree.get_object("e_timer2_label") + timer2_label.set_text(gconf_client.get_string(gconf_key + "/timer2_label") or "") + timer2_label.connect("changed", self._label_changed, gconf_key + "/timer2_label", gconf_client) + + g15uigconf.configure_checkbox_from_gconf(gconf_client, gconf_key + "/timer2_mode_stopwatch", "rb_timer2_stopwatch_mode", True, widget_tree, True) + rb_timer2_stopwatch = widget_tree.get_object("rb_timer2_stopwatch_mode") + rb_timer2_stopwatch.connect("clicked", self._timer_timer_mode, widget_tree, "2", False) + + g15uigconf.configure_checkbox_from_gconf(gconf_client, gconf_key + "/timer2_mode_countdown", "rb_timer2_countdown_mode", False, widget_tree, True) + rb_timer2_countdown = widget_tree.get_object("rb_timer2_countdown_mode") + rb_timer2_countdown.connect("clicked", self._timer_timer_mode, widget_tree, "2", True) + + g15uigconf.configure_spinner_from_gconf(gconf_client, gconf_key + "/timer2_hours", "sb_timer2_hours", 0, widget_tree, False) + g15uigconf.configure_spinner_from_gconf(gconf_client, gconf_key + "/timer2_minutes", "sb_timer2_minutes", 5, widget_tree, False) + g15uigconf.configure_spinner_from_gconf(gconf_client, gconf_key + "/timer2_seconds", "sb_timer2_seconds", 0, widget_tree, False) + + g15uigconf.configure_checkbox_from_gconf(gconf_client, gconf_key + "/timer2_loop", "cb_timer2_loop", False, widget_tree, True) + g15uigconf.configure_checkbox_from_gconf(gconf_client, gconf_key + "/keep_page_visible", "cb_keep_page_visible", True, widget_tree, True) + + # Refresh UI state + self._timer_timer_mode(None, widget_tree, "1", rb_timer1_countdown.get_active()) + self._timer_timer_mode(None, widget_tree, "2", rb_timer2_countdown.get_active()) + + + def _label_changed(self, widget, gconf_key, gconf_client): + gconf_client.set_string(gconf_key, widget.get_text()) + + ''' + Set the UI sensivity according to the selected mode + ''' + def _timer_timer_mode(self, widget, widget_tree, timer_no, mode = False): + sb_timer_hours = widget_tree.get_object("sb_timer" + timer_no + "_hours") + sb_timer_hours.set_sensitive(mode) + sb_timer_minutes = widget_tree.get_object("sb_timer" + timer_no + "_minutes") + sb_timer_minutes.set_sensitive(mode) + sb_timer_seconds = widget_tree.get_object("sb_timer" + timer_no + "_seconds") + sb_timer_seconds.set_sensitive(mode) + cb_timer_loop = widget_tree.get_object("cb_timer" + timer_no + "_loop") + cb_timer_loop.set_sensitive(mode) + + def run(self): + self.dialog.run() + self.dialog.hide() + +# vim:set ts=4 sw=4 et: diff --git a/src/plugins/stopwatch/stopwatch.py b/src/plugins/stopwatch/stopwatch.py new file mode 100644 index 0000000..853d411 --- /dev/null +++ b/src/plugins/stopwatch/stopwatch.py @@ -0,0 +1,296 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2011 Nuno Araujo +# Brett Smith +# +# 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 . + +import gnome15.g15locale as g15locale +_ = g15locale.get_translation("stopwatch", modfile = __file__).ugettext + +import gnome15.g15screen as g15screen +import gnome15.g15theme as g15theme +import gnome15.util.g15gconf as g15gconf +import gnome15.util.g15pythonlang as g15pythonlang +import gnome15.g15driver as g15driver +import gnome15.g15globals as g15globals +import gnome15.g15plugin as g15plugin +import gnome15.g15text as g15text +import datetime +import pango +import timer + +import preferences as g15preferences + +# Plugin details - All of these must be provided +id="stopwatch" +name=_("Stopwatch") +description=_("Stopwatch/Countdown timer plugin for gnome15.\ +Two timers are available. User can select the a mode (stopwatch/countdown) for each of them.") +author="Nuno Araujo " +copyright=_("Copyright (C)2011 Nuno Araujo") +site="http://www.russo79.com/gnome15" +has_preferences=True +unsupported_models = [ g15driver.MODEL_G110, g15driver.MODEL_G11, g15driver.MODEL_G930, g15driver.MODEL_G35 ] +actions={ + g15driver.PREVIOUS_SELECTION : _("Toggle selected timer"), + g15driver.NEXT_SELECTION : _("Reset selected timer"), + g15driver.VIEW : _("Switch between timers") + } +actions_g19={ + g15driver.PREVIOUS_SELECTION : _("Toggle timer 1"), + g15driver.NEXT_SELECTION : _("Reset timer 1"), + g15driver.NEXT_PAGE : _("Toggle timer 2"), + g15driver.PREVIOUS_PAGE : _("Reset timer 2") + } + + +# +# A stopwatch / timer plugin for gnome15 +# + +def create(gconf_key, gconf_client, screen): + return G15Stopwatch(gconf_key, gconf_client, screen) + +def show_preferences(parent, driver, gconf_client, gconf_key): + preferences = g15preferences.G15StopwatchPreferences(parent, driver, gconf_client, gconf_key) + preferences.run() + + +class G15Stopwatch(g15plugin.G15RefreshingPlugin): + + def __init__(self, gconf_key, gconf_client, screen): + g15plugin.G15RefreshingPlugin.__init__(self, gconf_client, gconf_key, \ + screen, [ "cairo-clock", "clock", "gnome-panel-clock", "xfce4-clock", "rclock", "player-time" ], id, name) + self._active_timer = None + self._message = None + self._priority = g15screen.PRI_NORMAL + + def activate(self): + self._timer = None + self._text = g15text.new_text(self.screen) + self._notify_timer = None + self._timer1 = timer.G15Timer() + self._timer1.on_finish = self._on_finish + self._timer2 = timer.G15Timer() + self._timer2.on_finish = self._on_finish + self._load_configuration() + + g15plugin.G15RefreshingPlugin.activate(self) + + self.screen.key_handler.action_listeners.append(self) + self.watch(None, self._config_changed) + + def deactivate(self): + if self._timer1.is_running(): + self._timer1.toggle() + if self._timer2.is_running(): + self._timer2.toggle() + self.screen.key_handler.action_listeners.remove(self) + g15plugin.G15RefreshingPlugin.deactivate(self) + + def destroy(self): + pass + + def create_page(self): + page = g15plugin.G15RefreshingPlugin.create_page(self) + if self.screen.driver.get_bpp() != 16: + """ + Don't show on the panel for G15, there just isn't enough room + Long term, this will be configurable per plugin + """ + page.panel_painter = None + return page + + def create_theme(self): + variant = None + if self._timer1.get_enabled() and self._timer2.get_enabled(): + variant = "two_timers" + elif self._timer1.get_enabled() or self._timer2.get_enabled(): + variant = "one_timer" + return g15theme.G15Theme(self, variant) + + def action_performed(self, binding): + if self.page and self.page.is_visible(): + # G19 we make use of more keys + if self.screen.driver.get_model_name() == g15driver.MODEL_G19: + if self._timer1.get_enabled(): + if binding.action == g15driver.PREVIOUS_SELECTION: + self._timer1.toggle() + self._check_page_priority() + self._refresh() + elif binding.action == g15driver.NEXT_SELECTION: + self._timer1.reset() + + if self._timer2.get_enabled(): + if binding.action == g15driver.PREVIOUS_PAGE: + self._timer2.toggle() + self._check_page_priority() + self._refresh() + elif binding.action == g15driver.NEXT_PAGE: + self._timer2.reset() + else: + # For everything else we allow switching between timers + if binding.action == g15driver.VIEW: + if self._active_timer == self._timer1: + self._active_timer = self._timer2 + else: + self._active_timer = self._timer1 + self._refresh() + + if self._active_timer: + if binding.action == g15driver.PREVIOUS_SELECTION: + self._active_timer.toggle() + self._check_page_priority() + self._refresh() + elif binding.action == g15driver.NEXT_SELECTION: + self._active_timer.reset() + self._check_page_priority() + self._refresh() + + def get_next_tick(self): + return g15pythonlang.total_seconds( datetime.timedelta( seconds = 1 )) + + def get_theme_properties(self): + properties = { } + if self._timer1.get_enabled() and self._timer2.get_enabled(): + properties["timer1_label"] = self._timer1.label + properties["timer1"] = self._format_time_delta(self._timer1.value()) + if self._active_timer == self._timer1: + properties["timer1_active"] = True + properties["timer2_active"] = False + else: + properties["timer1_active"] = False + properties["timer2_active"] = True + properties["timer2_label"] = self._timer2.label + properties["timer2"] = self._format_time_delta(self._timer2.value()) + elif self._timer1.get_enabled(): + properties["timer_label"] = self._timer1.label + properties["timer"] = self._format_time_delta(self._timer1.value()) + elif self._timer2.get_enabled(): + properties["timer_label"] = self._timer2.label + properties["timer"] = self._format_time_delta(self._timer2.value()) + + return properties + + def _paint_panel(self, canvas, allocated_size, horizontal): + if not self.page or self.screen.is_visible(self.page): + return + if not (self._timer1.get_enabled() or self._timer2.get_enabled()): + return + if not (self._timer1.is_running() or self._timer2.is_running()): + return + properties = self.get_theme_properties() + # Don't display the date or seconds on mono displays, not enough room as it is + if self.screen.driver.get_bpp() == 1: + if self._timer1.get_enabled() and self._timer2.get_enabled(): + text = "%s %s" % ( properties["timer1"], properties["timer2"] ) + else: + text = properties["timer"] + font_size = 8 + factor = 2 + font_name = g15globals.fixed_size_font_name + gap = 1 + else: + factor = 1 if horizontal else 1.2 + font_name = "Sans" + if self._timer1.get_enabled() and self._timer2.get_enabled(): + text = "%s\n%s" % (properties["timer1"], properties["timer2"]) + font_size = allocated_size / 3 + else: + text = properties["timer"] + font_size = allocated_size / 2 + gap = 8 + + self._text.set_canvas(canvas) + self._text.set_attributes(text, align = pango.ALIGN_CENTER, font_desc = font_name, font_absolute_size = font_size * pango.SCALE / factor) + x, y, width, height = self._text.measure() + if horizontal: + if self.screen.driver.get_bpp() == 1: + y = 0 + else: + y = (allocated_size / 2) - height / 2 + else: + x = (allocated_size / 2) - width / 2 + y = 0 + self._text.draw(x, y) + if horizontal: + return width + gap + else: + return height + 4 + + ''' + *********************************************************** + * Functions specific to plugin * + *********************************************************** + ''' + + def _config_changed(self, client, connection_id, entry, args): + self._load_configuration() + self.reload_theme() + self.screen.set_priority(self.page, g15screen.PRI_HIGH, revert_after = 3.0) + + def _get_or_default(self, key, default_value): + v = self.gconf_client.get(key) + return v.get_int() if v != None else default_value + + def _load_timer(self, timer_object, number): + timer_object.set_enabled(self.gconf_client.get_bool(self.gconf_key + "/timer%d_enabled" % number) or False) + timer_object.label = self.gconf_client.get_string(self.gconf_key + "/timer%d_label" % number) or "" + if self.gconf_client.get_bool(self.gconf_key + "/timer%d_mode_countdown" % number): + timer_object.mode = timer.G15Timer.TIMER_MODE_COUNTDOWN + timer_object.initial_value = datetime.timedelta(hours = self._get_or_default(self.gconf_key + "/timer%d_hours" % number, 0), \ + minutes = self._get_or_default(self.gconf_key + "/timer%d_minutes" % number, 5), \ + seconds = self._get_or_default(self.gconf_key + "/timer%d_seconds" % number, 0)) + timer_object.loop = self.gconf_client.get_bool(self.gconf_key + "/timer%d_loop" % number ) + else: + timer_object.mode = timer.G15Timer.TIMER_MODE_STOPWATCH + timer_object.initial_value = datetime.timedelta(0, 0, 0) + + def _load_configuration(self): + self._load_timer(self._timer1, 1) + self._load_timer(self._timer2, 2) + + # Set active timer + if self._active_timer == None and self._timer1.get_enabled() and self._timer2.get_enabled(): + self._active_timer = self._timer1 + elif self._timer1.get_enabled() and self._timer2.get_enabled(): + #Keeps the current timer active + pass + elif self._timer1.get_enabled(): + self._active_timer = self._timer1 + elif self._timer2.get_enabled(): + self._active_timer = self._timer2 + + self._check_page_priority() + + def _check_page_priority(self): + self._priority = g15screen.PRI_EXCLUSIVE if self._is_any_timer_active() and g15gconf.get_bool_or_default(self.gconf_client, "%s/keep_page_visible" % self.gconf_key, True) \ + else g15screen.PRI_NORMAL + if self.page: + self.page.set_priority(self._priority) + + def _format_time_delta(self, td): + hours = td.seconds // 3600 + minutes = (td.seconds % 3600) // 60 + seconds = td.seconds % 60 + return '%s:%02d:%02d' % (hours, minutes, seconds) + + def _is_any_timer_active(self): + return ( self._timer1 is not None and self._timer1.is_running() ) or \ + ( self._timer2 is not None and self._timer2.is_running() ) + + def _on_finish(self): + self._check_page_priority() + +# vim:set ts=4 sw=4 et: diff --git a/src/plugins/stopwatch/stopwatch.ui b/src/plugins/stopwatch/stopwatch.ui new file mode 100644 index 0000000..b46797c --- /dev/null +++ b/src/plugins/stopwatch/stopwatch.ui @@ -0,0 +1,625 @@ + + + + + + + + + + + + + + + + 999 + 1 + 10 + + + 59 + 1 + 10 + + + 60 + 1 + 10 + + + 999 + 1 + 10 + + + 59 + 1 + 10 + + + 59 + 1 + 10 + + + 320 + False + 5 + Stopwatch Preferences + False + True + center-on-parent + dialog + + + True + False + 2 + + + True + False + end + + + gtk-close + True + True + True + True + + + False + False + 0 + + + + + False + True + end + 0 + + + + + True + False + + + True + True + + + True + False + 4 + + + Enabled + True + True + False + True + + + True + True + 4 + 0 + + + + + True + False + + + True + False + Label + + + True + True + 0 + + + + + True + True + + True + False + False + True + True + + + True + True + 1 + + + + + True + True + 1 + + + + + True + False + 0 + none + + + True + False + 4 + 4 + 12 + + + True + False + 4 + + + Stopwatch + True + True + False + True + True + + + True + True + 0 + + + + + Countdown + True + True + False + True + rb_timer1_stopwatch_mode + + + True + True + 1 + + + + + + + + + True + False + <b>Mode</b> + True + + + + + True + True + 2 + + + + + True + False + 0 + none + + + True + False + 4 + 4 + 12 + + + True + False + 4 + + + True + True + + True + False + False + True + True + adj_timer1_hours + True + + + True + True + 0 + + + + + True + True + + True + False + False + True + True + adj_timer1_minutes + True + + + True + True + 1 + + + + + True + True + + True + False + False + True + True + adj_timer1_seconds + True + + + True + True + 2 + + + + + + + + + True + False + <b>Time (hours:minutes:seconds)</b> + True + + + + + True + True + 3 + + + + + Loop + True + True + False + True + + + True + True + 4 + 4 + + + + + + + True + False + Timer 1 + + + False + + + + + True + False + 4 + + + Enabled + True + True + False + True + + + True + True + 0 + + + + + True + False + + + True + False + Label + + + True + True + 0 + + + + + True + True + + True + False + False + True + True + + + True + True + 1 + + + + + True + True + 1 + + + + + True + False + 0 + none + + + True + False + 12 + + + True + False + 4 + + + Stopwatch + True + True + False + True + True + + + True + True + 0 + + + + + Countdown + True + True + False + True + rb_timer2_stopwatch_mode + + + True + True + 1 + + + + + + + + + True + False + <b>Mode</b> + True + + + + + True + True + 2 + + + + + True + False + 0 + none + + + True + False + 4 + 4 + 12 + + + True + False + 4 + + + True + True + + True + False + False + True + True + adj_timer2_hours + True + + + True + True + 0 + + + + + True + True + + True + False + False + True + True + adj_timer2_minutes + True + + + True + True + 1 + + + + + True + True + + True + False + False + True + True + adj_timer2_seconds + True + + + True + True + 2 + + + + + + + + + True + False + <b>Time (hours:minutes:seconds)</b> + True + + + + + True + True + 3 + + + + + Loop + True + True + False + True + + + True + True + 4 + + + + + 1 + + + + + True + False + Timer 2 + + + 1 + False + + + + + True + True + 4 + 0 + + + + + Keep stopwatch visible while actve + True + True + False + True + + + True + True + 1 + + + + + True + True + 1 + + + + + + button1 + + + diff --git a/src/plugins/stopwatch/timer.py b/src/plugins/stopwatch/timer.py new file mode 100644 index 0000000..d7c329d --- /dev/null +++ b/src/plugins/stopwatch/timer.py @@ -0,0 +1,91 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2011 Nuno Araujo +# +# 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 . + +import datetime +import gnome15.g15notify as g15notify + +class G15Timer(): + TIMER_MODE_STOPWATCH = 0 + TIMER_MODE_COUNTDOWN = 1 + + def __init__(self): + self.__enabled = False + self.__running = False + self.label = "" + self.on_finish = None + self.mode = G15Timer.TIMER_MODE_STOPWATCH + self.initial_value = datetime.timedelta() + self.loop = False + self.reset() + + def set_enabled(self, value): + if value != self.__enabled: + self.__enabled = value + self.pause() + self.reset() + + def get_enabled(self): + return self.__enabled + + def value(self): + rv = self.__value() + if self.mode == G15Timer.TIMER_MODE_COUNTDOWN: + # Handle timeout + if rv >= self.initial_value: + # Stop timer if not in loop mode + if not self.loop: + self.pause() + self.reset() + rv = self.__value() + if self.on_finish: + self.on_finish() + self.notify() + rv = self.initial_value - rv + return rv + + def __value(self): + if not self.__running: + rv = self._last_value + else: + rv = datetime.datetime.now() - self._last_resume + self._last_value + return rv + + def toggle(self): + if self.__running: + self.pause() + else: + self.resume() + + def is_running(self): + return self.__running + + def pause(self): + self._last_value = self.__value() + self._last_resume = datetime.datetime.now() + self.__running = False + + def resume(self): + self._last_resume = datetime.datetime.now() + self.__running = True + + def reset(self): + self._last_value = datetime.timedelta() + self._last_resume = datetime.datetime.now() + + def notify(self): + g15notify.notify("Stopwatch", "Timer '" + self.label + "' is over.", timeout = 0) + +# vim:set ts=4 sw=4 et: diff --git a/src/plugins/sysmon/Makefile.am b/src/plugins/sysmon/Makefile.am new file mode 100644 index 0000000..73d794b --- /dev/null +++ b/src/plugins/sysmon/Makefile.am @@ -0,0 +1,7 @@ +SUBDIRS = default dials graphs +plugindir = $(datadir)/gnome15/plugins/sysmon +plugin_DATA = sysmon.py \ + sysmon.ui + +EXTRA_DIST = \ + $(plugin_DATA) \ No newline at end of file diff --git a/src/plugins/sysmon/default/Makefile.am b/src/plugins/sysmon/default/Makefile.am new file mode 100644 index 0000000..f1991e6 --- /dev/null +++ b/src/plugins/sysmon/default/Makefile.am @@ -0,0 +1,7 @@ +themedir = $(datadir)/gnome15/plugins/sysmon/default +theme_DATA = default.svg \ + g19.svg \ + mx5500.svg + +EXTRA_DIST = \ + $(theme_DATA) \ No newline at end of file diff --git a/src/plugins/sysmon/default/default.svg b/src/plugins/sysmon/default/default.svg new file mode 100644 index 0000000..eaa0a89 --- /dev/null +++ b/src/plugins/sysmon/default/default.svg @@ -0,0 +1,224 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + ${cpu_no}: + ${net_no} + Mem: + ${cpu_pc}% + ${net_recv_mbps}/${net_send_mbps} + ${mem_used_gb}/${mem_total_gb} + + + ${info} + L3 ${next_cpu_no} + L4 ${next_net_no} + + diff --git a/src/plugins/sysmon/default/g19.svg b/src/plugins/sysmon/default/g19.svg new file mode 100644 index 0000000..14decd8 --- /dev/null +++ b/src/plugins/sysmon/default/g19.svg @@ -0,0 +1,482 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + ${cpu_pc}% + ${net_recv_mbps} Mbps + ${net_send_mbps} Mbps + ${mem_noncached_gb} GB of + ${mem_total_gb} GB + + + + ${next_cpu_no} + + + + + ${next_net_no} + + ${cpu_no} + ${net_no} + + diff --git a/src/plugins/sysmon/default/i18n/default.en_GB.po b/src/plugins/sysmon/default/i18n/default.en_GB.po new file mode 100644 index 0000000..e69de29 diff --git a/src/plugins/sysmon/default/i18n/default.pot b/src/plugins/sysmon/default/i18n/default.pot new file mode 100644 index 0000000..e69de29 diff --git a/src/plugins/sysmon/default/i18n/g19.en_GB.po b/src/plugins/sysmon/default/i18n/g19.en_GB.po new file mode 100644 index 0000000..e69de29 diff --git a/src/plugins/sysmon/default/i18n/g19.pot b/src/plugins/sysmon/default/i18n/g19.pot new file mode 100644 index 0000000..e69de29 diff --git a/src/plugins/sysmon/default/i18n/mx5500.en_GB.po b/src/plugins/sysmon/default/i18n/mx5500.en_GB.po new file mode 100644 index 0000000..e69de29 diff --git a/src/plugins/sysmon/default/i18n/mx5500.pot b/src/plugins/sysmon/default/i18n/mx5500.pot new file mode 100644 index 0000000..e69de29 diff --git a/src/plugins/sysmon/default/mx5500.svg b/src/plugins/sysmon/default/mx5500.svg new file mode 100644 index 0000000..5c56f58 --- /dev/null +++ b/src/plugins/sysmon/default/mx5500.svg @@ -0,0 +1,200 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + ${cpu_no}: + ${net_no} + Mem: + ${cpu_pc}% + ${net_recv_mbps}/${net_send_mbps} + ${mem_used_gb}/${mem_total_gb} + + + ${info} + + diff --git a/src/plugins/sysmon/dials/Makefile.am b/src/plugins/sysmon/dials/Makefile.am new file mode 100644 index 0000000..b8c698c --- /dev/null +++ b/src/plugins/sysmon/dials/Makefile.am @@ -0,0 +1,9 @@ +themedir = $(datadir)/gnome15/plugins/sysmon/dials +theme_DATA = sysmon_dials_g19.py \ + g19.svg \ + g19-large-needle.svg \ + g19-small-needle.svg \ + g19-tiny-needle.svg + +EXTRA_DIST = \ + $(theme_DATA) diff --git a/src/plugins/sysmon/dials/g19-large-needle.svg b/src/plugins/sysmon/dials/g19-large-needle.svg new file mode 100644 index 0000000..5f46130 --- /dev/null +++ b/src/plugins/sysmon/dials/g19-large-needle.svg @@ -0,0 +1,139 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + diff --git a/src/plugins/sysmon/dials/g19-small-needle.svg b/src/plugins/sysmon/dials/g19-small-needle.svg new file mode 100644 index 0000000..5213be2 --- /dev/null +++ b/src/plugins/sysmon/dials/g19-small-needle.svg @@ -0,0 +1,130 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + diff --git a/src/plugins/sysmon/dials/g19-tiny-needle.svg b/src/plugins/sysmon/dials/g19-tiny-needle.svg new file mode 100644 index 0000000..0d76118 --- /dev/null +++ b/src/plugins/sysmon/dials/g19-tiny-needle.svg @@ -0,0 +1,140 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + diff --git a/src/plugins/sysmon/dials/g19.svg b/src/plugins/sysmon/dials/g19.svg new file mode 100644 index 0000000..5fbf536 --- /dev/null +++ b/src/plugins/sysmon/dials/g19.svg @@ -0,0 +1,1024 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + CPU + + + + + + + + + Network + + + + + + + + + + Memory + ${mem_used_gb} GiB / ${mem_total_gb} GiB + ${cpu_pc}% + Receive: ${net_recv_mbps}mbps Send: ${net_send_mbps}mbps + ${mem_cached_gb} GiB cached + + diff --git a/src/plugins/sysmon/dials/sysmon_dials_g19.py b/src/plugins/sysmon/dials/sysmon_dials_g19.py new file mode 100644 index 0000000..aafbab0 --- /dev/null +++ b/src/plugins/sysmon/dials/sysmon_dials_g19.py @@ -0,0 +1,52 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2010 Brett Smith +# +# 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 . + +import gnome15.util.g15convert as g15convert +import gnome15.util.g15cairo as g15cairo +import os +import rsvg +import cairo + +needles = { + "cpu_pc" : (170, 90, rsvg.Handle(os.path.join(os.path.dirname(__file__), "g19-large-needle.svg"))), + "net_send_pc" : (82, 198, rsvg.Handle(os.path.join(os.path.dirname(__file__), "g19-tiny-needle.svg"))), + "net_recv_pc" : (82, 198, rsvg.Handle(os.path.join(os.path.dirname(__file__), "g19-small-needle.svg"))), + "mem_used_pc" : (254, 198, rsvg.Handle(os.path.join(os.path.dirname(__file__), "g19-small-needle.svg"))), + "mem_cached_pc" : (254, 198, rsvg.Handle(os.path.join(os.path.dirname(__file__), "g19-tiny-needle.svg"))) + } + +def paint_foreground(theme, canvas, properties, attributes, args): + for key in needles.keys(): + needle = needles[key] + svg = needle[2] + surface = create_needle_surface(svg, ( ( 180.0 / 100.0 ) * float(properties[key]) ) ) + canvas.save() + svg_size = svg.get_dimension_data()[2:4] + canvas.translate (needle[0] - svg_size[0], needle[1] - svg_size[1]) + canvas.set_source_surface(surface) + canvas.paint() + canvas.restore() + +def create_needle_surface(svg, degrees): + svg_size = svg.get_dimension_data()[2:4] + surface = cairo.SVGSurface(None, svg_size[0] * 2,svg_size[1] *2) + context = cairo.Context(surface) + context.translate(svg_size[0], svg_size[1]) + g15cairo.rotate(context, -180) + g15cairo.rotate(context, degrees) + svg.render_cairo(context) + context.translate(-svg_size[0], -svg_size[1]) + return surface diff --git a/src/plugins/sysmon/graphs/Makefile.am b/src/plugins/sysmon/graphs/Makefile.am new file mode 100644 index 0000000..97d8d4e --- /dev/null +++ b/src/plugins/sysmon/graphs/Makefile.am @@ -0,0 +1,8 @@ +themedir = $(datadir)/gnome15/plugins/sysmon/graphs +theme_DATA = g19.svg \ + default.svg \ + graphs.theme \ + sysmon_graphs_default.py + +EXTRA_DIST = \ + $(theme_DATA) \ No newline at end of file diff --git a/src/plugins/sysmon/graphs/default.svg b/src/plugins/sysmon/graphs/default.svg new file mode 100644 index 0000000..7b93da2 --- /dev/null +++ b/src/plugins/sysmon/graphs/default.svg @@ -0,0 +1,215 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + ${info} + L3 ${next_cpu_no} + L4 ${next_net_no} + + + + ${cpu_no} + ${net_no} + Mem + ${cpu_pc}% + ${mem_used_gb}/${mem_total_gb} + ${net_recv_mbps}/${net_send_mbps} + + diff --git a/src/plugins/sysmon/graphs/g19.svg b/src/plugins/sysmon/graphs/g19.svg new file mode 100644 index 0000000..c05c929 --- /dev/null +++ b/src/plugins/sysmon/graphs/g19.svg @@ -0,0 +1,358 @@ + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + ${cpu_no} + + ${cpu_pc}% + + + ${net_recv_mbps} Mbps + ${net_send_mbps} Mbps + ${net_no} + + + _(Mem) + ${mem_noncached_gb} GB of + ${mem_total_gb} GB + + + + + ${next_cpu_no} + + + + + ${next_net_no} + + + diff --git a/src/plugins/sysmon/graphs/graphs.theme b/src/plugins/sysmon/graphs/graphs.theme new file mode 100644 index 0000000..2574fb0 --- /dev/null +++ b/src/plugins/sysmon/graphs/graphs.theme @@ -0,0 +1,7 @@ +[theme] +name=Graphs +description=Displays system status as a number graphs. +#unsupported_models=g110,g11,mx5500,g930,g35 +# CairoPlot doesn't handle tiny graphs well at all. Disabled other models +# until this is fixed +supported_models=g19 diff --git a/src/plugins/sysmon/graphs/i18n/g19.en_GB.po b/src/plugins/sysmon/graphs/i18n/g19.en_GB.po new file mode 100644 index 0000000..511a6c4 --- /dev/null +++ b/src/plugins/sysmon/graphs/i18n/g19.en_GB.po @@ -0,0 +1,22 @@ +# English translations for gnome15-plugins package. +# Copyright (C) 2012 THE gnome15-plugins'S COPYRIGHT HOLDER +# This file is distributed under the same license as the gnome15-plugins package. +# Automatically generated, 2012. +# +msgid "" +msgstr "" +"Project-Id-Version: gnome15-plugins 0.8.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: 2012-01-08 11:23+0000\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: en_GB\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=ASCII\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: i18n/g19.h:1 +msgid "Mem" +msgstr "Mem" diff --git a/src/plugins/sysmon/graphs/i18n/g19.h b/src/plugins/sysmon/graphs/i18n/g19.h new file mode 100644 index 0000000..21fdc9c --- /dev/null +++ b/src/plugins/sysmon/graphs/i18n/g19.h @@ -0,0 +1 @@ +char *s = N_("Mem"); diff --git a/src/plugins/sysmon/graphs/i18n/g19.pot b/src/plugins/sysmon/graphs/i18n/g19.pot new file mode 100644 index 0000000..291191d --- /dev/null +++ b/src/plugins/sysmon/graphs/i18n/g19.pot @@ -0,0 +1,22 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: i18n/g19.h:1 +msgid "Mem" +msgstr "" diff --git a/src/plugins/sysmon/graphs/sysmon_graphs_default.py b/src/plugins/sysmon/graphs/sysmon_graphs_default.py new file mode 100644 index 0000000..612e35a --- /dev/null +++ b/src/plugins/sysmon/graphs/sysmon_graphs_default.py @@ -0,0 +1,156 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2012 Brett Smith +# +# 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 . + +import gnome15.g15theme as g15theme +import gnome15.g15driver as g15driver +import gnome15.util.g15convert as g15convert +import cairoplot +import cairo + +def create(theme): + page = theme.component + plugin = theme.plugin + page.add_child(G15CPUGraph("cpu", plugin)) + page.add_child(G15NetGraph("net", plugin)) + page.add_child(G15MemGraph("mem", plugin)) + +def destroy(theme): + page = theme.component +# page.remove_child(page.get_child_by_id("cpu")) +# page.remove_child(page.get_child_by_id("net")) +# page.remove_child(page.get_child_by_id("mem")) + +class G15Graph(g15theme.Component): + + def __init__(self, component_id, plugin): + g15theme.Component.__init__(self, component_id) + self.plugin = plugin + + def get_colors(self): + if self.plugin.screen.driver.get_bpp() == 1: + return (0.0,0.0,0.0,1.0), (0.0,0.0,0.0,1.0) + elif self.plugin.screen.driver.get_control_for_hint(g15driver.HINT_HIGHLIGHT): + highlight_color = self.plugin.screen.driver.get_color_as_ratios(g15driver.HINT_HIGHLIGHT, (255, 0, 0 )) + return (highlight_color[0],highlight_color[1],highlight_color[2], 1.0), \ + (highlight_color[0],highlight_color[1],highlight_color[2], 0.50) + + def create_plot(self, graph_surface): + raise Exception("Not implemented") + + def paint(self, canvas): + g15theme.Component.paint(self, canvas) + if self.view_bounds: + graph_surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, + int(self.view_bounds[2]), + int(self.view_bounds[3])) + plot = self.create_plot(graph_surface) + + if self.plugin.screen.driver.get_bpp() == 1: + plot.line_color = (1.0,1.0,1.0) + plot.line_width = 1.0 + plot.display_labels = False + else: + plot.line_width = 2.0 + plot.bounding_box = False + plot.line_color = self.plugin.screen.driver.get_color_as_ratios(g15driver.HINT_FOREGROUND, (255, 255, 255)) + plot.label_color = self.plugin.screen.driver.get_color_as_ratios(g15driver.HINT_FOREGROUND, (255, 255, 255)) + plot.shadow = True + plot.render() + plot.commit() + + canvas.save() + canvas.translate(self.view_bounds[0], self.view_bounds[1]) + canvas.set_source_surface(graph_surface, 0.0, 0.0) + canvas.paint() + canvas.restore() + +class G15CPUGraph(G15Graph): + + def __init__(self, component_id, plugin): + G15Graph.__init__(self, component_id, plugin) + + def create_plot(self, graph_surface): + series_colors, fill_colors = self.get_colors() + return cairoplot.AreaPlot(graph_surface, self.plugin.selected_cpu.history, + self.view_bounds[2], + self.view_bounds[3], + background = None, + grid = False, + x_labels = [], + y_labels = ["%-6d" % 0, "%-6d" % 50, "%-6d" % 100], + y_bounds = (0, 100), + series_colors = [ series_colors ], + fill_colors = [ fill_colors ]) + + +class G15NetGraph(G15Graph): + + def __init__(self, component_id, plugin): + G15Graph.__init__(self, component_id, plugin) + + def create_plot(self, graph_surface): + y_labels = [] + max_y = max(max(self.plugin.selected_net.max_send, self.plugin.selected_net.max_recv), 102400) + for x in range(0, int(max_y), int(max_y / 4)): + y_labels.append("%-3.2f" % ( float(x) / 102400.0 ) ) + series_color, fill_color = self.get_colors() + if self.plugin.screen.driver.get_bpp() == 1: + alt_series_color = (1.0,1.0,1.0,1.0) + alt_fill_color = (1.0,1.0,1.0,1.0) + else: + alt_series_color = g15convert.get_alt_color(series_color) + alt_fill_color = g15convert.get_alt_color(fill_color) + return cairoplot.AreaPlot( graph_surface, [ self.plugin.selected_net.send_history, self.plugin.selected_net.recv_history ], + self.view_bounds[2], + self.view_bounds[3], + background = None, + grid = False, + x_labels = [], + y_labels = y_labels, + y_bounds = (0, max_y ), + series_colors = [ series_color, alt_series_color ], + fill_colors = [ fill_color, alt_fill_color ] ) + +class G15MemGraph(G15Graph): + """ + Memory graph + """ + def __init__(self, component_id, plugin): + G15Graph.__init__(self, component_id, plugin) + + def create_plot(self, graph_surface): + y_labels = [] + max_y = self.plugin.max_total_mem + for x in range(0, int(max_y), int(max_y / 4)): + y_labels.append("%-4d" % int( float(x) / 1024.0 / 1024.0 ) ) + series_color, fill_color = self.get_colors() + + if self.plugin.screen.driver.get_bpp() == 1: + alt_series_color = (1.0,1.0,1.0,1.0) + alt_fill_color = (1.0,1.0,1.0,1.0) + else: + alt_series_color = g15convert.get_alt_color(series_color) + alt_fill_color = g15convert.get_alt_color(fill_color) + return cairoplot.AreaPlot( graph_surface, [ self.plugin.used_history, self.plugin.cached_history ], + self.view_bounds[2], + self.view_bounds[3], + background = None, + grid = False, + x_labels = [], + y_labels = y_labels, + y_bounds = (0, max_y ), + series_colors = [ series_color, alt_series_color ], + fill_colors = [ fill_color, alt_fill_color ] ) \ No newline at end of file diff --git a/src/plugins/sysmon/i18n/sysmon.en_GB.po b/src/plugins/sysmon/i18n/sysmon.en_GB.po new file mode 100644 index 0000000..7569452 --- /dev/null +++ b/src/plugins/sysmon/i18n/sysmon.en_GB.po @@ -0,0 +1,26 @@ +# English translations for gnome15-plugins package. +# Copyright (C) 2012 THE gnome15-plugins'S COPYRIGHT HOLDER +# This file is distributed under the same license as the gnome15-plugins package. +# Automatically generated, 2012. +# +msgid "" +msgstr "" +"Project-Id-Version: gnome15-plugins 0.8.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: 2012-01-08 11:23+0000\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: en_GB\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=ASCII\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: i18n/sysmon.glade.h:1 +msgid "Sensors Preferences" +msgstr "Sensors Preferences" + +#: i18n/sysmon.glade.h:2 +msgid "Show CPU usage on panel" +msgstr "Show CPU usage on panel" diff --git a/src/plugins/sysmon/i18n/sysmon.glade.h b/src/plugins/sysmon/i18n/sysmon.glade.h new file mode 100644 index 0000000..6714dbb --- /dev/null +++ b/src/plugins/sysmon/i18n/sysmon.glade.h @@ -0,0 +1,2 @@ +char *s = N_("Sensors Preferences"); +char *s = N_("Show CPU usage on panel"); diff --git a/src/plugins/sysmon/i18n/sysmon.pot b/src/plugins/sysmon/i18n/sysmon.pot new file mode 100644 index 0000000..778f76a --- /dev/null +++ b/src/plugins/sysmon/i18n/sysmon.pot @@ -0,0 +1,26 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: i18n/sysmon.glade.h:1 +msgid "Sensors Preferences" +msgstr "" + +#: i18n/sysmon.glade.h:2 +msgid "Show CPU usage on panel" +msgstr "" diff --git a/src/plugins/sysmon/sysmon.py b/src/plugins/sysmon/sysmon.py new file mode 100644 index 0000000..68b1a79 --- /dev/null +++ b/src/plugins/sysmon/sysmon.py @@ -0,0 +1,472 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2010 Brett Smith +# +# 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 . + +import gnome15.g15locale as g15locale +_ = g15locale.get_translation("sysmon", modfile = __file__).ugettext + +import gnome15.util.g15convert as g15convert +import gnome15.util.g15uigconf as g15uigconf +import gnome15.util.g15gconf as g15gconf +import gnome15.util.g15cairo as g15cairo +import gnome15.util.g15icontools as g15icontools +import gnome15.g15driver as g15driver +import gnome15.g15plugin as g15plugin +import time +import logging +logger=logging.getLogger(__name__) +try: + import gtop +except Exception as e: + logger.debug("Could not import gtop. Falling back to g15top", exc_info = e) + # API compatible work around for Ubuntu 12.10 + import gnome15.g15top as gtop +import gtk +import os +import sys +import socket + +id = "sysmon" +name = _("System Monitor") +description = _("Display CPU, Memory, and Network statistics. Either a summary of each system's stats is displayed, or \ +you may cycle through the CPU and Network interfaces.") +author = "Brett Smith " +copyright = _("Copyright (C)2010 Brett Smith") +site = "http://www.gnome15.org" +default_enabled = True +has_preferences = True +actions={ + g15driver.PREVIOUS_SELECTION : _("Toggle Monitored CPU"), + g15driver.NEXT_SELECTION : _("Toggle Monitored Network\nInterface") } +unsupported_models = [ g15driver.MODEL_G110, g15driver.MODEL_G11, g15driver.MODEL_G930, g15driver.MODEL_G35 ] + +# Various constants +GRAPH_SIZE = 50 +CPU_ICONS = [ "utilities-system-monitor","gnome-cpu-frequency-applet", "computer" ] + +''' +This plugin displays system statistics +''' + +def create(gconf_key, gconf_client, screen): + return G15SysMon(gconf_key, gconf_client, screen) + +def show_preferences(parent, driver, gconf_client, gconf_key): + widget_tree = gtk.Builder() + widget_tree.add_from_file(os.path.join(os.path.dirname(__file__), "sysmon.ui")) + dialog = widget_tree.get_object("SysmonDialog") + dialog.set_transient_for(parent) + g15uigconf.configure_checkbox_from_gconf(gconf_client, gconf_key + "/show_cpu_on_panel", "ShowCPUUsageOnPanel", True, widget_tree) + dialog.run() + dialog.hide() + +class Net(): + + def __init__(self, net_no, name): + self.net_no = net_no + self.name = name + self.recv_bps = 0.0 + self.send_bps = 0.0 + self.last_net_list = None + self.max_send = 0.0001 + self.max_recv = 0.0001 + self.send_history = [0] * GRAPH_SIZE + self.recv_history = [0] * GRAPH_SIZE + self.last_net_list = None + self.last_time = 0 + + def new_data(self, this_net_list): + now = time.time() + + ''' + Net + ''' + + self.recv_bps = 0.0 + self.send_bps = 0.0 + + if self.last_net_list != None: + time_taken = now - self.last_time + if self.net_no == 0: + this_total = self._get_net_total(this_net_list) + last_total = self._get_net_total(self.last_net_list) + else: + this_total = self._get_net(this_net_list[self.name]) + last_total = self._get_net(self.last_net_list[self.name]) + + # How many bps + self.recv_bps = (this_total[0] - last_total[0]) / time_taken + self.send_bps = (this_total[1] - last_total[1]) / time_taken + + # Adjust the maximums if necessary + if self.recv_bps > self.max_recv: + self.max_recv = self.recv_bps + if self.send_bps > self.max_send: + self.max_send = self.send_bps + + # History + self.send_history.append(self.recv_bps) + while len(self.send_history) > GRAPH_SIZE: + del self.send_history[0] + self.recv_history.append(self.send_bps) + while len(self.recv_history) > GRAPH_SIZE: + del self.recv_history[0] + + self.last_net_list = this_net_list + self.last_time = now + + def _get_net(self, card): + totals = (card[0], card[1]) + return totals + + def _get_net_total(self, net_list): + totals = (0, 0) + for l in net_list: + card = net_list[l] + totals = (totals[0] + card[0], totals[1]) + totals = (totals[0], totals[1] + card[1]) + return totals + +class CPU(): + + def __init__(self, number): + self.number = number + self.name = "cpu%d" % number if number >= 0 else "cpu" + self.history = [0] * GRAPH_SIZE + self.value = 0 + self.times = None + self.last_times = None + + def new_times(self, time_list): + + if self.last_times is not None: + working_list = list(time_list) + + ''' Work out the number of time units the CPU has spent on each task type since the last + time we checked + ''' + + for i in range(len(self.last_times)): + working_list[i] -= self.last_times[i] + + self.pc = self.get_pc(working_list) + else: + self.pc = 0 + + self.last_times = time_list + + # Update the history and trim it to the graph data size + self.history.append(self.pc) + while len(self.history) > GRAPH_SIZE: + del self.history[0] + + def get_pc(self, times): + sum_l = sum(times) + val = times[len(times)- 1] + if sum_l > 0: + return 100 - (val * 100.00 / sum_l) + return 0 + +class G15SysMon(g15plugin.G15RefreshingPlugin): + """ + Plugin implementation + """ + def __init__(self, gconf_key, gconf_client, screen): + g15plugin.G15RefreshingPlugin.__init__(self, gconf_client, gconf_key, screen, CPU_ICONS, id, name) + self.only_refresh_when_visible = False + + def activate(self): + self._net_icon = g15icontools.get_icon_path([ "network-transmit-receive", + "gnome-fs-network", + "network-server" ], + self.screen.height) + self._cpu_icon = g15icontools.get_icon_path( CPU_ICONS, + self.screen.height) + self._mem_icon = g15icontools.get_icon_path( [ "media-memory", + "media-flash" ], + self.screen.height) + self._thumb_icon = g15cairo.load_surface_from_file(self._cpu_icon) + + self.variant = 0 + self.graphs = {} + self.last_time_list = None + self.last_times_list = [] + self.last_time = 0 + + # CPU + self.selected_cpu = None + self.cpu_no = 0 + self.cpu_data = [] + selected_cpu_name = self.gconf_client.get_string(self.gconf_key + "/cpu") + cpus = gtop.cpu().cpus + for i in range(-1, len(cpus)): + cpu = CPU(i) + self.cpu_data.append(cpu) + if cpu.name == selected_cpu_name: + self.selected_cpu = cpu + if self.selected_cpu is None: + self.selected_cpu = self.cpu_data[0] + + # Net + self.selected_net = None + _, self.net_list = self._get_net_stats() + net_name = self.gconf_client.get_string(self.gconf_key + "/net") + self.net_data = [] + for idx, n in enumerate(self.net_list): + net = Net(idx, n) + self.net_data.append(net) + if net.name == net_name: + self.selected_net = net + + if self.selected_net is None and len(self.net_data) > 0: + self.selected_net = self.net_data[0] + + + # Memory + self.max_total_mem = 0 + self.total = 1.0 + self.cached = 0 + self.free = 0 + self.used = 0 + self.cached_history = [0] * GRAPH_SIZE + self.used_history = [0] * GRAPH_SIZE + + g15plugin.G15RefreshingPlugin.activate(self) + self._set_panel() + self.watch(["show_cpu_on_panel","theme"], self._config_changed) + self.screen.key_handler.action_listeners.append(self) + + # Start refreshing + self.do_refresh() + + def reload_theme(self): + g15plugin.G15RefreshingPlugin.reload_theme(self) + self._set_panel() + + def deactivate(self): + g15plugin.G15RefreshingPlugin.deactivate(self) + self.screen.key_handler.action_listeners.remove(self) + + def action_performed(self, binding): + if self.page and self.page.is_visible(): + if binding.action == g15driver.PREVIOUS_SELECTION: + idx = self.cpu_data.index(self.selected_cpu) + idx += 1 + if idx >= len(self.cpu_data): + idx = 0 + self.gconf_client.set_string(self.gconf_key + "/cpu", self.cpu_data[idx].name) + self.selected_cpu = self.cpu_data[idx] + self.do_refresh() + return True + elif binding.action == g15driver.NEXT_SELECTION: + if self.selected_net is not None: + idx = self.net_data.index(self.selected_net) + idx += 1 + if idx >= len(self.net_data): + idx = 0 + self.gconf_client.set_string(self.gconf_key + "/net", self.net_data[idx].name) + self.selected_net = self.net_data[idx] + self.do_refresh() + return True + + def refresh(self): + + # Memory + mem = self._get_mem_info() + now = time.time() + + ''' + CPU + ''' + for c in self.cpu_data: + c.new_times(self._get_time_list(c)) + + ''' + Net + ''' + + # Current net status + this_net_list, self.net_list = self._get_net_stats() + for n in self.net_data: + n.new_data(this_net_list) + + ''' + Memory + ''' + + self.total = float(mem.total) + self.max_total_mem = max(self.max_total_mem, self.total) + self.free = float(mem.free) + self.used = self.total - self.free + self.cached = float(mem.cached) + self.noncached = self.total - self.free - self.cached + self.used_history.append(self.used + self.cached) + + while len(self.used_history) > GRAPH_SIZE: + del self.used_history[0] + self.cached_history.append(self.cached) + while len(self.cached_history) > GRAPH_SIZE: + del self.cached_history[0] + + self.last_time = now + + ''' Private + ''' + def _config_changed(self, client, connection_id, entry, args): + self.reload_theme() + self._reschedule_refresh() + + def _set_panel(self, client = None, connection_id = None, entry = None, args = None): + self.page.panel_painter = self._paint_panel if g15gconf.get_bool_or_default(self.gconf_client, self.gconf_key + "/show_cpu_on_panel", True) else None + + def _refresh(self): + if self.page is not None: + if self.screen.is_visible(self.page): + self.refresh() + self.screen.redraw(self.page) + elif self.page.panel_painter is not None: + self.refresh() + self.screen.redraw(redraw_content = False) + self._schedule_refresh() + + def get_theme_properties(self): + + properties = {} + properties["cpu_pc"] = "%3d" % self.selected_cpu.pc + + properties["mem_total"] = "%f" % ( self.total / 1024 ) + properties["mem_free_k"] = "%f" % ( self.free / 1024 ) + properties["mem_used_k"] = "%f" % ( self.used / 1024 ) + properties["mem_cached_k"] = "%f" % ( self.cached / 1024 ) + properties["mem_noncached_k"] = "%f" % ( self.noncached / 1024 ) + + properties["mem_total_mb"] = "%.2f" % ( self.total / 1024 / 1024 ) + properties["mem_free_mb"] = "%.2f" % ( self.free / 1024 / 1024 ) + properties["mem_used_mb"] = "%.2f" % ( self.used / 1024 / 1024 ) + properties["mem_cached_mb" ] = "%3d" % ( self.cached / 1024 / 1024 ) + properties["mem_noncached_mb" ] = "%3d" % ( self.noncached / 1024 / 1024 ) + + properties["mem_total_gb"] = "%.1f" % ( self.total / 1024 / 1024 / 1024 ) + properties["mem_free_gb"] = "%.1f" % ( self.free / 1024 / 1024 / 1024 ) + properties["mem_used_gb"] = "%.1f" % ( self.used / 1024 / 1024 / 1024 ) + properties["mem_cached_gb" ] = "%.1f" % ( self.cached / 1024 / 1024 / 1024 ) + properties["mem_noncached_gb"] = "%.1f" % ( self.noncached / 1024 / 1024 / 1024 ) + + properties["mem_used_pc"] = int(self.used * 100.0 / self.total) + properties["mem_cached_pc"] = int(self.cached * 100.0 / self.total) + properties["mem_noncached_pc"] = int(self.noncached * 100.0 / self.total) + + if self.selected_net is not None: + properties["net_recv_pc"] = int(self.selected_net.recv_bps * 100.0 / self.selected_net.max_recv) + properties["net_send_pc"] = int(self.selected_net.send_bps * 100.0 / self.selected_net.max_send) + properties["net_recv_mbps"] = "%.2f" % (self.selected_net.recv_bps / 1024 / 1024) + properties["net_send_mbps"] = "%.2f" % (self.selected_net.send_bps / 1024 / 1024) + properties["net_no"] = self.selected_net.name.upper() + idx = self.net_data.index(self.selected_net) + properties["next_net_no"] = self.net_list[idx + 1].upper() if idx < ( len(self.net_list) - 1) else self.net_list[0].upper() + else: + for c in ["net_recv_pc","net_send_pc","net_recv_mbps","net_send_mbps"]: + properties[c] = "" + + # TODO we should ship some more appropriate default icons + properties["net_icon"] = self._net_icon + properties["cpu_icon"] = self._cpu_icon + properties["mem_icon"] = self._mem_icon + + try : + properties["info"] = socket.gethostname() + except Exception as e: + logger.debug("Could not get hostname. Falling back to 'System'", exc_info = e) + properties["info"] = "System" + + properties["cpu_no"] = self.selected_cpu.name.upper() + idx = self.cpu_data.index(self.selected_cpu) + properties["next_cpu_no"] = self.cpu_data[idx + 1].name.upper() if idx < ( len(self.cpu_data) - 1) else self.cpu_data[0].name.upper() + + + return properties + + def _paint_thumbnail(self, canvas, allocated_size, horizontal): + if self.page != None and self._thumb_icon != None and self.screen.driver.get_bpp() == 16: + return g15cairo.paint_thumbnail_image(allocated_size, self._thumb_icon, canvas) + + def _paint_panel(self, canvas, allocated_size, horizontal): + if self.page != None and self.screen.driver.get_bpp() == 16: + canvas.save() + + no_cpus = len(self.cpu_data) - 1 + if no_cpus < 2: + bar_width = 16 + elif no_cpus < 3: + bar_width = 8 + elif no_cpus < 5: + bar_width = 6 + elif no_cpus < 9: + bar_width = 4 + else: + bar_width = 2 + + total_width = ( bar_width + 1 ) * no_cpus + available_height = allocated_size - 4 + + r, g, b = self.screen.driver.get_color_as_ratios(g15driver.HINT_FOREGROUND, (0,0,0)) + + canvas.set_line_width(1.0) + canvas.set_source_rgba(r, g, b, 0.3) + canvas.rectangle(0, 0, total_width + 4, allocated_size ) + canvas.stroke() + canvas.set_source_rgb(*self.screen.driver.get_color_as_ratios(g15driver.HINT_HIGHLIGHT, (0,0,0))) + canvas.translate(2, 0) + for i in self.cpu_data: + if i.number >= 0: + bar_height = float(available_height) * ( float(i.pc) / 100.0 ) + canvas.rectangle(0, available_height - bar_height + 2, bar_width, bar_height ) + canvas.fill() + canvas.translate(bar_width + 1, 0) + + canvas.restore() + + return 4 + total_width + + def _get_net_stats(self): + ifs = { } + nets = gtop.netlist() + for net in nets: + netload = gtop.netload(net) + ifs[net] = [ netload.bytes_in, netload.bytes_out ] + nets.insert(0, "Net") + return ifs, nets + + + def _get_time_list(self, cpu): + ''' + Returns a 4 element list containing the amount of time the CPU has + spent performing the different types of work + + 0 user + 1 nice + 2 system + 3 idle + + Values are in USER_HZ or Jiffies + ''' + if cpu.number == -1: + cpu_times = gtop.cpu() + else: + cpu_times = gtop.cpu().cpus[cpu.number] + return [cpu_times.user, cpu_times.nice, cpu_times.sys, cpu_times.idle] + + def _get_mem_info(self): + return gtop.mem() diff --git a/src/plugins/sysmon/sysmon.ui b/src/plugins/sysmon/sysmon.ui new file mode 100644 index 0000000..78e0cb8 --- /dev/null +++ b/src/plugins/sysmon/sysmon.ui @@ -0,0 +1,66 @@ + + + + + + 320 + False + 5 + Sensors Preferences + False + True + center-on-parent + dialog + + + True + False + 2 + + + True + False + end + + + gtk-close + True + True + True + True + + + False + False + 0 + + + + + False + True + end + 0 + + + + + Show CPU usage on panel + True + True + False + True + + + True + True + 1 + + + + + + button1 + + + diff --git a/src/plugins/tails/LICENSE b/src/plugins/tails/LICENSE new file mode 100644 index 0000000..65c5ca8 --- /dev/null +++ b/src/plugins/tails/LICENSE @@ -0,0 +1,165 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + + This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below. + + 0. Additional Definitions. + + As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the GNU +General Public License. + + "The Library" refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + + The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + + Each version is given a distinguishing version number. If the +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License "or any later version" +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. diff --git a/src/plugins/tails/Makefile.am b/src/plugins/tails/Makefile.am new file mode 100644 index 0000000..85ac349 --- /dev/null +++ b/src/plugins/tails/Makefile.am @@ -0,0 +1,9 @@ +SUBDIRS = default tailer +plugindir = $(datadir)/gnome15/plugins/tails +plugin_DATA = tails.py \ + tails.ui \ + LICENSE \ + README + +EXTRA_DIST = \ + $(plugin_DATA) LICENSE README \ No newline at end of file diff --git a/src/plugins/tails/README b/src/plugins/tails/README new file mode 100644 index 0000000..c9c7bcb --- /dev/null +++ b/src/plugins/tails/README @@ -0,0 +1,53 @@ + +pytailer +A python implementation of GNU tail and head + +http://code.google.com/p/pytailer/ + +====== + +Python tail is a simple implementation of GNU tail and head. + +It provides 3 main functions that can be performed on any file-like object that +supports seek() and tell(). + +* tail - read lines from the end of a file +* head - read lines from the top of a file +* follow - read lines as a file grows + +It also comes with pytail, a command line version offering the same +functionality as GNU tail. This can be particularly useful on Windows systems +that have no tail equivalent. + + +:: + +import tailer +f = open('test.txt', 'w') +for i in range(11): +f.write('Line %d\n' % (i + 1)) +f.close() + +Tail +---- +:: + +# Get the last 3 lines of the file +tailer.tail(open('test.txt'), 3) +# ['Line 9', 'Line 10', 'Line 11'] + +Head +---- +:: + +# Get the first 3 lines of the file +tailer.head(open('test.txt'), 3) +# ['Line 1', 'Line 2', 'Line 3'] + +Follow +------ +:: + +# Follow the file as it grows +for line in tailer.follow(open('test.txt')): +print line \ No newline at end of file diff --git a/src/plugins/tails/default/Makefile.am b/src/plugins/tails/default/Makefile.am new file mode 100644 index 0000000..d4bdb66 --- /dev/null +++ b/src/plugins/tails/default/Makefile.am @@ -0,0 +1,8 @@ +themedir = $(datadir)/gnome15/plugins/tails/default +theme_DATA = default-menu-screen.svg \ + default-menu-entry.svg \ + g19-menu-screen.svg \ + g19-menu-entry.svg + +EXTRA_DIST = \ + $(theme_DATA) diff --git a/src/plugins/tails/default/default-menu-entry.svg b/src/plugins/tails/default/default-menu-entry.svg new file mode 100644 index 0000000..2b045bd --- /dev/null +++ b/src/plugins/tails/default/default-menu-entry.svg @@ -0,0 +1,203 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + ${line} + + + + ${line} + + diff --git a/src/plugins/tails/default/default-menu-screen.svg b/src/plugins/tails/default/default-menu-screen.svg new file mode 100644 index 0000000..6deafb5 --- /dev/null +++ b/src/plugins/tails/default/default-menu-screen.svg @@ -0,0 +1,145 @@ + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + ${title} - ${subtitle} + + + + + + ${message} + + diff --git a/src/plugins/tails/default/g19-menu-entry.svg b/src/plugins/tails/default/g19-menu-entry.svg new file mode 100644 index 0000000..65d8f39 --- /dev/null +++ b/src/plugins/tails/default/g19-menu-entry.svg @@ -0,0 +1,139 @@ + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + ${line} + + diff --git a/src/plugins/tails/default/g19-menu-screen.svg b/src/plugins/tails/default/g19-menu-screen.svg new file mode 100644 index 0000000..265f25d --- /dev/null +++ b/src/plugins/tails/default/g19-menu-screen.svg @@ -0,0 +1,234 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + ${title} + ${subtitle} + + ${message} + + + + + + + diff --git a/src/plugins/tails/i18n/tails.en_GB.po b/src/plugins/tails/i18n/tails.en_GB.po new file mode 100644 index 0000000..55c1b40 --- /dev/null +++ b/src/plugins/tails/i18n/tails.en_GB.po @@ -0,0 +1,46 @@ +# English translations for gnome15-plugins package. +# Copyright (C) 2012 THE gnome15-plugins'S COPYRIGHT HOLDER +# This file is distributed under the same license as the gnome15-plugins package. +# Automatically generated, 2012. +# +msgid "" +msgstr "" +"Project-Id-Version: gnome15-plugins 0.8.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: 2012-01-08 11:23+0000\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: en_GB\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=ASCII\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: i18n/tails.glade.h:1 +msgid "Files" +msgstr "Files" + +#: i18n/tails.glade.h:2 +msgid "Options" +msgstr "Options" + +#: i18n/tails.glade.h:3 +msgid "Show" +msgstr "Show" + +#: i18n/tails.glade.h:4 +msgid "Tails Preferences" +msgstr "Tails Preferences" + +#: i18n/tails.glade.h:5 +msgid "lines" +msgstr "lines" + +#: i18n/tails.glade.h:6 +msgid "toolbutton1" +msgstr "toolbutton1" + +#: i18n/tails.glade.h:7 +msgid "toolbutton2" +msgstr "toolbutton2" diff --git a/src/plugins/tails/i18n/tails.glade.h b/src/plugins/tails/i18n/tails.glade.h new file mode 100644 index 0000000..21ad402 --- /dev/null +++ b/src/plugins/tails/i18n/tails.glade.h @@ -0,0 +1,7 @@ +char *s = N_("Files"); +char *s = N_("Options"); +char *s = N_("Show"); +char *s = N_("Tails Preferences"); +char *s = N_("lines"); +char *s = N_("toolbutton1"); +char *s = N_("toolbutton2"); diff --git a/src/plugins/tails/i18n/tails.pot b/src/plugins/tails/i18n/tails.pot new file mode 100644 index 0000000..757c10e --- /dev/null +++ b/src/plugins/tails/i18n/tails.pot @@ -0,0 +1,46 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: i18n/tails.glade.h:1 +msgid "Files" +msgstr "" + +#: i18n/tails.glade.h:2 +msgid "Options" +msgstr "" + +#: i18n/tails.glade.h:3 +msgid "Show" +msgstr "" + +#: i18n/tails.glade.h:4 +msgid "Tails Preferences" +msgstr "" + +#: i18n/tails.glade.h:5 +msgid "lines" +msgstr "" + +#: i18n/tails.glade.h:6 +msgid "toolbutton1" +msgstr "" + +#: i18n/tails.glade.h:7 +msgid "toolbutton2" +msgstr "" diff --git a/src/plugins/tails/tailer/Makefile.am b/src/plugins/tails/tailer/Makefile.am new file mode 100644 index 0000000..1919694 --- /dev/null +++ b/src/plugins/tails/tailer/Makefile.am @@ -0,0 +1,5 @@ +plugindir = $(datadir)/gnome15/plugins/tails/tailer +plugin_DATA = __init__.py + +EXTRA_DIST = \ + $(plugin_DATA) \ No newline at end of file diff --git a/src/plugins/tails/tailer/__init__.py b/src/plugins/tails/tailer/__init__.py new file mode 100644 index 0000000..25df9f2 --- /dev/null +++ b/src/plugins/tails/tailer/__init__.py @@ -0,0 +1,300 @@ +# $Id: __init__.py 3 2008-01-29 18:39:09Z msthornton $ + +import re +import time + +class Tailer(object): + """\ + Implements tailing and heading functionality like GNU tail and head + commands. + """ + line_terminators = ('\r\n', '\n', '\r') + + def __init__(self, file, read_size=1024, end=False): + self.read_size = read_size + self.file = file + self.start_pos = self.file.tell() + if end: + self.seek_end() + + def splitlines(self, data): + return re.split('|'.join(self.line_terminators), data) + + def seek_end(self): + self.seek(0, 2) + + def seek(self, pos, whence=0): + self.file.seek(pos, whence) + + def read(self, read_size=None): + if read_size: + read_str = self.file.read(read_size) + else: + read_str = self.file.read() + + return len(read_str), read_str + + def seek_line_forward(self): + """\ + Searches forward from the current file position for a line terminator + and seeks to the charachter after it. + """ + pos = start_pos = self.file.tell() + + bytes_read, read_str = self.read(self.read_size) + + start = 0 + if bytes_read and read_str[0] in self.line_terminators: + # The first charachter is a line terminator, don't count this one + start += 1 + + while bytes_read > 0: + # Scan forwards, counting the newlines in this bufferfull + i = start + while i < bytes_read: + if read_str[i] in self.line_terminators: + self.seek(pos + i + 1) + return self.file.tell() + i += 1 + + pos += self.read_size + self.seek(pos) + + bytes_read, read_str = self.read(self.read_size) + + return None + + def seek_line(self): + """\ + Searches backwards from the current file position for a line terminator + and seeks to the charachter after it. + """ + pos = end_pos = self.file.tell() + + read_size = self.read_size + if pos > read_size: + pos -= read_size + else: + pos = 0 + read_size = end_pos + + self.seek(pos) + + bytes_read, read_str = self.read(read_size) + + if bytes_read and read_str[-1] in self.line_terminators: + # The last charachter is a line terminator, don't count this one + bytes_read -= 1 + + if read_str[-2:] == '\r\n' and '\r\n' in self.line_terminators: + # found crlf + bytes_read -= 1 + + while bytes_read > 0: + # Scan backward, counting the newlines in this bufferfull + i = bytes_read - 1 + while i >= 0: + if read_str[i] in self.line_terminators: + self.seek(pos + i + 1) + return self.file.tell() + i -= 1 + + if pos == 0 or pos - self.read_size < 0: + # Not enought lines in the buffer, send the whole file + self.seek(0) + return None + + pos -= self.read_size + self.seek(pos) + + bytes_read, read_str = self.read(self.read_size) + + return None + + def tail(self, lines=10): + """\ + Return the last lines of the file. + """ + self.seek_end() + end_pos = self.file.tell() + + for i in xrange(lines): + if not self.seek_line(): + break + + data = self.file.read(end_pos - self.file.tell() - 1) + if data: + return self.splitlines(data) + else: + return [] + + def head(self, lines=10): + """\ + Return the top lines of the file. + """ + self.seek(0) + + for i in xrange(lines): + if not self.seek_line_forward(): + break + + end_pos = self.file.tell() + + self.seek(0) + data = self.file.read(end_pos - 1) + + if data: + return self.splitlines(data) + else: + return [] + + def follow(self, delay=1.0): + """\ + Iterator generator that returns lines as data is added to the file. + + Based on: http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/157035 + """ + trailing = True + + while 1: + where = self.file.tell() + line = self.file.readline() + if line: + if trailing and line in self.line_terminators: + # This is just the line terminator added to the end of the file + # before a new line, ignore. + trailing = False + continue + + if line[-1] in self.line_terminators: + line = line[:-1] + if line[-1:] == '\r\n' and '\r\n' in self.line_terminators: + # found crlf + line = line[:-1] + + trailing = False + yield line + else: + trailing = True + self.seek(where) + time.sleep(delay) + + def __iter__(self): + return self.follow() + + def close(self): + self.file.close() + +def tail(file, lines=10): + """\ + Return the last lines of the file. + + >>> import StringIO + >>> f = StringIO.StringIO() + >>> for i in range(11): + ... f.write('Line %d\\n' % (i + 1)) + >>> tail(f, 3) + ['Line 9', 'Line 10', 'Line 11'] + """ + return Tailer(file).tail(lines) + +def head(file, lines=10): + """\ + Return the top lines of the file. + + >>> import StringIO + >>> f = StringIO.StringIO() + >>> for i in range(11): + ... f.write('Line %d\\n' % (i + 1)) + >>> head(f, 3) + ['Line 1', 'Line 2', 'Line 3'] + """ + return Tailer(file).head(lines) + +def follow(file, delay=1.0): + """\ + Iterator generator that returns lines as data is added to the file. + + >>> import os + >>> f = file('test_follow.txt', 'w') + >>> fo = file('test_follow.txt', 'r') + >>> generator = follow(fo) + >>> f.write('Line 1\\n') + >>> f.flush() + >>> generator.next() + 'Line 1' + >>> f.write('Line 2\\n') + >>> f.flush() + >>> generator.next() + 'Line 2' + >>> f.close() + >>> fo.close() + >>> os.remove('test_follow.txt') + """ + return Tailer(file, end=True).follow(delay) + +def _test(): + import doctest + doctest.testmod() + +def _main(filepath, options): + tailer = Tailer(open(filepath, 'rb')) + + try: + try: + if options.lines > 0: + if options.head: + if options.follow: + print >>sys.stderr, 'Cannot follow from top of file.' + sys.exit(1) + lines = tailer.head(options.lines) + else: + lines = tailer.tail(options.lines) + + for line in lines: + print line + elif options.follow: + # Seek to the end so we can follow + tailer.seek_end() + + if options.follow: + for line in tailer.follow(delay=options.sleep): + print line + except KeyboardInterrupt: + # Escape silently + pass + finally: + tailer.close() + +def main(): + from optparse import OptionParser + import sys + + parser = OptionParser(usage='usage: %prog [options] filename') + parser.add_option('-f', '--follow', dest='follow', default=False, action='store_true', + help='output appended data as the file grows') + + parser.add_option('-n', '--lines', dest='lines', default=10, type='int', + help='output the last N lines, instead of the last 10') + + parser.add_option('-t', '--top', dest='head', default=False, action='store_true', + help='output lines from the top instead of the bottom. Does not work with follow') + + parser.add_option('-s', '--sleep-interval', dest='sleep', default=1.0, metavar='S', type='float', + help='with -f, sleep for approximately S seconds between iterations') + + parser.add_option('', '--test', dest='test', default=False, action='store_true', + help='Run some basic tests') + + (options, args) = parser.parse_args() + + if options.test: + _test() + elif not len(args) == 1: + parser.print_help() + sys.exit(1) + else: + _main(args[0], options) + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/src/plugins/tails/tails.py b/src/plugins/tails/tails.py new file mode 100644 index 0000000..25d28c7 --- /dev/null +++ b/src/plugins/tails/tails.py @@ -0,0 +1,356 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2011 Brett Smith +# +# 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 . + +import gnome15.g15locale as g15locale +_ = g15locale.get_translation("tails", modfile = __file__).ugettext + +import gnome15.util.g15gconf as g15gconf +import gnome15.util.g15cairo as g15cairo +import gnome15.util.g15icontools as g15icontools +import gnome15.util.g15markup as g15markup +import gnome15.g15theme as g15theme +import gnome15.g15driver as g15driver +import gnome15.g15screen as g15screen +import subprocess +import time +import tailer +import os +import gtk +import gconf +import logging +import xdg.Mime as mime +from threading import Thread +logger = logging.getLogger(__name__) + +# Plugin details - All of these must be provided +id = "tails" +name = _("Tails") +description = _("Monitor multiple files, updating when they change. Just \ +like the tail command.\n\n\ +\ +Warning: When monitoring large files that grow quickly, this plugin may \ +cause massive memory usage.\n\n\ +Uses the pytailer library (http://code.google.com/p/pytailer/), licensed \ +under the LGPL. See %s and %s for more details." % ( os.path.join(__file__, "LICENSE" ), os.path.join(__file__, "README" ) ) ) +author = "Brett Smith " +copyright = _("Copyright (C)2011 Brett Smith, Michael Thornton") +site = "http://www.russo79.com/gnome15" +has_preferences = True +unsupported_models = [ g15driver.MODEL_G110, g15driver.MODEL_G11, g15driver.MODEL_G930, g15driver.MODEL_G35 ] +actions={ + g15driver.PREVIOUS_SELECTION : _("Previous line"), + g15driver.NEXT_SELECTION : _("Next line"), + g15driver.NEXT_PAGE : _("Next page"), + g15driver.PREVIOUS_PAGE : _("Previous page"), + g15driver.SELECT : _("Open file in browser") + } + +def create(gconf_key, gconf_client, screen): + return G15Tails(gconf_client, gconf_key, screen) + +def show_preferences(parent, driver, gconf_client, gconf_key): + G15TailsPreferences(parent, driver, gconf_client, gconf_key) + +def changed(widget, key, gconf_client): + gconf_client.set_bool(key, widget.get_active()) + +class G15TailsPreferences(): + + def __init__(self, parent, driver, gconf_client, gconf_key): + self._gconf_client = gconf_client + self._gconf_key = gconf_key + + widget_tree = gtk.Builder() + widget_tree.add_from_file(os.path.join(os.path.dirname(__file__), "tails.ui")) + + # Feeds + self.file_model = widget_tree.get_object("FileModel") + self.reload_model() + self.file_list = widget_tree.get_object("FileList") + self.file_renderer = widget_tree.get_object("FileRenderer") + + # Lines + self.lines_adjustment = widget_tree.get_object("LinesAdjustment") + self.lines_adjustment.set_value(g15gconf.get_int_or_default(self._gconf_client, "%s/lines" % self._gconf_key, 10)) + + # Connect to events + self.lines_adjustment.connect("value-changed", self.lines_changed) + self.file_renderer.connect("edited", self.file_edited) + widget_tree.get_object("NewFile").connect("clicked", self.new_file) + widget_tree.get_object("RemoveFile").connect("clicked", self.remove_file) + + # Show dialog + self.dialog = widget_tree.get_object("TailsDialog") + self.dialog.set_transient_for(parent) + + ah = gconf_client.notify_add(gconf_key + "/files", self.files_changed); + self.dialog.run() + self.dialog.hide() + gconf_client.notify_remove(ah); + + def lines_changed(self, widget): + self._gconf_client.set_int(self._gconf_key + "/lines", int(widget.get_value())) + + def add_file(self, file_path): + files = self._gconf_client.get_list(self._gconf_key + "/files", gconf.VALUE_STRING) + if file_path in files: + files.remove(file_path) + files.append(file_path) + self._gconf_client.set_list(self._gconf_key + "/files", gconf.VALUE_STRING, files) + + def file_edited(self, widget, row_index, value): + files = self._gconf_client.get_list(self._gconf_key + "/files", gconf.VALUE_STRING) + row_index = int(row_index) + if value != "": + if self.file_model[row_index][0] != value: + self.file_model.set_value(self.file_model.get_iter(row_index), 0, value) + files[row_index] = value + self._gconf_client.set_list(self._gconf_key + "/files", gconf.VALUE_STRING, files) + else: + self.file_model.remove(self.file_model.get_iter(row_index)) + del files[row_index] + self._gconf_client.set_list(self._gconf_key + "/files", gconf.VALUE_STRING, files) + + def files_changed(self, client, connection_id, entry, args): + self.reload_model() + + def reload_model(self): + self.file_model.clear() + for url in self._gconf_client.get_list(self._gconf_key + "/files", gconf.VALUE_STRING): + self.file_model.append([ url, True ]) + + def new_file(self, widget): + dialog = gtk.FileChooserDialog(_("Add file to monitor.."), + None, + gtk.FILE_CHOOSER_ACTION_OPEN, + (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, + gtk.STOCK_OPEN, gtk.RESPONSE_OK)) + dialog.set_default_response(gtk.RESPONSE_OK) + dialog.set_transient_for(self.dialog) + response = dialog.run() + if response == gtk.RESPONSE_OK: + self.file_model.append([dialog.get_filename(), True]) + self.add_file(dialog.get_filename()) + + dialog.destroy() + + def remove_file(self, widget): + (model, path) = self.file_list.get_selection().get_selected() + file = model[path][0] + files = self._gconf_client.get_list(self._gconf_key + "/files", gconf.VALUE_STRING) + if file in files: + files.remove(file) + self._gconf_client.set_list(self._gconf_key + "/files", gconf.VALUE_STRING, files) + + +class G15TailMenuItem(g15theme.MenuItem): + def __init__(self, id, line, file_path): + g15theme.MenuItem.__init__(self, id) + self.line = line + self.file = file_path + + def on_configure(self): + self.set_theme(g15theme.G15Theme(self.parent.get_theme().dir, "menu-entry")) + + def get_theme_properties(self): + element_properties = g15theme.MenuItem.get_theme_properties(self) + element_properties["line"] = self.line + return element_properties + + def activate(self): + logger.info("xdg-open '%s'", self.file) + subprocess.Popen(['xdg-open', self.file]) + return True + +class G15TailThread(Thread): + def __init__(self, page): + Thread.__init__(self) + self.page = page + self.fd = None + self.line_seq = 0 + self.setDaemon(True) + self.setName("Monitor%s" % self.page.file_path) + self._stopped = False + + def stop_monitoring(self): + self._stopped = True + if self.fd is not None: + self.fd.close() + + def run(self): + for line in tailer.tail(open(self.page.file_path), self.page.plugin.lines): + g15screen.run_on_redraw(self._add_line, line) + self.fd = open(self.page.file_path) + try: + for line in tailer.follow(self.fd): + if self._stopped: + break + g15screen.run_on_redraw(self._add_line, line) + if self._stopped: + break + except ValueError as e: + logger.debug("Error while reading", exc_info = e) + if not self._stopped: + raise e + self.page.redraw() + + def _add_line(self, line): + line = line.strip() + if len(line) > 0 and not self._stopped: + line = g15markup.html_escape(line) + while self.page._menu.get_child_count() > self.page.plugin.lines: + self.page._menu.remove_child_at(0) + self.page._menu.add_child(G15TailMenuItem("Line-%d" % self.line_seq, line, self.page.file_path)) + self.page._menu.select_last_item() + self.line_seq += 1 + +class G15TailPage(g15theme.G15Page): + + def __init__(self, plugin, file_path): + + self._gconf_client = plugin._gconf_client + self._gconf_key = plugin._gconf_key + self._screen = plugin._screen + self._icon_surface = None + self._icon_embedded = None + self.plugin = plugin + self.file_path = file_path + self.thread = None + self.index = -1 + self._menu = g15theme.Menu("menu") + g15theme.G15Page.__init__(self, os.path.basename(file_path), self._screen, + thumbnail_painter=self._paint_thumbnail, + theme=g15theme.G15Theme(self, "menu-screen"), theme_properties_callback=self._get_theme_properties, + originating_plugin = plugin) + self.add_child(self._menu) + self.add_child(g15theme.MenuScrollbar("viewScrollbar", self._menu)) + self._reload() + self._screen.add_page(self) + self._screen.redraw(self) + self.on_deleted = self._stop + + """ + Private + """ + + def _reload(self): + icons = [] + mime_type = mime.get_type(self.file_path) + if mime_type != None: + icons.append(str(mime_type).replace("/","-")) + icons.append("text-plain") + icons.append("panel-searchtool") + icons.append("gnome-searchtool") + icon = g15icontools.get_icon_path(icons, size=self.plugin._screen.height) + + if icon is None: + self._icon_surface = None + self._icon_embedded = None + else: + try : + icon_surface = g15cairo.load_surface_from_file(icon) + self._icon_surface = icon_surface + self._icon_embedded = g15icontools.get_embedded_image_url(icon_surface) + except Exception as e: + logger.warning("Failed to get icon %s", str(icon), exc_info = e) + self._icon_surface = None + self._icon_embedded = None + + self._stop() + if os.path.exists(self.file_path): + self._subtitle = time.strftime('%Y-%m-%d %H:%M', time.localtime(os.path.getmtime(self.file_path))) + self._message = "" + self.thread = G15TailThread(self) + self.thread.start() + else: + self._subtitle = "" + self._message = "File does not exist" + + def _stop(self): + if self.thread is not None: + self.thread.stop_monitoring() + self.thread = None + + def _get_theme_properties(self): + properties = {} + properties["title"] = self.title + properties["icon"] = self._icon_embedded + properties["subtitle"] = self._subtitle + properties["message"] = self._message + properties["alt_title"] = "" + return properties + + def _paint_thumbnail(self, canvas, allocated_size, horizontal): + if self._icon_surface: + return g15cairo.paint_thumbnail_image(allocated_size, self._icon_surface, canvas) + +class G15Tails(): + + def __init__(self, gconf_client, gconf_key, screen): + self._screen = screen; + self._gconf_key = gconf_key + self._gconf_client = gconf_client + + def activate(self): + self._pages = {} + self._lines_changed_handle = self._gconf_client.notify_add(self._gconf_key + "/lines", self._lines_changed) + self._files_changed_handle = self._gconf_client.notify_add(self._gconf_key + "/files", self._files_changed) + self._load_files() + + def deactivate(self): + self._gconf_client.notify_remove(self._lines_changed_handle); + self._gconf_client.notify_remove(self._files_changed_handle); + for page in self._pages: + self._screen.del_page(self._pages[page]) + self._pages = {} + + ''' + Private + ''' + + def destroy(self): + pass + + def _lines_changed(self, client, connection_id, entry, args): + self._load_files() + + def _files_changed(self, client, connection_id, entry, args): + self._load_files() + + def _load_files(self): + self.lines = g15gconf.get_int_or_default(self._gconf_client, "%s/lines" % self._gconf_key, 10) + file_list = self._gconf_client.get_list(self._gconf_key + "/files", gconf.VALUE_STRING) + + def init(): + # Add new pages + for file_path in file_list: + if not file_path in self._pages: + pg = G15TailPage(self, file_path) + self._pages[file_path] = pg + else: + self._pages[file_path]._reload() + + # Remove pages that no longer exist + to_remove = [] + for file_path in self._pages: + page = self._pages[file_path] + if not page.file_path in file_list: + self._screen.del_page(page) + to_remove.append(file_path) + for page in to_remove: + del self._pages[page] + g15screen.run_on_redraw(init) + diff --git a/src/plugins/tails/tails.ui b/src/plugins/tails/tails.ui new file mode 100644 index 0000000..6eebe35 --- /dev/null +++ b/src/plugins/tails/tails.ui @@ -0,0 +1,272 @@ + + + + + + + + + + + + + + 1 + 100 + 1 + 1 + 1 + + + 320 + False + 5 + Tails Preferences + False + True + center-on-parent + dialog + + + True + False + 2 + + + True + False + end + + + gtk-close + True + True + True + True + + + False + False + 0 + + + + + False + True + end + 0 + + + + + True + False + + + True + False + + + True + False + 0 + none + + + True + False + 12 + + + True + False + + + True + False + + + True + False + toolbutton1 + True + gtk-add + + + False + True + + + + + True + False + toolbutton2 + True + gtk-remove + + + False + True + + + + + False + False + 0 + + + + + True + True + automatic + automatic + in + + + 200 + True + True + FileModel + False + False + 0 + + + URL + + + + 1 + 0 + + + + + + + + + True + True + 1 + + + + + + + + + True + False + <b>Files</b> + True + + + + + True + True + 0 + + + + + True + False + 0 + none + + + True + False + 12 + + + True + False + + + True + False + Show + + + True + True + 0 + + + + + True + True + + False + False + True + True + LinesAdjustment + + + True + True + 1 + + + + + True + False + lines + + + True + True + 2 + + + + + + + + + True + False + <b>Options</b> + True + + + + + True + True + 1 + + + + + True + True + 0 + + + + + False + False + 1 + + + + + + button9 + + + diff --git a/src/plugins/things/Makefile.am b/src/plugins/things/Makefile.am new file mode 100644 index 0000000..771aa9e --- /dev/null +++ b/src/plugins/things/Makefile.am @@ -0,0 +1,9 @@ +SUBDIRS = cg.stuff clouds.stuff + +plugindir = $(datadir)/gnome15/plugins/things +plugin_DATA = things.py \ + test1.py \ + cloudsthingum.py + +EXTRA_DIST = \ + $(plugin_DATA) diff --git a/src/plugins/things/cg.stuff/Makefile.am b/src/plugins/things/cg.stuff/Makefile.am new file mode 100644 index 0000000..02f231c --- /dev/null +++ b/src/plugins/things/cg.stuff/Makefile.am @@ -0,0 +1,5 @@ +cgstuffdir = $(datadir)/gnome15/plugins/things/cg.stuff +cgstuff_DATA = cairo.svg + +EXTRA_DIST = \ + $(cgstuff_DATA) diff --git a/src/plugins/things/cg.stuff/cairo.svg b/src/plugins/things/cg.stuff/cairo.svg new file mode 100644 index 0000000..770248e --- /dev/null +++ b/src/plugins/things/cg.stuff/cairo.svg @@ -0,0 +1,554 @@ + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/plugins/things/clouds.stuff/Makefile.am b/src/plugins/things/clouds.stuff/Makefile.am new file mode 100644 index 0000000..b5cad5e --- /dev/null +++ b/src/plugins/things/clouds.stuff/Makefile.am @@ -0,0 +1,6 @@ +cloudsstuffdir = $(datadir)/gnome15/plugins/things/clouds.stuff +cloudsstuff_DATA = README \ + clouds.svg + +EXTRA_DIST = \ + $(cloudsstuff_DATA) diff --git a/src/plugins/things/clouds.stuff/README b/src/plugins/things/clouds.stuff/README new file mode 100644 index 0000000..2bfa351 --- /dev/null +++ b/src/plugins/things/clouds.stuff/README @@ -0,0 +1,5 @@ +The following images are Copyright (C) 2009 Donn.C.Ingle and may be redistributed +and/or modified 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. +1. clouds.svg diff --git a/src/plugins/things/clouds.stuff/clouds.svg b/src/plugins/things/clouds.stuff/clouds.svg new file mode 100644 index 0000000..8481439 --- /dev/null +++ b/src/plugins/things/clouds.stuff/clouds.svg @@ -0,0 +1,1355 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/plugins/things/cloudsthingum.py b/src/plugins/things/cloudsthingum.py new file mode 100644 index 0000000..01b223c --- /dev/null +++ b/src/plugins/things/cloudsthingum.py @@ -0,0 +1,130 @@ +#!/usr/bin/env python +# -*- coding: utf8 -*- + +## Things Copyright(C) 2009 Donn.C.Ingle +## +## Contact: donn.ingle@gmail.com - I hope this email lasts. +## +## This file is part of Things. +## +## Things 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. +## +## Things 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 Things. If not, see . + + +from Things.ThingsApp import * +from Things.Thinglets import * +from Things.BoxOfTricks import * + + + +## ---- General +SKYHEXBLUE="#aaccff"; SKYBLUE=hexfloat(SKYHEXBLUE) + +class BlueSky(DrawThing): + R = cairo.RadialGradient(0,200,0,0,200,400) + R.add_color_stop_rgb(0, 1,1,1 ) + R.add_color_stop_rgb(1, *SKYBLUE ) + def draw(self,ctx,fr): + ctx.set_source( BlueSky.R ) + ctx.paint() + +class Cloud1(DrawThing): + def draw(self,ctx,fr): + BOS['clouds:cloud1'].draw(ctx) +class Cloud2(DrawThing): + def draw(self,ctx,fr): + BOS['clouds:cloud2'].draw(ctx) + +class Puffer(Thing): + def __init__(self,smax,smin): + Thing.__init__(self) + self.keys("#----------------#----------------------#--------#",Props(),Props(sz=smax),Props(sz=smin),Props() ) + +class CloudA(Thing): + def __init__(self): + Thing.__init__(self) + self.keys("#" + "-"*190 + "#" + "-"*190 + "#",Props(x=250),Props(x=-250),Props(x=250)) + self.loops=True + + self.P = Puffer(1.5,0.6) + self.P.add( Cloud1() ) + self.add( self.P) + +class CloudB(Thing): + def __init__(self): + Thing.__init__(self) + self.keys("#" + "-"*90 + "#" + "-"*90 + "#",Props(x=-220),Props(x=220),Props(x=-220)) + self.loops=True + self.P = Puffer(1.2,0.8) + self.P.add( Cloud2() ) + self.add( self.P) + +class HugeCloud(Thing): + def __init__(self): + Thing.__init__(self) + self.keys('#',Props(sz=3,a=0.5, x=-90, y=-80)) + self.add( Cloud1() ) + +class SpinCity(Thing): + def __init__(self): + Thing.__init__(self) + ## Let's use the Python multiply string trick to get lots of tween frames: + self.keys ( "#" + "-"*250 + "#", Props(), Props(rot=-pi2)) + + def draw(self,ctx,fr): + BOS['clouds:city'].draw(ctx) + +## Here we use two LoopThings. +class Walking(LoopThing): + ## The legs - on the loops->walkloop layer in the Inkscape SVG file. + def __init__(self): + LoopThing.__init__(self) + self.keys("#--#---#---#---#---#----#---#---#---#---#===",Props(),Props(),Props(),Props(),Props(),Props(),Props(),Props(),Props(),Props(),Props()) + + self.addLoop( BOS["clouds:walkloop"]) +class Torso(LoopThing): + ## The 'torso' -- loops->torsoloop layer in the SVG + def __init__(self): + LoopThing.__init__(self) + self.keys("#--#---#---#",Props(),Props(),Props(),Props()) + + self.addLoop( BOS["clouds:torsoloop"]) + +class Walker(Thing): + def __init__(self): + Thing.__init__(self) + self.keys('#',Props()) + self.add( Walking() ) + self.add( Torso(), globalProps=Props(x=-10, y=10) ) + +class Madness(Thing): + ### This is our 'main' Thing. It holds all the action. + def __init__(self): + Thing.__init__(self) + self.keys("#",Props()) + self.loops = False + + self.add( SpinCity(), globalProps=Props(y=250,sz=1.3), layer=10) + self.add( CloudA(), layer=5 ) + self.add( CloudB(), layer=20 ) + self.add( HugeCloud(), layer=1) + + self.add( Walker(), globalProps=Props(sz=1, y=100),layer=30 ) + +## BEGIN THE APP + +## Get a Bag of stuff +BOS = BagOfStuff() + +# Add stuff to it +BOS.add(os.path.join(os.path.dirname(__file__), "clouds.stuff/clouds.svg"),"clouds") diff --git a/src/plugins/things/test1.py b/src/plugins/things/test1.py new file mode 100644 index 0000000..ec1ffdf --- /dev/null +++ b/src/plugins/things/test1.py @@ -0,0 +1,341 @@ +#!/usr/bin/env python +# -*- coding: utf8 -*- + +## Things Copyright(C) 2009 Donn.C.Ingle +## +## Contact: donn.ingle@gmail.com - I hope this email lasts. +## +## This file is part of Things. +## +## Things 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. +## +## Things 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 Things. If not, see . + + +from Things.ThingsApp import * +from Things.Thinglets import * +from Things.BoxOfTricks import * + +## NOTE: +## Head down to the end: look for the first scene (class FadeStart) and work from there. +## Written with Version 0.1 of the API: 2 May 2009 + + +## ---- General +CAIROHEXBLUE="#162284"; CAIROBLUE=hexfloat(CAIROHEXBLUE) + +class Backdrop(Thing): + ## This Thing holds three frames with three draw methods; one for each. + ## We use the frame number to decide which to employ. + ## This thing serves as a background drawer for each scene. + def __init__(self): + Thing.__init__(self) + self.keys ( "#==", Props()) + self.stops( "^^^" ) + self.loops=False + + self.draws=[self.draw1,self.draw2,self.draw3] + + self.L = cairo.LinearGradient(0, -300, 0, 300) + self.L.add_color_stop_rgba(0, 1, 1, 1, 1) + self.L.add_color_stop_rgba(0.25, 0, 0.6, 1, 1) + self.L.add_color_stop_rgba(0.5, 0, 0.8, 1, 1) + self.L.add_color_stop_rgba(1, 1, 1, 1, 1) + + self.yell = hexfloat("#dfaa00") + + self.R = cairo.RadialGradient(0,-20,50,0,-20,300) + self.R.add_color_stop_rgb(0, 1,1,1) + self.R.add_color_stop_rgb(1, *self.yell) + def draw(self, ctx, fr): + self.draws[fr-1](ctx) + + def draw1(self, ctx): + ctx.set_source(self.L) + ctx.paint() + def draw2(self, ctx): + ctx.set_source(self.R) + ctx.paint() + def draw3(self, ctx): + ctx.set_source_rgb(*self.yell) + ctx.paint() + +class ScarabShape(DrawThing): + def draw(self, ctx, fr): + BOS["CG:cairo_scarab"].draw(ctx) +SCARABsh=ScarabShape() + +## ----------------------------------- SCENE 3 + +class Exit(Thing): + def __init__(self, app): + Thing.__init__(self) + self.keys ( "#======================================================================================.", Props()) + self.stops ( ".......................................................................................^") + self.funcs ( "^.....................................................................................^", (BACKDROP.goStop,3), app.quit ) + + class EndScarab(Thing): + def __init__(self): + Thing.__init__(self) + self.keys( "#----------------------------------#---------------------------#",Props(),Props(sz=6,rot=pi),Props(a=0,rot=pi2)) + self.loops=False + self.add(SCARABsh) + self.add( EndScarab() ) + + #def draw(self,ctx,fr): + # ctx.set_source_rgb(0,0,0) + # ctx.paint() + +## ----------------------------------- SCENE 2 + +class ClipWord(ClipThing): + def __init__(self): + ClipThing.__init__(self) + self.keys( "#", Props()) + self.loops = False + + class CairoWord(Thing): + def __init__(self): + Thing.__init__(self) + self.keys( ".#-----------------------------------------------------------------#", Props(y=45),Props()) + self.stops( "^..................................................................^") + self.funcs( "..................................................................^",self.dostuff ) + def dostuff(self): + self.parentThing.parentThing.SUN.goPlay(2) + + def draw(self, ctx, fr): + BOS["CG:cairo_word"].draw(ctx) + self.CAIROWORD = CairoWord() + self.add( self.CAIROWORD ) + + def draw(self,ctx,fr): + ctx.rectangle(-150,-35,260,65) + + +# Prep some shapes from the SVG file +class ArrowButtonN(DrawThing): + def draw(self,ctx,fr): + BOS["CG:button_right_normal"].draw(ctx) +class ArrowButtonO(DrawThing): + def draw(self,ctx,fr): + BOS["CG:button_right_over"].draw(ctx) +class ArrowButtonD(DrawThing): + def draw(self,ctx,fr): + BOS["CG:button_right_down"].draw(ctx) +class ArrowButtonU(DrawThing): + def draw(self,ctx,fr): + BOS["CG:button_right_up"].draw(ctx) + +class Next(Thing): + def __init__(self): + Thing.__init__(self) + self.keys("#--#-----#", Props(sz=2,a=0),Props(sz=0.5),Props()) + self.loops = False + # Just define the button within myself -- it shows the relationship better. + class NextButton(ButtonThing): + """ButtonThings cannot have keys().""" + def __init__(self): + ButtonThing.__init__(self,"testbutton") + self.addStates({"normal":ArrowButtonN(),"over":ArrowButtonO(),"down":ArrowButtonD(),"up":ArrowButtonU()} ) + def drawHitarea(self,ctx,frame): + ctx.rectangle(-14,-14,30,30) + def onButtonUp(self): + self.parentThing.parentThing.parentThing.goPlay('hide') + + self.add(NextButton()) + +class IntroText(Thing): + def __init__(self): + Thing.__init__(self) + self.keys ( "#----------------------------------------------------------------#",Props(a=0),Props(a=1)) + self.stops( ".................................................................^") + + ## Button appears a little later in the animation: + self.add( Next(), parentFrame=65, globalProps=Props(x=200,y=240),layer=20) + + fname = "Sans 10" + + txt= """Thanks to Cairo, Python and many others, there is now an easy way to produce vector animations in Python code. This library is called "Things" and it's what you are seeing right now. + +It needs work. It's slow and inefficient. But, oddly, it runs! If it could be converted into a C library "Things" would really start to cook! A GUI timeline & on-canvas designer would then be possible. + +"Things" works alongside Inkscape. You can pull items out by id and employ them in the API. You can also add images and font files as you need them. + +Please check it out and hack!""" + + self.text = '%s' % (fname, CAIROHEXBLUE, txt ) + + self.tbox = TextBlock() + self.tbox.setup(self.text, x=-250, y=40, align=pango.ALIGN_LEFT, width=500) + def draw(self,ctx,fr): + self.tbox.draw(ctx) + + + +class BlueBox(Thing): + def __init__(self): + Thing.__init__(self) + self.keys ( "#-------------------------#==================",Props(sx=0.1),Props()) + self.stops ( "..........................^.................^") + self.funcs ( ".........................^.^", self.tell,self.dostuff) + self.labels( "...........................^", "grow") + + self.h=1 + def dostuff(self): + ## Change a flag so draw can do stuff. + self.h=2 + + def tell(self): + ## Tell logo to pop-up. + self.parentThing.CAIROCLIP.CAIROWORD.play() + + def draw(self,ctx,fr): + ## Rather than tween this box down, I draw it bigger every time, to avoid distortion. + if self.h > 1: + fr = self.h + self.h += 1 + if self.h > 25: self.h=25 + + ctx.rectangle(-260,30,510,((self.h-1)*10)) + + # The blue outline that grows. + ctx.set_line_width(5) + ctx.set_source_rgb(*CAIROBLUE) # The * means make a list of CAIROBLUE -- so it's passed into the func as three params! + ctx.stroke_preserve() + ctx.set_source_rgba(1,1,1,0.7) + ctx.fill() + +class RisingSun(Thing): + def __init__(self): + Thing.__init__(self) + ## Starts on a blank + stop frame : i.e. it is not visible at first. + ## Somewhere there will be a command to tell this to play from frame 2. + self.keys ( ".#----#--#--------#",Props(a=0,sz=2),Props(sz=0.5),Props(sz=2),Props() ) + self.stops ( "^.................^") + self.funcs ( ".................^",self.dostuff) + + ## Here we add a pre-prepared Thing. It will draw itself. + self.add( SCARABsh ) + + def dostuff(self): + ## A func to tell some other thing to do something. + self.parentThing.BLUEBOX.goPlay("grow") + + +class IntroduceLogo(Thing): + """ + This is the main Thing in scene 2. It was elected thus when we added it to the scene2 var. + """ + def __init__(self, app): + Thing.__init__(self) + self.keys ( "#==============.", Props()) + self.stops ( ".........^.....^") + ## I want to spend some time simply + ## looping so that sub-animations get a chance to + ## finish. Hence this ^.^ cute hello-kitty stuff: + self.labels( ".^.^......^" ,"pause","go","hide") + self.funcs ( "^.^...........^", (BACKDROP.goStop,2), self.Delay, app.playNextScene ) + + ## Add the elements of my animation: + ## Some are given instance in self because I will refer to them from elsewhere. + self.BLUEBOX=BlueBox() + self.add( self.BLUEBOX, layer=12) + + self.CAIROCLIP = ClipWord() + self.add( self.CAIROCLIP, layer=11) + + self.SUN=RisingSun() + self.add( self.SUN, layer=0, globalProps=Props(x=5,y=-70)) + + ## This one starts on frame 6, just after the delay. + self.add( IntroText(), layer=15, parentFrame=6) # It's instanced on-the-fly. + + ## Used in Delay func. + self.countdown=60 + + def Delay(self): + ## So, we are on frame 3 and this func is called. + self.countdown -=1 + if self.countdown == 0: + self.goPlay("go") # all done, continue animation. + else: + self.goPlay("pause") # rewind and loop again. + + + +## ----------------------------------- SCENE 1 +class BuzzWord(Thing): + def __init__(self, pFrom, buzz): + Thing.__init__(self) + pFrom2 = Props(x=pFrom.y/2, y=pFrom.x/2,sz=5) + self.keys ( "#----------------------#----------------------------------------------------------#",pFrom,Props(),pFrom2) + ## Run a func in myself on the last frame. + self.funcs ( "..................................................................................^", self.atend) + + self.loops=False + self.buzz=buzz + + fname = "Serif 11" + self.text = '%s' % (fname, buzz ) + self.tbox = TextBlock() + self.tbox.setup(self.text, x=0, y=0, align=pango.ALIGN_CENTER) + def draw(self,ctx,fr): + self.tbox.draw(ctx) + def atend(self): + ## I am finished, so tell my parent. + self.parentThing.atend(self.buzz) # pass my buzz phrase. + +class BuzzWords(Thing): + def __init__(self): + Thing.__init__(self) + ## We provide a bunch of frames because we want to start manu instances + ## of a Thing -- and space them out every so-many frames. + self.keys ( "#=============================",Props()) + self.loops=False + + bzz=["Animation","Vectors","Cairo","Python","Tweening","Keyframes","Simple","API","Things","GPL"] + for bw in range(0,10): + x,y=circrandom(600) + BW = BuzzWord( Props(x=x,y=y,sz=20), bzz[bw] ) + self.add( BW, parentFrame=bw*3) # Here we start each one on different frames; this staggers the animation. + + def atend(self,buzz): + ## Am I done with the animations? + ## only the last one in the list is what we want. + if buzz=="GPL": + self.parentThing.play() # We tell the parentThing (FadeStart) to carry on playing now. + +class FadeStart(Thing): + """ + Start looking here to understand the whole animation. + """ + def __init__(self, app): + Thing.__init__(self) + ## We have a stop on frame 2. The BuzzWords() Thing is playing all the time, but this + ## timeline does not go past frame 2; until we tell it to... + self.keys ( "##----------#.", Props(),Props(),Props(a=0,sz=0.1)) + self.stops ( ".^...........^") + ## When this gets to the end, it will run a method of app: + self.funcs ( "............^", app.playNextScene ) # Off we go to scene2! + + self.add( BuzzWords() ) + + +## Get a Bag of stuff +BOS = BagOfStuff() + +# Add stuff to it +BOS.add(os.path.join(os.path.dirname(__file__), "cg.stuff/cairo.svg"),"CG") + +## Add Things to app +BACKDROP=Backdrop() + diff --git a/src/plugins/things/things.py b/src/plugins/things/things.py new file mode 100644 index 0000000..557791d --- /dev/null +++ b/src/plugins/things/things.py @@ -0,0 +1,164 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2011 Brett Smith +# +# 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 . + +import gnome15.g15driver as g15driver +import gnome15.g15screen as g15screen +import gnome15.util.g15convert as g15convert +import gnome15.util.g15scheduler as g15scheduler +import logging +logger = logging.getLogger(__name__) + +from Things.ThingsApp import * +from Things.Thinglets import * +from Things.BoxOfTricks import * +from Things.OutputDevice import * + + +# Plugin details - All of these must be provided +id="things" +name="Things" +description="Integrates the Things. A python animation API. Doesn't do anything " + \ + "by itself, but provides a framework for other plugins to add " + \ + "animations and special effects" +author="Brett Smith " +copyright="Copyright (C)2011 Brett Smith" +site="http://www.gnome15.org/" +has_preferences=False +unsupported_models = [ g15driver.MODEL_G110, g15driver.MODEL_G11 ] + +def create(gconf_key, gconf_client, screen): + return G15Things(gconf_key, gconf_client, screen) + +class G15ThingOutputDevice(OutputDevice): + + def __init__(self, canvas_width, canvas_height, screen): + OutputDevice.__init__(self) + self._screen = screen + self._windowSize = (canvas_width, canvas_height, canvas_width, canvas_height) + + def is_button_press(self, e): + return False + + def is_button_release(self, e): + return False + + def is_motion(self, e): + return False + + def comeToLife(self, owner): + self.owner = owner + g15scheduler.schedule("ThingPaint", self.owner.speed / 1000.0, self._mainLoop) + + ## This gives life to the whole show. + def _mainLoop(self): + """ + Private + ======= + + Called by timeout in comeToLife. Keeps looping on timeout. This is the heart of the app. + + """ + if self.pauseapp: return True + self.owner._tick() + self._screen.redraw() + if not self.stack.quitApp: + g15scheduler.schedule("ThingPaint", self.owner.speed / 1000.0, self._mainLoop) + +class G15ThingPainter(g15screen.Painter): + + def __init__(self, screen): + g15screen.Painter.__init__(self, g15screen.BACKGROUND_PAINTER, -9999) + self.output_device = G15ThingOutputDevice(screen.available_size[0], screen.available_size[1], screen) + self.screen = screen + self.app2() + + def app1(self): + ## BEGIN THE APP + + ## Get an app ref. + ## Fiddle with the speed param. Make it bigger if you want the animation slower. + + import test1 + + app = AllThings ( self.screen.available_size[0], self.screen.available_size[1], speed = 20, output = self.output_device) + app.add(test1.BACKDROP) + + ## Make some scene Things to hold many items each + scene1 = test1.SceneThing() + scene2 = test1.SceneThing() + scene3 = test1.SceneThing() + + ## Add Things to each scene + scene1.add( test1.FadeStart(app) ) + scene2.add( test1.IntroduceLogo(app) ) + scene3.add( test1.Exit(app) ) + + ## Add the scences to the app + app.add( scene1 ) + app.add( scene2 ) + app.add( scene3 ) + + ## Tell it which one to start with + app.startScene(1) + + + #app.showGrid() # optional for debugging + + ## Bring app to life! + app.comeToLife ( ) + + def app2(self): + + ## Get an app ref. + ## Fiddle with the speed param. Make it bigger if you want the animation slower. + import cloudsthingum + app = AllThings ( self.screen.available_size[0], self.screen.available_size[1], speed = 20, output = self.output_device) + app.add( cloudsthingum.BlueSky() ) + ## Add the main thing to the app + app.add( cloudsthingum.Madness() ) + + app.panZoom(True) + ## Bring app to life! + app.comeToLife ( ) + + def paint(self, canvas): + canvas.save() + self.output_device.stack._expose(canvas) + canvas.restore() + +class G15Things(): + + def __init__(self, gconf_key, gconf_client, screen): + self.screen = screen + self.gconf_client = gconf_client + self.gconf_key = gconf_key + self.target_surface = None + self.target_context = None + + def activate(self): + self.bg_img = None + self.this_image = None + self.current_style = None + self.notify_handlers = [] + self.painter = G15ThingPainter(self.screen) + self.screen.painters.append(self.painter) + + def deactivate(self): + self.screen.painters.remove(self.painter) + self.screen.redraw() + + def destroy(self): + pass \ No newline at end of file diff --git a/src/plugins/trafficstats/Makefile.am b/src/plugins/trafficstats/Makefile.am new file mode 100644 index 0000000..7226263 --- /dev/null +++ b/src/plugins/trafficstats/Makefile.am @@ -0,0 +1,10 @@ +SUBDIRS = default + +plugindir = $(datadir)/gnome15/plugins/trafficstats +plugin_DATA = \ + trafficstats.ui \ + trafficstats.png \ + trafficstats.py + +EXTRA_DIST = \ + $(plugin_DATA) \ No newline at end of file diff --git a/src/plugins/trafficstats/default/Makefile.am b/src/plugins/trafficstats/default/Makefile.am new file mode 100644 index 0000000..740d1c4 --- /dev/null +++ b/src/plugins/trafficstats/default/Makefile.am @@ -0,0 +1,27 @@ +themedir = $(datadir)/gnome15/plugins/trafficstats/default +theme_DATA = \ + g19.svg \ + mx5500.svg \ + default.svg + +EXTRA_DIST = \ + $(theme_DATA) + +all-local: + for M_LOCALE in @ENABLED_LOCALES@; do \ + mkdir -p i18n/$$M_LOCALE/LC_MESSAGES ; \ + if [ `ls i18n/*.po 2>/dev/null|wc -l` -gt 0 ]; then \ + for M_PO in i18n/*.po; do \ + BN=`basename $$M_PO .po`; \ + LL=`basename $$BN .$$M_LOCALE`.mo; \ + echo "$$M_PO -> $$LL"; \ + msgfmt $$M_PO --output-file i18n/$$M_LOCALE/LC_MESSAGES/$$LL; \ + done; \ + fi; \ + done + +install-exec-hook: + for M_LOCALE in @ENABLED_LOCALES@; do \ + mkdir -p $(DESTDIR)$(datadir)/gnome15/plugins/trafficstats/default/i18n; \ + cp -pR i18n/$$M_LOCALE $(DESTDIR)$(datadir)/gnome15/plugins/trafficstats/default/i18n; \ + done \ No newline at end of file diff --git a/src/plugins/trafficstats/default/default.svg b/src/plugins/trafficstats/default/default.svg new file mode 100644 index 0000000..95fe685 --- /dev/null +++ b/src/plugins/trafficstats/default/default.svg @@ -0,0 +1,282 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + ${title} + ${des1} + ${dup1} + ${des2} + ${dup2} + ${des3} + ${dup3} + ${ddn1} + ${d} + ${ddn2} + ${d} + ${ddn3} + ${d} + + + ${sup} + ${sdn} + + + ${message} + + diff --git a/src/plugins/trafficstats/default/g19.svg b/src/plugins/trafficstats/default/g19.svg new file mode 100644 index 0000000..7b5fabc --- /dev/null +++ b/src/plugins/trafficstats/default/g19.svg @@ -0,0 +1,303 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + ${title} + ${des1} + ${dup1} + ${des2} + ${dup2} + ${des3} + ${dup3} + ${ddn1} + ${d} + ${ddn2} + ${d} + ${ddn3} + ${d} + ${sup} + ${sdn} + + + + ${message} + + diff --git a/src/plugins/trafficstats/default/mx5500.svg b/src/plugins/trafficstats/default/mx5500.svg new file mode 100644 index 0000000..6bce46a --- /dev/null +++ b/src/plugins/trafficstats/default/mx5500.svg @@ -0,0 +1,679 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + ${title} + ${des1} + ${dup1} + ${des2} + ${dup2} + ${des3} + ${dup3} + ${ddn1} + ${d} + ${ddn2} + ${d} + ${ddn3} + ${d} + ${sup} + ${sdn} + + + ${message} + + diff --git a/src/plugins/trafficstats/trafficstats.png b/src/plugins/trafficstats/trafficstats.png new file mode 100644 index 0000000..a38bbfd Binary files /dev/null and b/src/plugins/trafficstats/trafficstats.png differ diff --git a/src/plugins/trafficstats/trafficstats.py b/src/plugins/trafficstats/trafficstats.py new file mode 100644 index 0000000..49fbbf0 --- /dev/null +++ b/src/plugins/trafficstats/trafficstats.py @@ -0,0 +1,307 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2013 NoXPhasma +# +# 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 . + +import gnome15.g15locale as g15locale +_ = g15locale.get_translation("trafficstats", modfile = __file__).ugettext + +import gnome15.g15screen as g15screen +import gnome15.g15theme as g15theme +import gnome15.util.g15uigconf as g15uigconf +import gnome15.util.g15gconf as g15gconf +import gnome15.util.g15os as g15os +import gnome15.g15actions as g15actions +import gnome15.g15devices as g15devices +import gnome15.g15driver as g15driver +import gnome15.g15globals as g15globals +import gnome15.g15text as g15text +import gnome15.g15plugin as g15plugin +import time +import datetime +import logging +logger=logging.getLogger(__name__) +try: + import gtop +except Exception as e: + logger.debug("Could not import gtop. Falling back to g15top", exc_info = e) + # API compatible work around for Ubuntu 12.10 + import gnome15.g15top as gtop +import os +import gtk +import locale + +# Plugin details - All of these must be provided +id="trafficstats" +name=_("Traffic Stats") +description=_("Displays network traffic stats. Either of actual session or from vnstat.") +author="NoXPhasma " +copyright=_("Copyright (C)2013 NoXPhasma") +site="http://www.russo79.com/gnome15" +has_preferences=True +default_enabled=True +ICON=os.path.join(os.path.dirname(__file__), 'trafficstats.png') +unsupported_models = [ g15driver.MODEL_G110, g15driver.MODEL_G11, g15driver.MODEL_G930, g15driver.MODEL_G35 ] +actions={ + g15driver.PREVIOUS_SELECTION : _("Switch daily/monthly stats (Only vnstat)"), + g15driver.NEXT_SELECTION : _("Switch network device") + } + +# +# This plugin displays the network traffic stats +# + +''' +This function must create your plugin instance. You are provided with +a GConf client and a Key prefix to use if your plugin has preferences +''' +def create(gconf_key, gconf_client, screen): + return G15TrafficStats(gconf_key, gconf_client, screen) + +def show_preferences(parent, driver, gconf_client, gconf_key): + widget_tree = gtk.Builder() + widget_tree.add_from_file(os.path.join(os.path.dirname(__file__), "trafficstats.ui")) + dialog = widget_tree.get_object("TrafficStats") + + # Resets the value of the use_vnstat flag if vnstat is not installed + vnstat_installed = g15os.is_program_in_path('vnstat') + if not vnstat_installed: + gconf_client.set_bool("%s/use_vnstat" % gconf_key, False) + + # Displays a warning message to the user if vnstat is not installed + warning = widget_tree.get_object("NoVnstatMessage") + warning.set_visible(not vnstat_installed) + + # Disables the vnstat checkbox if vnstat is not installed + use_vnstat = widget_tree.get_object('UseVnstat') + use_vnstat.set_sensitive(vnstat_installed) + + g15uigconf.configure_checkbox_from_gconf(gconf_client, gconf_key + "/use_vnstat", "UseVnstat", vnstat_installed, widget_tree) + ndevice = widget_tree.get_object("NetDevice") + for netdev in gtop.netlist(): + ndevice.append([netdev]) + g15uigconf.configure_combo_from_gconf(gconf_client, gconf_key + "/networkdevice", "NetworkDevice", "lo", widget_tree) + g15uigconf.configure_adjustment_from_gconf(gconf_client, gconf_key + "/refresh_interval", "RefreshingScale", 10.0, widget_tree) + dialog.set_transient_for(parent) + dialog.run() + dialog.hide() + +class G15TrafficStats(g15plugin.G15RefreshingPlugin): + + ''' + ****************************************************************** + * Lifecycle functions. You must provide activate and deactivate, * + * the constructor and destroy function are optional * + ****************************************************************** + ''' + + def __init__(self, gconf_key, gconf_client, screen): + self.gconf_client = gconf_client + self.gconf_key = gconf_key + g15plugin.G15RefreshingPlugin.__init__(self, gconf_client, gconf_key, \ + screen, ICON, id, name, g15gconf.get_float_or_default(self.gconf_client, self.gconf_key + "/refresh_interval", 10.0)) + self.hidden = False + + def activate(self): + ''' + The activate function is invoked when gnome15 starts up, or the plugin is re-enabled + after it has been disabled. When extending any of the provided base plugin classes, + you nearly always want to call the function in the supoer class as well + ''' + self._load_configuration() + + g15plugin.G15RefreshingPlugin.activate(self) + + ''' + Most plugins will delegate their drawing to a 'Theme'. A theme usually consists of an SVG file, one + for each model that is supported, and optionally a fragment of Python for anything that can't + be done with SVG and the built in theme facilities + ''' + self._reload_theme() + + self.watch(None, self._config_changed) + + self.page.title = "Traffic Stats" + + ''' + Once created, we should always ask for the screen to be drawn (even if another higher + priority screen is actually active. If the canvas is not displayed immediately, + the on_shown function will be invoked when it finally is. + ''' + self.screen.redraw(self.page) + + self.screen.key_handler.action_listeners.append(self) + + def deactivate(self): + g15plugin.G15RefreshingPlugin.deactivate(self) + self.screen.key_handler.action_listeners.remove(self) + + def action_performed(self, binding): + if self.page and self.page.is_visible(): + if binding.action == g15driver.PREVIOUS_SELECTION and self.use_vnstat is True: + if self.loadpage == 'vnstat_daily': + self.gconf_client.set_string(self.gconf_key + "/vnstat_view", "vnstat_monthly") + else: + self.gconf_client.set_string(self.gconf_key + "/vnstat_view", "vnstat_daily") + return True + elif binding.action == g15driver.NEXT_SELECTION: + if self.networkdevice is not None: + # get all network devices + self.net_data = gtop.netlist() + # set network device id +1, to get next device + idx = self.net_data.index(self.networkdevice) + 1 + # if next device id is not present, take first device + if idx >= len(self.net_data): + idx = 0 + self.gconf_client.set_string(self.gconf_key + "/networkdevice", self.net_data[idx]) + return True + + def destroy(self): + ''' + Invoked when the plugin is disabled or the applet is stopped + ''' + pass + + def _config_changed(self, client, connection_id, entry, args): + + ''' + Load the gconf configuration + ''' + self._load_configuration() + + ''' + This is called when the gconf configuration changes. See add_notify and remove_notify in + the plugin's activate and deactive functions. + ''' + self.do_refresh() + + ''' + Reload the theme as the layout required may have changed (i.e. with the 'show date' + option has been change) + ''' + self._reload_theme() + + ''' + In this case, we temporarily raise the priority of the page. This will force + the page to be painted (i.e. the paint function invoked). After the specified time, + the page will revert it's priority. Only one revert timer is active at any one time, + so it is safe to call this function in quick succession + ''' + self.screen.set_priority(self.page, g15screen.PRI_HIGH, revert_after = 3.0) + + def _load_configuration(self): + self.use_vnstat = g15gconf.get_bool_or_default(self.gconf_client, self.gconf_key + "/use_vnstat", os.path.isfile("/usr/bin/vnstat")) + self.networkdevice = g15gconf.get_string_or_default(self.gconf_client, self.gconf_key + "/networkdevice", 'lo') + self.loadpage = g15gconf.get_string_or_default(self.gconf_client, self.gconf_key + "/vnstat_view", "vnstat_daily") + self.refresh_interval = g15gconf.get_float_or_default(self.gconf_client, self.gconf_key + "/refresh_interval", 10.0) + + ''' + *********************************************************** + * Functions specific to plugin * + *********************************************************** + ''' + + def _reload_theme(self): + variant = None + self.theme = g15theme.G15Theme(os.path.join(os.path.dirname(__file__), "default"), variant) + + ''' + Get the properties dictionary + ''' + + def get_theme_properties(self): + properties = { } + + def convert_bytes(bytes): + bytes = float(bytes) + if bytes >= 1099511627776: + terabytes = bytes / 1099511627776 + size = '%.2fT' % terabytes + elif bytes >= 1073741824: + gigabytes = bytes / 1073741824 + size = '%.2fG' % gigabytes + elif bytes >= 1048576: + megabytes = bytes / 1048576 + size = '%.2fM' % megabytes + elif bytes >= 1024: + kilobytes = bytes / 1024 + size = '%.2fK' % kilobytes + else: + size = '%.2fb' % bytes + return size + + # Split vnstat data into array + def get_traffic_data(dataType, dataValue, vn): + line='' + for item in vn.split("\n"): + if "%s;%d;" % (dataType, dataValue) in item: + line = item.strip().split(';') + break + return line + + # convert MiB and KiB into KB + def cb(mib, kib): + return (int(mib) * 1000000) + (int(kib) * 1000) + + ''' + Get the details to display and place them as properties which are passed to + the theme + ''' + + if self.use_vnstat is False: + bootup = datetime.datetime.fromtimestamp(int(gtop.uptime().boot_time)).strftime('%d.%m.%y %H:%M') + sd = gtop.netload(self.networkdevice) + properties["sdn"] = "DL: " +convert_bytes(sd.bytes_in) + properties["sup"] = "UL: " +convert_bytes(sd.bytes_out) + properties["des1"] = "Traffic since: " +bootup + properties["title"] = self.networkdevice + " Traffic" + + else: + vnstat, vn = g15os.get_command_output('vnstat -i ' + self.networkdevice + ' --dumpdb') + if vnstat != 0: + properties["message"] = "vnstat is not installed!" + else: + chErr = str(vn.find("Error")); + if chErr != "-1": + properties["message"] = "No stats for device " + self.networkdevice + else: + properties["title"] = self.networkdevice +" Traffic (U/D)" + + def get_data(kind, period): + # get vnstat data as array, array content: 2 = unixtime, 4 = up MiB, 6 = up KiB, 3 = dn MiB, 5 = dn KiB + line = get_traffic_data(kind, period, vn) + if line[7] == '1': + up = convert_bytes(cb(line[4], line[6])) + dn = convert_bytes(cb(line[3], line[5])) + des = int(line[2]) + return [up, dn, des] + else: + return None + + if self.loadpage == 'vnstat_daily': + k = "d" + fmt = '%A' + elif self.loadpage == 'vnstat_monthly': + k = "m" + fmt = '%B' + + for p in range(0,3): + data = get_data(k,p) + if data is not None: + properties["d"] = "/" + properties["dup" + str(p + 1)] = data[0] + properties["ddn" + str(p + 1)] = data[1] + properties["des" + str(p + 1)] = datetime.datetime.fromtimestamp(data[2]).strftime(fmt) + + return properties diff --git a/src/plugins/trafficstats/trafficstats.ui b/src/plugins/trafficstats/trafficstats.ui new file mode 100644 index 0000000..79003fd --- /dev/null +++ b/src/plugins/trafficstats/trafficstats.ui @@ -0,0 +1,191 @@ + + + + + + + + + + + + False + 5 + Traffic Stats Config + dialog + + + True + False + 2 + + + True + False + end + + + gtk-close + True + True + True + False + True + + + False + False + 0 + + + + + False + True + end + 0 + + + + + True + False + 4 + + + Use vnstat? + True + True + False + False + True + + + False + True + 0 + + + + + True + False + The vnstat software is not available on this computer. + + + + + + False + True + 1 + + + + + True + False + True + + + 120 + True + False + 2 + Network Device + + + False + True + 0 + + + + + 190 + True + False + NetDevice + + + + 0 + + + + + False + True + 1 + + + + + False + True + 2 + + + + + True + False + True + + + 120 + True + False + Refresh Interval + + + False + True + 0 + + + + + 190 + True + True + adjustment1 + 1 + + + False + True + 1 + + + + + False + True + 3 + + + + + True + True + 1 + + + + + + + + + button1 + + + + 1 + 300 + 10 + 1 + 10 + + diff --git a/src/plugins/tweak/Makefile.am b/src/plugins/tweak/Makefile.am new file mode 100644 index 0000000..fb4f685 --- /dev/null +++ b/src/plugins/tweak/Makefile.am @@ -0,0 +1,6 @@ +plugindir = $(datadir)/gnome15/plugins/tweak +plugin_DATA = tweak.py \ + tweak.ui + +EXTRA_DIST = \ + $(plugin_DATA) \ No newline at end of file diff --git a/src/plugins/tweak/i18n/tweak.en_GB.po b/src/plugins/tweak/i18n/tweak.en_GB.po new file mode 100644 index 0000000..dbec32d --- /dev/null +++ b/src/plugins/tweak/i18n/tweak.en_GB.po @@ -0,0 +1,110 @@ +# English translations for gnome15-plugins package. +# Copyright (C) 2012 THE gnome15-plugins'S COPYRIGHT HOLDER +# This file is distributed under the same license as the gnome15-plugins package. +# Automatically generated, 2012. +# +msgid "" +msgstr "" +"Project-Id-Version: gnome15-plugins 0.8.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: 2012-01-08 11:23+0000\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: en_GB\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=ASCII\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: i18n/tweak.glade.h:1 +msgid "Animation Delay" +msgstr "Animation Delay" + +#: i18n/tweak.glade.h:2 +msgid "Disable SVG Glow" +msgstr "Disable SVG Glow" + +#: i18n/tweak.glade.h:3 +msgid "Fade keyboard backlight on close" +msgstr "Fade keyboard backlight on close" + +#: i18n/tweak.glade.h:4 +msgid "Fade screen on close" +msgstr "Fade screen on close" + +#: i18n/tweak.glade.h:5 +msgid "Gnome15 Hidden Preferences" +msgstr "Gnome15 Hidden Preferences" + +#: i18n/tweak.glade.h:6 +msgid "Key Hold Duration" +msgstr "Key Hold Duration" + +#: i18n/tweak.glade.h:7 +msgid "Power off all lights on close" +msgstr "Power off all lights on close" + +#: i18n/tweak.glade.h:8 +msgid "Scroll Amount" +msgstr "Scroll Amount" + +#: i18n/tweak.glade.h:9 +msgid "Scroll Delay" +msgstr "Scroll Delay" + +#: i18n/tweak.glade.h:10 +msgid "Start each screen in it's own thread" +msgstr "Start each screen in it's own thread" + +#: i18n/tweak.glade.h:11 +msgid "" +"The SVG glow effect currently\n" +"uses lots of CPU. Select this option\n" +"to remove the effect." +msgstr "" +"The SVG glow effect currently\n" +"uses lots of CPU. Select this option\n" +"to remove the effect." + +#: i18n/tweak.glade.h:14 +msgid "Use XTEST for macros and macro recording" +msgstr "Use XTEST for macros and macro recording" + +#: i18n/tweak.glade.h:15 +msgid "" +"When enabled, the XTEST \n" +"extensions will be used for\n" +"recording macros and sending\n" +"keystrokes. When disable, raw\n" +"X11 events will be used." +msgstr "" +"When enabled, the XTEST \n" +"extensions will be used for\n" +"recording macros and sending\n" +"keystrokes. When disable, raw\n" +"X11 events will be used." + +#: i18n/tweak.glade.h:20 +msgid "center" +msgstr "center" + +#: i18n/tweak.glade.h:21 +msgid "ms" +msgstr "ms" + +#: i18n/tweak.glade.h:22 +msgid "scale" +msgstr "scale" + +#: i18n/tweak.glade.h:23 +msgid "stretch" +msgstr "stretch" + +#: i18n/tweak.glade.h:24 +msgid "tile" +msgstr "tile" + +#: i18n/tweak.glade.h:25 +msgid "zoom" +msgstr "zoom" diff --git a/src/plugins/tweak/i18n/tweak.glade.h b/src/plugins/tweak/i18n/tweak.glade.h new file mode 100644 index 0000000..5aaf196 --- /dev/null +++ b/src/plugins/tweak/i18n/tweak.glade.h @@ -0,0 +1,25 @@ +char *s = N_("Animation Delay"); +char *s = N_("Disable SVG Glow"); +char *s = N_("Fade keyboard backlight on close"); +char *s = N_("Fade screen on close"); +char *s = N_("Gnome15 Hidden Preferences"); +char *s = N_("Key Hold Duration"); +char *s = N_("Power off all lights on close"); +char *s = N_("Scroll Amount"); +char *s = N_("Scroll Delay"); +char *s = N_("Start each screen in it's own thread"); +char *s = N_("The SVG glow effect currently\n" + "uses lots of CPU. Select this option\n" + "to remove the effect."); +char *s = N_("Use XTEST for macros and macro recording"); +char *s = N_("When enabled, the XTEST \n" + "extensions will be used for\n" + "recording macros and sending\n" + "keystrokes. When disable, raw\n" + "X11 events will be used."); +char *s = N_("center"); +char *s = N_("ms"); +char *s = N_("scale"); +char *s = N_("stretch"); +char *s = N_("tile"); +char *s = N_("zoom"); diff --git a/src/plugins/tweak/i18n/tweak.pot b/src/plugins/tweak/i18n/tweak.pot new file mode 100644 index 0000000..1b109fb --- /dev/null +++ b/src/plugins/tweak/i18n/tweak.pot @@ -0,0 +1,102 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: i18n/tweak.glade.h:1 +msgid "Animation Delay" +msgstr "" + +#: i18n/tweak.glade.h:2 +msgid "Disable SVG Glow" +msgstr "" + +#: i18n/tweak.glade.h:3 +msgid "Fade keyboard backlight on close" +msgstr "" + +#: i18n/tweak.glade.h:4 +msgid "Fade screen on close" +msgstr "" + +#: i18n/tweak.glade.h:5 +msgid "Gnome15 Hidden Preferences" +msgstr "" + +#: i18n/tweak.glade.h:6 +msgid "Key Hold Duration" +msgstr "" + +#: i18n/tweak.glade.h:7 +msgid "Power off all lights on close" +msgstr "" + +#: i18n/tweak.glade.h:8 +msgid "Scroll Amount" +msgstr "" + +#: i18n/tweak.glade.h:9 +msgid "Scroll Delay" +msgstr "" + +#: i18n/tweak.glade.h:10 +msgid "Start each screen in it's own thread" +msgstr "" + +#: i18n/tweak.glade.h:11 +msgid "" +"The SVG glow effect currently\n" +"uses lots of CPU. Select this option\n" +"to remove the effect." +msgstr "" + +#: i18n/tweak.glade.h:14 +msgid "Use XTEST for macros and macro recording" +msgstr "" + +#: i18n/tweak.glade.h:15 +msgid "" +"When enabled, the XTEST \n" +"extensions will be used for\n" +"recording macros and sending\n" +"keystrokes. When disable, raw\n" +"X11 events will be used." +msgstr "" + +#: i18n/tweak.glade.h:20 +msgid "center" +msgstr "" + +#: i18n/tweak.glade.h:21 +msgid "ms" +msgstr "" + +#: i18n/tweak.glade.h:22 +msgid "scale" +msgstr "" + +#: i18n/tweak.glade.h:23 +msgid "stretch" +msgstr "" + +#: i18n/tweak.glade.h:24 +msgid "tile" +msgstr "" + +#: i18n/tweak.glade.h:25 +msgid "zoom" +msgstr "" diff --git a/src/plugins/tweak/tweak.py b/src/plugins/tweak/tweak.py new file mode 100644 index 0000000..4b3650a --- /dev/null +++ b/src/plugins/tweak/tweak.py @@ -0,0 +1,62 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2011 Brett Smith +# +# 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 . + +import gnome15.g15locale as g15locale +_ = g15locale.get_translation("tweak", modfile = __file__).ugettext + +import gnome15.util.g15uigconf as g15uigconf +import gtk +import os.path + +# Plugin details - All of these must be provided +id="tweak" +name=_("Tweak Gnome15") +description=_("Allows configuration of some hidden settings. These are mostly \ +performance tweaks. If Gnome15 is using too much CPU, \ +you will find adjusting some of these may reduce it. ") +author="Brett Smith " +copyright=_("Copyright (C)2010 Brett Smith") +site="http://www.russo79.com/gnome15" +has_preferences=True +passive=True +global_plugin=True + +def show_preferences(parent, driver, gconf_client, gconf_key): + widget_tree = gtk.Builder() + widget_tree.add_from_file(os.path.join(os.path.dirname(__file__), "tweak.ui")) + dialog = widget_tree.get_object("TweakDialog") + dialog.set_transient_for(parent) + g15uigconf.configure_adjustment_from_gconf(gconf_client, "/apps/gnome15/scroll_delay", "ScrollDelayAdjustment", 500, widget_tree) + g15uigconf.configure_adjustment_from_gconf(gconf_client, "/apps/gnome15/scroll_amount", "ScrollAmountAdjustment", 5, widget_tree) + g15uigconf.configure_adjustment_from_gconf(gconf_client, "/apps/gnome15/animation_delay", "AnimationDelayAdjustment", 100, widget_tree) + g15uigconf.configure_checkbox_from_gconf(gconf_client, "/apps/gnome15/animated_menus", "AnimatedMenus", True, widget_tree) + g15uigconf.configure_adjustment_from_gconf(gconf_client, "/apps/gnome15/key_hold_duration", "KeyHoldDurationAdjustment", 2000, widget_tree) + g15uigconf.configure_adjustment_from_gconf(gconf_client, "/apps/gnome15/usb_key_read_timeout", "UsbKeyReadTimeoutAdjustment", 100, widget_tree) + g15uigconf.configure_checkbox_from_gconf(gconf_client, "/apps/gnome15/use_xtest", "UseXTest", True, widget_tree) + g15uigconf.configure_checkbox_from_gconf(gconf_client, "/apps/gnome15/disable_svg_glow", "DisableSVGGlow", False, widget_tree) + g15uigconf.configure_checkbox_from_gconf(gconf_client, "/apps/gnome15/fade_screen_on_close", "FadeScreenOnClose", True, widget_tree) + g15uigconf.configure_checkbox_from_gconf(gconf_client, "/apps/gnome15/fade_keyboard_backlight_on_close", "FadeKeyboardBacklightOnClose", True, widget_tree) + g15uigconf.configure_checkbox_from_gconf(gconf_client, "/apps/gnome15/all_off_on_disconnect", "AllOffOnDisconnect", True, widget_tree) + g15uigconf.configure_checkbox_from_gconf(gconf_client, "/apps/gnome15/start_in_threads", "StartScreensInThreads", False, widget_tree) + g15uigconf.configure_checkbox_from_gconf(gconf_client, "/apps/gnome15/monitor_desktop_session", "MonitorDesktopSession", True, widget_tree) + g15uigconf.configure_text_from_gconf(gconf_client, "/apps/gnome15/time_format", "TimeFormat", "", widget_tree) + g15uigconf.configure_text_from_gconf(gconf_client, "/apps/gnome15/time_format_24hr", "TimeFormatTwentyFour", "", widget_tree) + g15uigconf.configure_text_from_gconf(gconf_client, "/apps/gnome15/date_format", "DateFormat", "", widget_tree) + g15uigconf.configure_text_from_gconf(gconf_client, "/apps/gnome15/date_time_format", "DateTimeFormat", "", widget_tree) + + dialog.run() + dialog.hide() + diff --git a/src/plugins/tweak/tweak.ui b/src/plugins/tweak/tweak.ui new file mode 100644 index 0000000..d3b52d8 --- /dev/null +++ b/src/plugins/tweak/tweak.ui @@ -0,0 +1,596 @@ + + + + + + 100000 + 1 + 10 + + + + 10000 + 1 + 10 + + + 100 + 1 + 10 + + + 10000 + 1 + 10 + + + + + + + + + zoom + + + tile + + + center + + + scale + + + stretch + + + + + 10000 + 1 + 10 + + + False + 5 + Gnome15 Hidden Preferences + False + True + center-on-parent + dialog + + + True + False + 2 + + + True + False + 8 + + + True + False + 4 + + + Animated Menus + True + True + False + True + + + True + True + 0 + + + + + Monitor desktop session + True + True + False + True + + + True + True + 1 + + + + + Use XTEST for macros and macro recording + True + True + False + True + + + True + True + 2 + + + + + Fade keyboard backlight on close + True + True + False + True + + + True + True + 3 + + + + + Fade screen on close + True + True + False + True + + + True + True + 4 + + + + + Disable SVG Glow + True + True + False + True + + + True + True + 5 + + + + + Start each screen in it's own thread + True + True + False + True + + + True + True + 6 + + + + + Power off all lights on close + True + True + False + True + + + True + True + 7 + + + + + True + False + 5 + 3 + 4 + 4 + + + + + + True + False + 0 + Scroll Delay + + + 1 + 2 + + + + + True + False + 0 + Scroll Amount + + + 2 + 3 + + + + + True + False + 0 + Animation Delay + + + 3 + 4 + + + + + True + False + 0 + Key Hold Duration + + + 4 + 5 + + + + + True + True + + True + False + False + True + True + ScrollDelayAdjustment + + + 1 + 2 + 1 + 2 + + + + + True + True + + True + False + False + True + True + ScrollAmountAdjustment + + + 1 + 2 + 2 + 3 + + + + + True + True + + True + False + False + True + True + AnimationDelayAdjustment + + + 1 + 2 + 3 + 4 + + + + + True + True + + True + False + False + True + True + KeyHoldDurationAdjustment + + + 1 + 2 + 4 + 5 + + + + + True + False + ms + + + 2 + 3 + 1 + 2 + + + + + True + False + ms + + + 2 + 3 + 3 + 4 + + + + + True + False + ms + + + 2 + 3 + 4 + 5 + + + + + True + False + 0 + USB Key Read Timeout + + + + + True + True + + True + False + False + True + True + UsbKeyReadTimeoutAdjustment + + + 1 + 2 + + + + + True + False + ms + + + 2 + 3 + + + + + True + True + 4 + 8 + + + + + False + False + 0 + + + + + True + False + + + True + False + 4 + 2 + 4 + 4 + + + True + False + 0 + Time Format + + + + + True + False + 0 + Time Format (24 hour) + + + 1 + 2 + + + + + True + False + 0 + Date Format + + + 2 + 3 + + + + + True + False + 0 + Datetime Format + + + 3 + 4 + + + + + True + True + + False + False + True + True + + + 1 + 2 + + + + + True + True + + False + False + True + True + + + 1 + 2 + 1 + 2 + + + + + True + True + + False + False + True + True + + + 1 + 2 + 2 + 3 + + + + + True + True + + False + False + True + True + + + 1 + 2 + 3 + 4 + + + + + False + False + 0 + + + + + True + True + 1 + + + + + True + True + 0 + + + + + True + False + end + + + gtk-close + True + True + True + True + + + False + False + 0 + + + + + False + True + end + 1 + + + + + + + + + + + + button9 + + + diff --git a/src/plugins/voip-mumble/Makefile.am b/src/plugins/voip-mumble/Makefile.am new file mode 100644 index 0000000..03dc05a --- /dev/null +++ b/src/plugins/voip-mumble/Makefile.am @@ -0,0 +1,6 @@ +plugindir = $(datadir)/gnome15/plugins/voip-mumble +plugin_DATA = voip-mumble.py \ + logo.png + +EXTRA_DIST = \ + $(plugin_DATA) \ No newline at end of file diff --git a/src/plugins/voip-mumble/logo.png b/src/plugins/voip-mumble/logo.png new file mode 100644 index 0000000..fc292b0 Binary files /dev/null and b/src/plugins/voip-mumble/logo.png differ diff --git a/src/plugins/voip-mumble/voip-mumble.py b/src/plugins/voip-mumble/voip-mumble.py new file mode 100644 index 0000000..641a9ed --- /dev/null +++ b/src/plugins/voip-mumble/voip-mumble.py @@ -0,0 +1,115 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2009-2012 Brett Smith +# Copyright (C) 2013 Gnome15 authors +# +# 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 . + +import gnome15.g15locale as g15locale +_ = g15locale.get_translation("voip-mumble", modfile = __file__).ugettext + +import gnome15.g15driver as g15driver +import gnome15.util.g15convert as g15convert +import ts3 +from threading import Thread +from threading import Lock +from threading import RLock +from threading import Semaphore +import voip +import os +import base64 +import socket +import errno + +# Plugin details +id="voip-mumble" +name=_("Mumble") +description=_("Provides integration with Mumble. Note, this plugin also\n\ +requires the 'Voip' plugin as well which provides the user interface.") +author="Brett Smith " +copyright=_("Copyright (C)2011 Brett Smith") +site="http://www.russo79.com/gnome15" +has_preferences=False +unsupported_models = [ g15driver.MODEL_G110, g15driver.MODEL_G11, g15driver.MODEL_G930, g15driver.MODEL_G35 ] + +# This plugin only supplies classes to the 'voip' plugin and so is never activated +passive=True +global_plugin=True + +# Logging +import logging +logger = logging.getLogger(__name__) + +""" +Calendar Back-end module functions +""" + +def create_backend(): + return MumbleBackend() + +""" +Mumble backend +""" + +class MumbleBackend(voip.VoipBackend): + + def __init__(self): + voip.VoipBackend.__init__(self) + + def get_name(self): + raise _("Mumble") + + def start(self, plugin): + raise Exception("Not implemented") + + def stop(self): + raise Exception("Not implemented") + + def get_current_channel(self): + """ + Get the current channel + """ + raise Exception("Not implemented") + + def get_talking(self): + """ + Get who is talking + """ + raise Exception("Not implemented") + + def get_me(self): + """ + Get the local user's buddy entry + """ + raise Exception("Not implemented") + + def get_channels(self): + raise Exception("Not implemented") + + def get_buddies(self, current_channel=True): + raise Exception("Not implemented") + + def get_icon(self): + raise Exception("Not implemented") + + def set_audio_input(self, mute): + raise Exception("Not implemented") + + def set_audio_output(self, mute): + raise Exception("Not implemented") + + def away(self): + raise Exception("Not implemented") + + def online(self): + raise Exception("Not implemented") diff --git a/src/plugins/voip-teamspeak3/Makefile.am b/src/plugins/voip-teamspeak3/Makefile.am new file mode 100644 index 0000000..107218c --- /dev/null +++ b/src/plugins/voip-teamspeak3/Makefile.am @@ -0,0 +1,8 @@ +SUBDIRS = ts3 + +plugindir = $(datadir)/gnome15/plugins/voip-teamspeak3 +plugin_DATA = voip-teamspeak3.py \ + logo.png + +EXTRA_DIST = \ + $(plugin_DATA) \ No newline at end of file diff --git a/src/plugins/voip-teamspeak3/logo.png b/src/plugins/voip-teamspeak3/logo.png new file mode 100644 index 0000000..fc292b0 Binary files /dev/null and b/src/plugins/voip-teamspeak3/logo.png differ diff --git a/src/plugins/voip-teamspeak3/test.py b/src/plugins/voip-teamspeak3/test.py new file mode 100644 index 0000000..a990e9c --- /dev/null +++ b/src/plugins/voip-teamspeak3/test.py @@ -0,0 +1,34 @@ +#!/usr/bin/env python + +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2012 Brett Smith +# +# 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 . + +import ts3 +import gnome15.g15logging as g15logging +import logging + +if __name__ == "__main__": + logger = g15logging.get_root_logger() + logger.setLevel(logging.INFO) + + t = ts3.TS3() + t.start() + + logger.info("schandlerid : %d", t.schandlerid) + + + logger.info("channel: %s", t.send_command(ts3.Command('channelconnectinfo')).args['path']) + \ No newline at end of file diff --git a/src/plugins/voip-teamspeak3/ts3/Makefile.am b/src/plugins/voip-teamspeak3/ts3/Makefile.am new file mode 100644 index 0000000..1553717 --- /dev/null +++ b/src/plugins/voip-teamspeak3/ts3/Makefile.am @@ -0,0 +1,8 @@ +ts3dir = $(datadir)/gnome15/plugins/voip-teamspeak3/ts3 +ts3_DATA = \ + __init__.py \ + message.py + +EXTRA_DIST = \ + $(ts3_DATA) + diff --git a/src/plugins/voip-teamspeak3/ts3/__init__.py b/src/plugins/voip-teamspeak3/ts3/__init__.py new file mode 100644 index 0000000..5ecd020 --- /dev/null +++ b/src/plugins/voip-teamspeak3/ts3/__init__.py @@ -0,0 +1,206 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2012 Brett Smith +# +# 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 . + +from telnetlib import Telnet +from threading import Thread +from threading import RLock +from message import MessageFactory +from message import Command + +# Logging +import logging +logger = logging.getLogger(__name__) + +def _receive_message(client): + while True: + incoming_message = client.read_until('\n', 10).strip() + if incoming_message is not None and incoming_message.strip(): + logger.info("Received: %s", incoming_message) + message = MessageFactory.get_message(incoming_message) + if message: + return message + +class TS3CommandException(Exception): + + def __init__(self, code, message): + Exception.__init__(self, message) + self.code = code + +class TS3(): + + class ReceiveThread(Thread): + def __init__(self, client): + Thread.__init__(self) + self._client = client + self.setDaemon(True) + self.setName("TS3ReceiveThread") + self._reply_handler = None + self._error_handler = None + self._stop = False + + def stop(self): + self._stop = True + def run(self): + try: + while True: + try: + if self._stop: + raise EOFError() + msg = _receive_message(self._client) + except TS3CommandException as e: + logger.debug("Error while receving message", exc_info = e) + self._error_handler(e) + else: + self._reply_handler(msg) + except Exception as e: + logger.debug("Error in main loop", exc_info = e) + self._error_handler(e) + + def __init__(self, hostname="127.0.0.1", port=25639, timeout=10): + self.timeout = timeout + self.hostname = hostname + self.port = port + + self._event_client = None + self._event_thread = None + self._command_client = None + self._lock = RLock() + + self.schandlerid = None + + def change_server(self, schandlerid): + if self._event_client is not None: + self._write_command(self._event_client, Command( + 'clientnotifyunregister') + ) + + self.schandlerid = schandlerid + self._send_command(self._command_client, Command( + 'use', + schandlerid=self.schandlerid) + ) + if self._event_client is not None: + self._send_command(self._event_client, Command( + 'use', + schandlerid=self.schandlerid) + ) + self._send_command(self._event_client, Command( + 'clientnotifyregister', + schandlerid=self.schandlerid, + event=self._event_type + ) + ) + + def close(self): + if self._event_thread is not None: + self._event_thread.stop() + self._command_client.close() + self._command_client = None + if self._event_client is not None: + self._event_client.close() + self._event_client = None + + def start(self): + self._create_command_client() + + + def send_event_command(self, command): + try: + self._lock.acquire() + if self._event_client is not None: + self._write_command(self._event_client, command) + finally: + self._lock.release() + + def send_command(self, command): + try: + self._lock.acquire() + if self._command_client is None: + self.start() + return self._send_command(self._command_client, command) + finally: + self._lock.release() + + def subscribe(self, reply_handler, type='any', error_handler = None): + """ + Shortcut method to subscribe to all messages received from the client. + + Keyword arguments: + reply_handler -- function called with Message as argument + error_handler -- function called with TSCommandException as argument + type -- type of event to subscribe to + """ + try: + self._lock.acquire() + + if self._event_client is not None: + raise Exception("Already subscribed") + + self._event_type = type + self._create_event_client() + self._event_thread._reply_handler = reply_handler + self._event_thread._error_handler = error_handler + self._write_command(self._event_client, Command( + 'clientnotifyregister', + schandlerid=self.schandlerid, + event=type + ) + ) + + finally: + self._lock.release() + + + """ + Private + """ + def _send_command(self, client, command): + try: + self._lock.acquire() + self._write_command(client, command) + r_reply = None + while True: + reply = _receive_message(client) + if reply.command == 'error': + msg = reply.args['msg'] + if msg != 'ok': + raise TS3CommandException(int(reply.args['id']), msg) + else: + break + else: + if r_reply is None: + r_reply = reply + else: + raise TS3CommandException(9999, "Multiple replies") + + + return r_reply + finally: + self._lock.release() + + def _write_command(self, client, command): + logger.info("Sending command: %s", command.output) + client.write("%s\n" % command.output) + + def _create_command_client(self): + self._command_client = Telnet(host=self.hostname, port=self.port) + self.schandlerid = int(_receive_message(self._command_client).args['schandlerid']) + + def _create_event_client(self): + self._event_client = Telnet(host=self.hostname, port=self.port) + _receive_message(self._event_client) + self._event_thread = self.ReceiveThread(self._event_client) + self._event_thread.start() \ No newline at end of file diff --git a/src/plugins/voip-teamspeak3/ts3/message.py b/src/plugins/voip-teamspeak3/ts3/message.py new file mode 100644 index 0000000..842996b --- /dev/null +++ b/src/plugins/voip-teamspeak3/ts3/message.py @@ -0,0 +1,226 @@ +# Copyright (c) 2012 Adam Coddington +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +__all__ = ['Message', 'Command'] + +class MessageFactory(object): + @classmethod + def get_message(cls, incoming_string): + if incoming_string[0] != incoming_string[0].upper(): + first_item = incoming_string.split(' ')[0] + if "=" in first_item and "|" in incoming_string: + return MultipartMessage(incoming_string) + else: + return Message(incoming_string) + +class MessageBase(object): + MAPPINGS = { + '\\\\': '\\', + '\\/': '/', + '\\s': ' ', + '\\p': '|', + '\\a': '', + '\\b': '', + '\\f': '\n', + '\\n': '\n', + '\\r': '\n', + '\\t': '\t', + '\\v': '', + } + + def __eq__(self, other): + if self.__repr__() == other.__repr__(): + return True + return False + + def __unicode__(self): + return unicode(self.__str__()) + + def __repr__(self): + return "<%s>" % self.__str__() + + def _clean_incoming_value_multipart(self, value): + raw_values = value.split('|') + + items = [] + items.append( + raw_values[0] + ) + for raw_item in raw_values[1:]: + items.append( + raw_item.split('=')[1] + ) + return tuple(items) + + def _clean_incoming_value(self, value): + for fr, to in self.MAPPINGS.items(): + value = value.replace(fr, to) + return value + + def _clean_outgoing_value(self, value): + value = str(value) + for fr, to in self.MAPPINGS.items(): + if to: + value = value.replace(to, fr) + return value + + @property + def ultimate_origination(self): + if self.is_response(): + return self.origination.command + else: + return self.command + +class Message(MessageBase): + def __init__(self, command): + command = command.strip() + if not command: + raise ValueError("No command") + + self.raw_command = command + + self.command = self._get_command_from_string(self.raw_command) + self.args = self._get_arguments_from_string(self.raw_command) + + def is_reset_message(self): + if self.command == 'error': + return True + return False + + def is_response(self): + if self.command: + return False + return True + + def is_response_to(self, command): + if self.is_response() and self.origination == command: + return True + return False + + def set_origination(self, command): + self.origination = command + + def _get_command_from_string(self, cmd): + command = cmd.split(' ')[0] + if command.find('=') > -1: + command = None + return command + + def _get_arguments_from_string(self, cmd): + args = {} + raw_args = cmd.split(' ')[1 if self.command else 0:] + for raw_arg in raw_args: + arg = raw_arg.split('=', 1) + attribute = arg[0] + if len(arg) > 1: + value = arg[1] + else: + value = None + if value: + if "|" in value: + args[attribute] = self._clean_incoming_value_multipart(value) + else: + args[attribute] = self._clean_incoming_value(value) + else: + args[attribute] = None + return args + + def __getitem__(self, key): + return self.args[key] + + def keys(self): + return self.args.keys() + + @property + def output(self): + arglist = [] + for param, value in self.args.items(): + arglist.append("%s=%s" % ( + param, + self._clean_outgoing_value(value), + )) + if self.is_response(): + return "%s %s" % ( + self.origination.__repr__(), + " ".join(arglist), + ) + else: + return "%s %s" % ( + self.command, + " ".join(arglist), + ) + + def __str__(self): + if self.is_response(): + return "%s %s" % ( + self.origination.__repr__(), + self.args + ) + else: + return "%s %s" % ( + self.command, + self.args + ) + +class MultipartMessage(MessageBase): + def __init__(self, command_string): + self.command = command_string + self.origination = None + + self.responses = self.parse_command( + self.command + ) + + def parse_command(self, string): + responses = [] + for string_part in string.split('|'): + responses.append( + Message( + string_part + ) + ) + return responses + + def set_origination(self, command): + self.origination = command + for response in self.responses: + response.set_origination(command) + + def __getitem__(self, key): + return self.responses[key] + + def __str__(self): + string_list = [] + for response in self.responses: + string_list.append(repr(response)) + return "[%s]" % ( + ", ".join(string_list) + ) + + def is_response(self): + return True + + def is_reset_message(self): + return False + +class Command(Message): + def __init__(self, command, **kwargs): + self.command = command + self.args = kwargs diff --git a/src/plugins/voip-teamspeak3/voip-teamspeak3.py b/src/plugins/voip-teamspeak3/voip-teamspeak3.py new file mode 100644 index 0000000..8fba11b --- /dev/null +++ b/src/plugins/voip-teamspeak3/voip-teamspeak3.py @@ -0,0 +1,673 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2012 Brett Smith +# Copyright (C) 2013 Brett Smith +# Nuno Araujo +# +# 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 . + +import gnome15.g15locale as g15locale +_ = g15locale.get_translation("voip-teamspeak3", modfile = __file__).ugettext + +import gnome15.g15driver as g15driver +import gnome15.util.g15icontools as g15icontools +import ts3 +from threading import Thread +from threading import Lock +from threading import RLock +from threading import Semaphore +import voip +import os +import base64 +import socket +import errno +import re + +# Plugin details +id="voip-teamspeak3" +name=_("Teamspeak3") +description=_("Provides integration with TeamSpeak3. Note, this plugin also\n\ +requires the 'Voip' plugin as well which provides the user interface.") +author="Brett Smith " +copyright=_("Copyright (C)2011 Brett Smith") +site="http://www.russo79.com/gnome15" +has_preferences=False +unsupported_models = [ g15driver.MODEL_G110, g15driver.MODEL_G11, g15driver.MODEL_G930, g15driver.MODEL_G35 ] + +# This plugin only supplies classes to the 'voip' plugin and so is never activated +passive=True +global_plugin=True + +# Logging +import logging +logger = logging.getLogger(__name__) + +""" +Calendar Back-end module functions +""" + +def create_backend(): + return Teamspeak3Backend() + +def find_avatar(server_unique_identifier, client_unique_identifier): + decoded = "" + try: + for c in base64.b64decode(client_unique_identifier): + decoded += chr(((ord(c) & 0xf0) >> 4) + 97) + decoded += chr((ord(c) & 0x0f) + 97) + except TypeError as e: + logger.debug("Error decoding client_unique_identifier. Using raw value", exc_info = e) + # Sometimes the client_unique_identifier is not base64 encoded + decoded = client_unique_identifier + + return os.path.expanduser("~/.ts3client/cache/%s/clients/avatar_%s" % (base64.b64encode(server_unique_identifier), decoded)) + +""" +Teamspeak3 backend +""" + +class Teamspeak3BuddyMenuItem(voip.BuddyMenuItem): + + def __init__(self, db_id, clid, nickname, channel, client_type, plugin): + voip.BuddyMenuItem.__init__(self, "client-%s" % clid, nickname, channel, plugin) + self.db_id = db_id + self.clid = clid + self.client_type = client_type + self.avatar = None + self.uid = None + + def set_uid(self, server_uid, uid): + self.uid = uid + self.avatar = find_avatar(server_uid, uid) + +class Teamspeak3ServerMenuItem(voip.ChannelMenuItem): + + def __init__(self, schandlerid, name, backend): + voip.ChannelMenuItem.__init__(self, "server-%s" % schandlerid, name, backend, icon=g15icontools.get_icon_path(['server', 'redhat-server', 'network-server', 'redhat-network-server', 'gnome-fs-server' ], include_missing=False)) + self.schandlerid = schandlerid + self.activatable = False + self.radio = False + self.path = "" + +class Teamspeak3ChannelMenuItem(voip.ChannelMenuItem): + + def __init__(self, schandlerid, cid, cpid, name, order, backend): + voip.ChannelMenuItem.__init__(self, "channel-%s-%d" % (cid, schandlerid), name, backend) + self.group = False + self.cid = cid + self.cpid = cpid + self.order = order + self._backend = backend + self.schandlerid = schandlerid + + @property + def path(self): + result = self.name + if self.cpid != 0: + parent_item = self.backend._channel_map[self.cpid] + result = parent_item.path + "/" + result + return result + + @property + def parent_count(self): + result = 0 + if self.cpid != 0: + parent_item = self.backend._channel_map[self.cpid] + result = 1 + parent_item.parent_count + return result + + @property + def direct_children(self): + return [ child for child in self._backend._channels if type(child) is Teamspeak3ChannelMenuItem and child.cpid == self.cid ] + + @property + def children(self): + children = [] + for direct_child in self.direct_children: + children.append(direct_child) + children.extend(direct_child.children) + return children + + @property + def child_count(self): + return len(self.children) + + def get_theme_properties(self): + p = voip.ChannelMenuItem.get_theme_properties(self) + p["item_name"] = self.parent_count * " " + p["item_name"] + return p + + def on_activate(self): + if self._backend._client.schandlerid != self.schandlerid: + self._backend._client.change_server(self.schandlerid) + self._backend.set_current_channel(self) + return True + +class Teamspeak3Backend(voip.VoipBackend): + + def __init__(self): + voip.VoipBackend.__init__(self) + self._buddies = None + self._buddy_map = {} + self._channels = None + self._channels_map = {} + self._me = None + self._clid = None + self._server_uid = None + self._client = None + self._current_channel = None + + def get_talking(self): + if self._buddies is not None: + for d in self._buddies: + if d.talking: + return d + + def set_current_channel(self, channel_item): + try: + reply = self._client.send_command(ts3.Command( + 'clientmove', + clid=self._clid, + cid=channel_item.cid + )) + + except ts3.TS3CommandException as e: + logger.debug("Error when changing channel", exc_info = e) + + def get_current_channel(self): + if self._current_channel is None: + if self._channels is None: + self.get_channels() + + reply = self._client.send_command( + ts3.Command( + 'channelconnectinfo' + )) + if 'path' in reply.args: + channel_path = reply.args['path'] + for c in self._channels: + if c.path == channel_path: + self._current_channel = c + + return self._current_channel + + def get_buddies(self): + if self._buddies == None: + self._buddy_map = {} + # Get the basic details + reply = self._client.send_command( + ts3.Command( + 'clientlist -away -voice -uid' + )) + self._parse_clientlist_reply(reply) + + return self._buddies + + def get_channels(self): + if self._channels == None: + self._channel_map = {} + self._channels = [] + + reply = self._client.send_command(ts3.Command( + 'serverconnectionhandlerlist')) + + for r in reply.responses if isinstance(reply, ts3.message.MultipartMessage) else [ reply ]: + s = int(r.args['schandlerid']) + reply = self._client.send_command(ts3.Command( + 'use', schandlerid = s)) + + # Get the server IP and port + try: + reply = self._client.send_command(ts3.Command( + 'serverconnectinfo')) + ip = reply.args['ip'] + port = int(reply.args['port']) + + # A menu item for the server + item = Teamspeak3ServerMenuItem(s, "%s:%d" % (ip, port), self) + self._channels.append(item) + + reply = self._client.send_command( + ts3.Command( + 'channellist' + )) + self._parse_channellist_reply(reply, s) + except ts3.TS3CommandException as e: + logger.debug("Error when getting channel list", exc_info = e) + + + # Switch back to the selected server connection + reply = self._client.send_command(ts3.Command( + 'use', schandlerid = self._client.schandlerid)) + + return self._channels + + def get_name(self): + return _("Teamspeak3") + + def start(self, plugin): + self._plugin = plugin + self._client = ts3.TS3() + + # Connect to ClientQuery plugin + try : + self._client.start() + except socket.error as v: + logger.debug("Error starting client. Could not open socket.", exc_info = v) + self._client = None + if v.error_code == errno.ECONNREFUSED: + return False + raise v + + # Get initial buddy lists, channel lists and other stuff + try: + self._get_clid() + self._get_server_uid() + self.get_channels() + self.get_current_channel() + self.get_buddies() + self._client.subscribe(self._handle_message, "any", self._handle_error) + + return True + except ts3.TS3CommandException as e: + logger.debug("TeamSpeak error getting initial data", exc_info = e) + self._client.close() + self._client = None + if e.code == 1794: + # Not connected to server + return False + else: + raise e + except Exception as e: + logger.debug("Error getting initial data", exc_info = e) + self._client.close() + self._client = None + raise e + + def is_connected(self): + return self._client is not None + + def stop(self): + if self._client is not None: + self._client.close() + self._client = None + + def get_me(self): + return self._me + + def get_icon(self): + return os.path.join(os.path.dirname(__file__), "logo.png") + + def kick(self, buddy): + reply = self._client.send_command(ts3.Command( + 'clientkick', clid = buddy.clid, reasonid = 5, reasonmsg = 'No reason given')) + logger.info("Kicked %s (%s)", buddy.nickname, buddy.clid) + + def ban(self, buddy): + if buddy.uid is None: + raise Exception("UID is not known") + reply = self._client.send_command(ts3.Command( + 'banadd', banreason = 'No reason given', uid = buddy.uid)) + logger.info("Banned %s (%s)", buddy.nickname, buddy.uid) + + def away(self): + reply = self._client.send_command(ts3.Command( + 'clientupdate', + client_away=1 + )) + + def online(self): + reply = self._client.send_command(ts3.Command( + 'clientupdate', + client_away=0 + )) + + def set_audio_input(self, mute): + reply = self._client.send_command(ts3.Command( + 'clientupdate', + client_input_muted=1 if mute else 0 + )) + + def set_audio_output(self, mute): + reply = self._client.send_command(ts3.Command( + 'clientupdate', + client_output_muted=1 if mute else 0 + )) + + """ + Private + """ + def _handle_error(self, error): + print error + if isinstance(error, EOFError): + self._disconnected() + else: + logger.warning("Teamspeak3 error. %s", str(error)) + + def _handle_message(self, message): + print message.command + try: + if message.command == 'notifyclientupdated': + self._parse_notifyclientupdated(message) + self._do_redraw() + elif message.command == 'notifyclientpermlist': + self._parse_notifyclientpermlist_reply(message) + elif message.command == 'notifytextmessage': + self._parse_notifytextmessage_reply(message) + elif message.command == 'notifytalkstatuschange': + self._parse_notifytalkstatuschange_reply(message) + elif message.command == 'notifyclientchannelgroupchanged': + self._parse_notifyclientchannelgroupchanged_reply(message) + elif message.command == 'notifycliententerview': + self._parse_notifycliententerview_reply(message) + elif message.command == 'notifyclientleftview': + self._parse_notifyclientleftview_reply(message) + elif message.command == 'notifyconnectstatuschange': + self._parse_notifyconnectstatuschange_reply(message) + elif message.command == 'notifychannelcreated': + self._parse_notifychannelcreated_reply(message) + elif message.command == 'notifychanneledited': + self._parse_notifychanneledited_reply(message) + elif message.command == 'notifychanneldeleted': + self._parse_notifychanneldeleted_reply(message) + elif message.command == 'notifychannelmoved': + self._parse_notifychannelmoved_reply(message) + elif message.command == 'notifycurrentserverconnectionchanged': + self._parse_notifycurrentserverconnectionchanged_reply(message) + + + + + except Exception as e: + logger.error("Possible corrupt reply.", exc_info = e) + + def _disconnected(self): + print "disconnex" + self._plugin._disconnected() + + def _create_channel_item(self, message, schandlerid): + item = Teamspeak3ChannelMenuItem(schandlerid, int(message.args['cid']), + int(message.args['cpid']) if 'cpid' in message.args else int(message.args['pid']), + message.args['channel_name'], + int(message.args['channel_order']), self) + if 'channel_topic' in message.args: + item.topic = message.args['channel_topic'] + return item + + def _create_menu_item(self, message, channel = None): + return Teamspeak3BuddyMenuItem(int(message.args['client_database_id']), + int(message.args['clid']), + message.args['client_nickname'], + channel, + int(message.args['client_type']), + self._plugin) + + def _get_clid(self): + reply = self._client.send_command(ts3.Command( + 'whoami', virtualserver_unique_identifier=None + )) + self._clid = int(reply.args['clid']) + logger.info("Your CLID is %d", self._clid) + + def _get_server_uid(self): + reply = self._client.send_command(ts3.Command( + 'servervariable', virtualserver_unique_identifier=None + + )) + self._server_uid = reply.args['virtualserver_unique_identifier'] + + def _do_redraw(self): + self._plugin.redraw() + + def _update_item_from_message(self, item, message): + if 'client_input_muted' in message.args: + item.input_muted = message.args['client_input_muted'] == '1' + if 'client_output_muted' in message.args: + item.output_muted = message.args['client_output_muted'] == '1' + if 'client_away' in message.args: + item.away = message.args['client_away'] == '1' + if 'client_away_message' in message.args: + a = message.args['client_away_message'] + if a and len(a) > 0: + item.away = a + if 'client_unique_identifier' in message.args: + item.set_uid(self._server_uid, message.args['client_unique_identifier']) + + def _my_channel_changed(self): + self._current_channel = None + self.get_current_channel() + self._buddies = None + self._plugin.reload_buddies() + + """ + Reply handlers + """ + def _parse_notifycurrentserverconnectionchanged_reply(self, message): + self._client.change_server(int(message.args['schandlerid'])) + self._my_channel_changed() + + def _parse_notifychanneledited_reply(self, message): + item = self._channel_map[int(message.args['cid'])] + if 'channel_topic' in message.args: + item.topic = message.args['channel_topic'] + if 'channel_name' in message.args: + if self._current_channel is not None and item.name == self._current_channel: + self._current_channel = None + item.name = message.args['channel_name'] + if self._current_channel is None: + self.get_current_channel() + + # Update the position of the channel in the channel list if it's order has been changed + if 'channel_order' in message.args: + children = self._remove_channel(item) + item.order = int(message.args['channel_order']) + self._insert_channel(item) + for child in children: + self._insert_channel(child) + + self._plugin.channel_updated(item) + + def _parse_notifyclientpermlist_reply(self, message): + pass + + def _parse_notifychanneldeleted_reply(self, message): + item = self._channel_map[int(message.args['cid'][-1] if type(message.args['cid']) is tuple else message.args['cid'])] + children = self._remove_channel(item) + del self._channel_map[item.cid] + for child in children: + del self._channel_map[child.cid] + self._plugin.channel_removed(item) + + def _parse_notifychannelmoved_reply(self, message): + item = self._channel_map[int(message.args['cid'])] + + children = self._remove_channel(item) + item.order = int(message.args['order']) + item.cpid = int(message.args['cpid']) + self._insert_channel(item) + for child in children: + self._insert_channel(child) + + self._plugin.channel_moved(item) + + def _remove_channel(self, item): + position = self._channels.index(item) + children = item.children + + # Update the following item order if necessary + try: + next_item = self._channels[position + item.child_count + 1] + if next_item.cpid == item.cpid: + next_item.order = item.order + except IndexError as e: + logger.debug("Did not found channel to remove", exc_info = e) + pass + + self._channels.remove(item) + for channel in children: + self._channels.remove(channel) + + return children + + def _find_teamspeak3servermenuitem(self, id): + matching_items = [ x for x in self._channels if x.schandlerid == id and type(x) is Teamspeak3ServerMenuItem ] + if len(matching_items) > 0: + return matching_items[0] + else: + return None + + def _parse_notifychannelcreated_reply(self, message): + item = self._create_channel_item(message, self._client.schandlerid) + self._channel_map[item.cid] = item + self._insert_channel(item) + self._plugin.new_channel(item) + + def _insert_channel(self, item): + # Insert the item at the correct position in the menu + if item.cpid == 0 and item.order == 0: + # If first channel of server + position = self._channels.index(self._find_teamspeak3servermenuitem(item.schandlerid)) + 1 + elif item.order == 0: + # If first sub-channel of a channel + position = self._channels.index(self._channel_map[item.cpid]) + 1 + else: + # Other cases + future_previous_item = self._channel_map[item.order] + position = self._channels.index(future_previous_item) + future_previous_item.child_count + 1 + self._channels.insert(position, item) + + # Update the following item order if necessary + try: + next_item = self._channels[position + 1] + if next_item.cpid == item.cpid: + next_item.order = item.cid + except IndexError as e: + logger.debug("Did not found channel to update", exc_info = e) + pass + + def _parse_notifyconnectstatuschange_reply(self, message): + status = message.args['status'] + if status == "disconnected": + logger.info("Disconnected from server. Stopping client") + self.stop() + + def _parse_notifyclientleftview_reply(self, message): + clid = int(message.args['clid']) + if clid in self._buddy_map: + item = self._buddy_map[clid] + self._buddies.remove(item) + del self._buddy_map[clid] + self._plugin.buddy_left(item) + self._do_redraw() + else: + logger.warning("Client left that we knew nothing about yet (%d)", clid) + + def _parse_notifycliententerview_reply(self, message): + reply= self._client.send_command(ts3.Command( + 'clientvariable', + clid=message.args['clid'] + )) + item = self._create_menu_item(message, None) + item.channel = self._channel_map[int(message.args['ctid'])] + self._buddies.append(item) + self._buddy_map[item.clid] = item + self._update_item_from_message(item, message) + c = self._plugin.new_buddy(item) + + def _parse_notifyclientchannelgroupchanged_reply(self, message): + if int(message.args['clid']) == self._clid: + self._my_channel_changed() + else: + buddy_id = int(message.args['clid']) + buddy = self._buddy_map[buddy_id] + new_channel_id = int(message.args['cid']) + new_channel = self._channel_map[new_channel_id] + old_channel = buddy.channel + buddy.channel = new_channel + self._plugin.moved_channels(buddy, old_channel, new_channel) + + def _parse_clientlist_reply(self, message): + items = [] + item_map = {} + for r in message.responses if isinstance(message, ts3.message.MultipartMessage) else [ message ]: + ch = self._channel_map[int(r.args['cid'])] + item = self._create_menu_item(r, ch) + self._update_item_from_message(item, r) + items.append(item) + item_map[item.clid] = item + if item.clid == self._clid: + self._me = item + self._buddies = items + self._buddy_map = item_map + + def _sort_channellist(self, channels): + """ + Sort the channel list the same way that it's done in TeamSpeak3 + """ + result = [] + search_stack = [] + # Initialize the search stack with the criteria for the first item (always 0,0) + search_stack.append((0, 0)) + while len(channels) > len(result): + search_criteria = search_stack.pop() + try: + item = channels[search_criteria] + result.append(item) + search_stack.append((item.cpid, item.cid)) + search_stack.append((item.cid, 0)) + except KeyError as e: + logger.debug("Did not found channels matching search_criteria", exc_info = e) + continue + + return result + + def _parse_channellist_reply(self, message, schandlerid): + channels = {} + for r in message.responses if isinstance(message, ts3.message.MultipartMessage) else [ message ]: + item = self._create_channel_item(r, schandlerid) + channels[item.cpid, item.order] = item + self._channel_map[item.cid] = item + + self._channels.extend(self._sort_channellist(channels)) + + def _parse_notifyclientupdated(self, message): + item = self._buddy_map[int(message.args['clid'])] + self._update_item_from_message(item, message) + item.mark_dirty() + + def _parse_notifytextmessage_reply(self, message): + if 'invokername' in message.args and 'msg' in message.args: + self._plugin.message_received(message.args['invokername'], self._filter_formatting_tags(message.args['msg'])) + else: + logger.warning("Got text messsage I didn't understand. %s", str(message)) + + def _parse_notifytalkstatuschange_reply(self, message): + clid = int(message.args['clid']) + if clid in self._buddy_map: + item = self._buddy_map[clid] + item.talking = message.args['status'] == '1' + item.mark_dirty() + if not self._plugin.menu.is_focused(): + self._plugin.menu.selected = item + self._plugin.menu.centre_on_selected() + + self._plugin.talking_status_changed(self.get_talking()) + + def _filter_formatting_tags(self, message): + filtered = message + for regex in ('\[B\](?P.*?)\[/B\]', \ + '\[I\](?P.*?)\[/I\]', \ + '\[U\](?P.*?)\[/U\]', \ + '\[COLOR=#([0-9]|[a-f]).*?\](?P.*?)\[/COLOR\]'): + filtered = re.sub(regex, '\g', filtered, flags = re.IGNORECASE) + return filtered diff --git a/src/plugins/voip/Makefile.am b/src/plugins/voip/Makefile.am new file mode 100644 index 0000000..5aaaecb --- /dev/null +++ b/src/plugins/voip/Makefile.am @@ -0,0 +1,18 @@ +SUBDIRS = default + +plugindir = $(datadir)/gnome15/plugins/voip +plugin_DATA = voip.py \ + g19_microphone-sensitivity-high.png \ + g19_microphone-sensitivity-muted.png \ + default_audio-high.gif \ + default_audio-muted.gif \ + default_available.gif \ + default_away.gif \ + default_microphone-sensitivity-high.gif \ + default_microphone-sensitivity-muted.gif \ + default_record.gif \ + voip.ui + + +EXTRA_DIST = \ + $(plugin_DATA) \ No newline at end of file diff --git a/src/plugins/voip/buddies-only/Makefile.am b/src/plugins/voip/buddies-only/Makefile.am new file mode 100644 index 0000000..3775d9d --- /dev/null +++ b/src/plugins/voip/buddies-only/Makefile.am @@ -0,0 +1,31 @@ +themedir = $(datadir)/gnome15/plugins/voip/default +theme_DATA = \ + g19-menu-screen.svg \ + g19-menu-entry.svg \ + g19-message-menu-entry.svg \ + default-menu-screen.svg \ + default-menu-entry.svg \ + default-message-menu-entry.svg + +EXTRA_DIST = \ + $(theme_DATA) + + +all-local: + for M_LOCALE in @ENABLED_LOCALES@; do \ + mkdir -p i18n/$$M_LOCALE/LC_MESSAGES ; \ + if [ `ls i18n/*.po 2>/dev/null|wc -l` -gt 0 ]; then \ + for M_PO in i18n/*.po; do \ + BN=`basename $$M_PO .po`; \ + LL=`basename $$BN .$$M_LOCALE`.mo; \ + echo "$$M_PO -> $$LL"; \ + msgfmt $$M_PO --output-file i18n/$$M_LOCALE/LC_MESSAGES/$$LL; \ + done; \ + fi; \ + done + +install-exec-hook: + for M_LOCALE in @ENABLED_LOCALES@; do \ + mkdir -p $(DESTDIR)$(datadir)/gnome15/plugins/im/default/i18n; \ + cp -pR i18n/$$M_LOCALE $(DESTDIR)$(datadir)/gnome15/plugins/im/default/i18n; \ + done \ No newline at end of file diff --git a/src/plugins/voip/buddies-only/buddies-only.theme b/src/plugins/voip/buddies-only/buddies-only.theme new file mode 100644 index 0000000..e17a8fb --- /dev/null +++ b/src/plugins/voip/buddies-only/buddies-only.theme @@ -0,0 +1,4 @@ +[theme] +name=Buddies Only +description=Displays only buddies +supported_models=g19,g13,g15,g15v2,g510 \ No newline at end of file diff --git a/src/plugins/voip/buddies-only/default-menu-entry.svg b/src/plugins/voip/buddies-only/default-menu-entry.svg new file mode 100644 index 0000000..a17f95c --- /dev/null +++ b/src/plugins/voip/buddies-only/default-menu-entry.svg @@ -0,0 +1,296 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + ${item_name} + ${item_alt} + + + + + + + + ${item_name} + ${item_alt} + + + + + + diff --git a/src/plugins/voip/buddies-only/default-menu-screen.svg b/src/plugins/voip/buddies-only/default-menu-screen.svg new file mode 100644 index 0000000..32686a1 --- /dev/null +++ b/src/plugins/voip/buddies-only/default-menu-screen.svg @@ -0,0 +1,215 @@ + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + ${channel} - ${mode} + + + + + + + + + + + + + + + + + ${emptyMessage} + + diff --git a/src/plugins/voip/buddies-only/g19-menu-entry.svg b/src/plugins/voip/buddies-only/g19-menu-entry.svg new file mode 100644 index 0000000..51d52a1 --- /dev/null +++ b/src/plugins/voip/buddies-only/g19-menu-entry.svg @@ -0,0 +1,411 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + ${item_name} + ${item_alt} + + + + + + + + ${item_name} + ${item_alt} + + + + + + diff --git a/src/plugins/voip/buddies-only/g19-menu-screen.svg b/src/plugins/voip/buddies-only/g19-menu-screen.svg new file mode 100644 index 0000000..7b252d9 --- /dev/null +++ b/src/plugins/voip/buddies-only/g19-menu-screen.svg @@ -0,0 +1,326 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + ${emptyMessage} + + + + + + ${name} + ${channel} - ${mode} + + diff --git a/src/plugins/voip/default/Makefile.am b/src/plugins/voip/default/Makefile.am new file mode 100644 index 0000000..3775d9d --- /dev/null +++ b/src/plugins/voip/default/Makefile.am @@ -0,0 +1,31 @@ +themedir = $(datadir)/gnome15/plugins/voip/default +theme_DATA = \ + g19-menu-screen.svg \ + g19-menu-entry.svg \ + g19-message-menu-entry.svg \ + default-menu-screen.svg \ + default-menu-entry.svg \ + default-message-menu-entry.svg + +EXTRA_DIST = \ + $(theme_DATA) + + +all-local: + for M_LOCALE in @ENABLED_LOCALES@; do \ + mkdir -p i18n/$$M_LOCALE/LC_MESSAGES ; \ + if [ `ls i18n/*.po 2>/dev/null|wc -l` -gt 0 ]; then \ + for M_PO in i18n/*.po; do \ + BN=`basename $$M_PO .po`; \ + LL=`basename $$BN .$$M_LOCALE`.mo; \ + echo "$$M_PO -> $$LL"; \ + msgfmt $$M_PO --output-file i18n/$$M_LOCALE/LC_MESSAGES/$$LL; \ + done; \ + fi; \ + done + +install-exec-hook: + for M_LOCALE in @ENABLED_LOCALES@; do \ + mkdir -p $(DESTDIR)$(datadir)/gnome15/plugins/im/default/i18n; \ + cp -pR i18n/$$M_LOCALE $(DESTDIR)$(datadir)/gnome15/plugins/im/default/i18n; \ + done \ No newline at end of file diff --git a/src/plugins/voip/default/default-menu-entry.svg b/src/plugins/voip/default/default-menu-entry.svg new file mode 100644 index 0000000..a17f95c --- /dev/null +++ b/src/plugins/voip/default/default-menu-entry.svg @@ -0,0 +1,296 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + ${item_name} + ${item_alt} + + + + + + + + ${item_name} + ${item_alt} + + + + + + diff --git a/src/plugins/voip/default/default-menu-screen.svg b/src/plugins/voip/default/default-menu-screen.svg new file mode 100644 index 0000000..32686a1 --- /dev/null +++ b/src/plugins/voip/default/default-menu-screen.svg @@ -0,0 +1,215 @@ + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + ${channel} - ${mode} + + + + + + + + + + + + + + + + + ${emptyMessage} + + diff --git a/src/plugins/voip/default/default-message-menu-entry.svg b/src/plugins/voip/default/default-message-menu-entry.svg new file mode 100644 index 0000000..8245cd6 --- /dev/null +++ b/src/plugins/voip/default/default-message-menu-entry.svg @@ -0,0 +1,183 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + ${line} + + + + ${sender} - ${line} + + diff --git a/src/plugins/voip/default/default.theme b/src/plugins/voip/default/default.theme new file mode 100644 index 0000000..135f732 --- /dev/null +++ b/src/plugins/voip/default/default.theme @@ -0,0 +1,4 @@ +[theme] +name=Default +description=Displays buddies, messages and avatar (when supported) +supported_models=g19,g13,g15,g15v2,g510 \ No newline at end of file diff --git a/src/plugins/voip/default/g19-menu-entry.svg b/src/plugins/voip/default/g19-menu-entry.svg new file mode 100644 index 0000000..ba5de93 --- /dev/null +++ b/src/plugins/voip/default/g19-menu-entry.svg @@ -0,0 +1,405 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + ${item_name} + ${item_alt} + + + + + + + + ${item_name} + ${item_alt} + + + + + + diff --git a/src/plugins/voip/default/g19-menu-screen.svg b/src/plugins/voip/default/g19-menu-screen.svg new file mode 100644 index 0000000..1ecd835 --- /dev/null +++ b/src/plugins/voip/default/g19-menu-screen.svg @@ -0,0 +1,410 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + ${emptyMessage} + + + + + + + + ${name} + ${channel} - ${mode} + + + + + + + ${talking} + + diff --git a/src/plugins/voip/default/g19-message-menu-entry.svg b/src/plugins/voip/default/g19-message-menu-entry.svg new file mode 100644 index 0000000..215ca32 --- /dev/null +++ b/src/plugins/voip/default/g19-message-menu-entry.svg @@ -0,0 +1,204 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + ${sender} + ${line} + : + ${sender} + + diff --git a/src/plugins/voip/default_audio-high.gif b/src/plugins/voip/default_audio-high.gif new file mode 100644 index 0000000..38852d3 Binary files /dev/null and b/src/plugins/voip/default_audio-high.gif differ diff --git a/src/plugins/voip/default_audio-muted.gif b/src/plugins/voip/default_audio-muted.gif new file mode 100644 index 0000000..94e0935 Binary files /dev/null and b/src/plugins/voip/default_audio-muted.gif differ diff --git a/src/plugins/voip/default_available.gif b/src/plugins/voip/default_available.gif new file mode 100644 index 0000000..43f933a Binary files /dev/null and b/src/plugins/voip/default_available.gif differ diff --git a/src/plugins/voip/default_away.gif b/src/plugins/voip/default_away.gif new file mode 100644 index 0000000..5a0e1e0 Binary files /dev/null and b/src/plugins/voip/default_away.gif differ diff --git a/src/plugins/voip/default_microphone-sensitivity-high.gif b/src/plugins/voip/default_microphone-sensitivity-high.gif new file mode 100644 index 0000000..e6b29a2 Binary files /dev/null and b/src/plugins/voip/default_microphone-sensitivity-high.gif differ diff --git a/src/plugins/voip/default_microphone-sensitivity-muted.gif b/src/plugins/voip/default_microphone-sensitivity-muted.gif new file mode 100644 index 0000000..045b6ef Binary files /dev/null and b/src/plugins/voip/default_microphone-sensitivity-muted.gif differ diff --git a/src/plugins/voip/default_record.gif b/src/plugins/voip/default_record.gif new file mode 100644 index 0000000..e5a613d Binary files /dev/null and b/src/plugins/voip/default_record.gif differ diff --git a/src/plugins/voip/g19_microphone-sensitivity-high.png b/src/plugins/voip/g19_microphone-sensitivity-high.png new file mode 100644 index 0000000..65209c5 Binary files /dev/null and b/src/plugins/voip/g19_microphone-sensitivity-high.png differ diff --git a/src/plugins/voip/g19_microphone-sensitivity-muted.png b/src/plugins/voip/g19_microphone-sensitivity-muted.png new file mode 100644 index 0000000..0aef2bd Binary files /dev/null and b/src/plugins/voip/g19_microphone-sensitivity-muted.png differ diff --git a/src/plugins/voip/voip.py b/src/plugins/voip/voip.py new file mode 100644 index 0000000..2c5eb52 --- /dev/null +++ b/src/plugins/voip/voip.py @@ -0,0 +1,942 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2012 Brett Smith +# +# 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 . +# +# Notes +# ===== +# +# The program "contact-selector" was a big help in getting this working. The ContactList +# class is very loosely based on this, with many modifications. These are licensed under +# LGPL. See http://telepathy.freedesktop.org/wiki/Contact%20selector + + +import gnome15.g15locale as g15locale +_ = g15locale.get_translation("voip", modfile=__file__).ugettext + +import gnome15.g15globals as g15globals +import gnome15.util.g15convert as g15convert +import gnome15.util.g15scheduler as g15scheduler +import gnome15.util.g15uigconf as g15uigconf +import gnome15.util.g15gconf as g15gconf +import gnome15.util.g15icontools as g15icontools +import gnome15.g15theme as g15theme +import gnome15.g15driver as g15driver +import gnome15.g15plugin as g15plugin +import gnome15.g15screen as g15screen +import os +import time +import gnome15.colorpicker as colorpicker +from math import pi +import base64 +import cairo +import gtk + +# Logging +import logging +logger = logging.getLogger(__name__) + +# Actions +MUTE_INPUT = "voip-mute-input" +MUTE_OUTPUT = "voip-mute-ouptut" + +# Plugin details - All of these must be provided +id = "voip" +name = _("VoIP") +description = _("Provides integration with VoIP clients such as TeamSpeak3, showing \n\ +buddy lists, microphone status and more.\n\n\ +Note, TeamSpeak3 is currently the only supported client, but the intention is\n\ +to add support for others such as Mumble") +author = "Brett Smith " +copyright = _("Copyright (C)2012 Brett Smith") +site = "http://www.russo79.com/gnome15" +has_preferences = True +needs_network=True +unsupported_models = [ g15driver.MODEL_G110, g15driver.MODEL_G11, g15driver.MODEL_G930, g15driver.MODEL_G35 ] +actions = { + g15driver.PREVIOUS_SELECTION : _("Previous contact"), + g15driver.NEXT_SELECTION : _("Next contact"), + g15driver.VIEW : _("Show settings"), + g15driver.SELECT : _("Buddy options"), + g15driver.CLEAR : _("Toggle buddies/messages"), + g15driver.NEXT_PAGE : _("Next page"), + g15driver.PREVIOUS_PAGE : _("Previous page"), + MUTE_INPUT : _("Mute Input (Microphone)"), + MUTE_OUTPUT : _("Mute Output (Headphones/Speakers)") + } + +# Other constants +POSSIBLE_ICON_NAMES = [ "im-user", "empathy", "pidgin", "emesene", "system-config-users", "im-message-new" ] +MUTED_ICONS = ["microphone-sensitivity-muted", "microphone-sensitivity-muted-symbolic", \ + "audio-input-microphone-muted", "audio-input-microphone-muted-symbolic", \ + os.path.join(os.path.dirname(__file__), "g19_microphone-sensitivity-muted.png")] +UNMUTED_ICONS = ["microphone-sensitivity-high", "microphone-sensitivity-high-symbolic", \ + "audio-input-microphone-high", "audio-input-microphone-high-symbolic", \ + os.path.join(os.path.dirname(__file__), "g19_microphone-sensitivity-high.png")] +RECORD_ICONS = [ "media-record", "player_record" ] +MONO_RECORD_ICON = os.path.join(os.path.dirname(__file__), "default_record.gif") +MONO_MIC_UNMUTED = os.path.join(os.path.dirname(__file__), "default_microphone-sensitivity-high.gif") +MONO_MIC_MUTED = os.path.join(os.path.dirname(__file__), "default_microphone-sensitivity-muted.gif") +MONO_SPKR_UNMUTED = os.path.join(os.path.dirname(__file__), "default_audio-high.gif") +MONO_SPKR_MUTED = os.path.join(os.path.dirname(__file__), "default_audio-muted.gif") +MONO_AWAY = os.path.join(os.path.dirname(__file__), "default_away.gif") +MONO_ONLINE = os.path.join(os.path.dirname(__file__), "default_available.gif") + +IMAGE_DIR = 'images' + +MODE_ALL = "all" +MODE_ONLINE = "online" +MODE_TALKING = "talking" +MODE_LIST = [ MODE_ONLINE, MODE_TALKING, MODE_ALL ] +MODES = { + MODE_ALL : [ "All", _("All") ], + MODE_ONLINE : [ "Online", _("Online") ], + MODE_TALKING : [ "Talking", _("Talking") ] + } + + +def show_preferences(parent, driver, gconf_client, gconf_key): + widget_tree = gtk.Builder() + widget_tree.add_from_file(os.path.join(os.path.dirname(__file__), "voip.ui")) + dialog = widget_tree.get_object("VoipDialog") + dialog.set_transient_for(parent) + g15uigconf.configure_checkbox_from_gconf(gconf_client, "%s/raise_on_talk_status_change" % gconf_key, "RaiseOnTalkStatusChange", False, widget_tree) + g15uigconf.configure_checkbox_from_gconf(gconf_client, "%s/raise_on_chat_message" % gconf_key, "RaiseOnChatMessage", False, widget_tree) + dialog.run() + dialog.hide() + +def create(gconf_key, gconf_client, screen): + """ + Create the plugin instance + + gconf_key -- GConf key that may be used for plugin preferences + gconf_client -- GConf client instance + """ + return G15Voip(gconf_client, gconf_key, screen) + +def get_backend(backend_type): + """ + Get the backend plugin module, given the backend_type + + Keyword arguments: + backend_type -- backend type + """ + import gnome15.g15pluginmanager as g15pluginmanager + return g15pluginmanager.get_module_for_id("voip-%s" % backend_type) + +def get_available_backends(): + """ + Get the "backend type" names that are available by listing all of the voip + backend plugins that are installed + """ + l = [] + import gnome15.g15pluginmanager as g15pluginmanager + for p in g15pluginmanager.imported_plugins: + if p.id.startswith("voip-"): + l.append(p.id[5:]) + return l + +def get_backlight_key(gconf_key, buddy): + """ + Get the gconf key used to store the buddy backlight color + + Keyword arguments: + gconf_key -- key root (from plugin) + buddy -- buddy menuitem object + """ + enc_name = base64.b16encode(buddy.nickname) + return "%s/backlight_colors/%s" % (gconf_key, enc_name) + + +def compare_buddies(a, b): + """ + Compare two buddies based on their alias and presence + + Keyword arguments: + a -- buddy 1 + b -- buddy 2 + """ + if ( a is None and b is not None ): + val = 1 + elif ( b is None and a is not None ): + val = -1 + elif ( b is None and a is None ): + val = 0 + else: + val = cmp(a.talking, b.talking) + if val == 0: + val = cmp(a.away, b.away) + + return val + + +class VoipBackend(): + + def __init__(self): + pass + + def get_name(self): + """ + Get the backend name + """ + raise Exception("Not implemented") + + def get_current_channel(self): + """ + Get the current channel + """ + raise Exception("Not implemented") + + def get_talking(self): + """ + Get who is talking + """ + raise Exception("Not implemented") + + def get_me(self): + """ + Get the local user's buddy entry + """ + raise Exception("Not implemented") + + def get_channels(self): + raise Exception("Not implemented") + + def get_buddies(self, current_channel=True): + raise Exception("Not implemented") + + def start(self, plugin): + raise Exception("Not implemented") + + def stop(self): + raise Exception("Not implemented") + + def get_icon(self): + raise Exception("Not implemented") + + def set_audio_input(self, mute): + raise Exception("Not implemented") + + def set_audio_output(self, mute): + raise Exception("Not implemented") + + def away(self): + raise Exception("Not implemented") + + def online(self): + raise Exception("Not implemented") + +class MessageMenuItem(g15theme.MenuItem): + def __init__(self, sender, text, highlight): + g15theme.MenuItem.__init__(self, "message-%s" % time.time(), text, highlight) + self._text = text + self._highlight = highlight + self._sender = sender + + def get_theme_properties(self): + item_properties = g15theme.MenuItem.get_theme_properties(self) + item_properties["highlight"] = self._highlight + item_properties["sender"] = self._sender + item_properties["line"] = self._text + return item_properties + + def on_configure(self): + self.set_theme(g15theme.G15Theme(self.parent.get_theme().dir, "message-menu-entry")) + +class ChannelMenuItem(g15theme.MenuItem): + def __init__(self, component_id, name, backend, icon = None): + g15theme.MenuItem.__init__(self, component_id, icon = icon) + self.name = name + self.topic = None + self.backend = backend + self.radio = True + + def get_theme_properties(self): + p = g15theme.MenuItem.get_theme_properties(self) + p["item_radio"] = self.radio + if self.radio: + p["item_radio_selected"] = self == self.backend.get_current_channel() + return p + + def activate(self): + ret = self.on_activate() + self.get_root().delete() + return ret + + def on_activate(self): + return True + +class BuddyMenuItem(g15theme.MenuItem): + def __init__(self, component_id, nickname, channel, plugin): + self.nickname = nickname + self._plugin = plugin + g15theme.MenuItem.__init__(self, component_id) + self.input_muted = None + self.output_muted = None + self.away = False + self.talking = False + self.channel = channel + + def activate(self): + self._plugin.activate_item(self) + return True + + def on_configure(self): + self.set_theme(g15theme.G15Theme(self.parent.get_theme().dir, "menu-entry" if self.group else "menu-child-entry")) + + def is_showing(self): + menu = self.parent + if not menu: + return False + is_showing_status = (menu.mode == MODE_TALKING and self.talking) or \ + (menu.mode == MODE_ONLINE and not self.away) or \ + menu.mode == MODE_ALL + is_showing_channel = self.channel == self._plugin.backend.get_current_channel() + return is_showing_status and is_showing_channel + + def get_theme_properties(self): + item_properties = g15theme.MenuItem.get_theme_properties(self) + + if self.away and isinstance(self.away, str): + item_properties["item_name"] = "%s - %s" % (self.nickname, self.away) + else: + item_properties["item_name"] = self.nickname + + item_properties["item_alt"] = "" + item_properties["item_type"] = "" + + if self.get_screen().device.bpp == 1: + item_properties["item_talking_icon"] = MONO_RECORD_ICON if self.talking else "" + item_properties["item_input_muted_icon"] = MONO_MIC_MUTED if self.input_muted else MONO_MIC_UNMUTED + item_properties["item_output_muted_icon"] = MONO_SPKR_MUTED if self.output_muted else MONO_SPKR_UNMUTED + item_properties["item_icon"] = MONO_ONLINE if not self.away else MONO_AWAY + else: + item_properties["item_input_muted_icon"] = g15icontools.get_icon_path(MUTED_ICONS if self.input_muted else UNMUTED_ICONS) + item_properties["item_output_muted_icon"] = g15icontools.get_icon_path("audio-volume-muted" if self.output_muted else "audio-volume-high") + item_properties["item_icon"] = g15icontools.get_icon_path("user-available" if not self.away else "user-away") + item_properties["item_talking_icon"] = g15icontools.get_icon_path(RECORD_ICONS) if self.talking else "" + + return item_properties + + +class BuddyMenu(g15theme.Menu): + + def __init__(self): + g15theme.Menu.__init__(self, "menu") + self.mode = MODE_ONLINE + self.focusable = True + +class G15Voip(g15plugin.G15MenuPlugin): + + def __init__(self, gconf_client, gconf_key, screen): + g15plugin.G15MenuPlugin.__init__(self, gconf_client, gconf_key, screen, POSSIBLE_ICON_NAMES, id, name) + self.hidden = False + + def activate(self): + """ + We override default activate behavior as we only want the page to + appear once connected to the backend + """ + self.backend = None + self.message_menu = None + self.active = True + + self._talking = None + self._backlight_ctrl = self.screen.driver.get_control("backlight_colour") + self._backlight_acq = None + self._raise_timer = None + self._connected = False + self._connection_timer = None + + self.screen.key_handler.action_listeners.append(self) + self.reload_theme() + self._attempt_connection() + + def _attempt_connection(self): + try: + if self._raise_timer is not None: + self._raise_timer.cancel() + # For now, TS3 only backend + self.backend = get_backend("teamspeak3").create_backend() + self.set_icon(self.backend.get_icon()) + if self.backend.start(self): + self.show_menu() + self._connected = True + else: + self._connection_timer = g15scheduler.schedule("ReconnectVoip", 5, self._attempt_connection) + except Exception as e: + logger.debug("Error connecting. Will retry...", exc_info = e) + self._connection_timer = g15scheduler.schedule("ReconnectVoip", 5, self._attempt_connection) + + def create_menu(self): + return BuddyMenu() + + def create_page(self): + page = g15plugin.G15MenuPlugin.create_page(self) + m = g15theme.Menu("messagesMenu") + m.focusable = True + page.add_child(m) + self.message_menu = m + page.add_child(g15theme.MenuScrollbar("messagesScrollbar", self.message_menu)) + return page + + def deactivate(self): + if self._backlight_acq: + self.screen.driver.release_control(self._backlight_acq) + self._backlight_acq = None + if self._connection_timer is not None: + self._connection_timer.cancel() + if self.backend is not None: + self.backend.stop() + self.screen.key_handler.action_listeners.remove(self) + g15plugin.G15MenuPlugin.deactivate(self) + + def load_menu_items(self): + g15plugin.G15MenuPlugin.load_menu_items(self) + self._load_buddy_list() + + def action_performed(self, binding): + if not self._connected: + return False + + if self.page != None and self.page.is_visible(): + + if binding.action == g15driver.VIEW: + MeOperationMenu(self.gconf_client, self.gconf_key, self.screen, self.backend, self.menu, self) + return True + + if binding.action == g15driver.CLEAR and self.page != None and self.page.is_visible(): + self.page.next_focus() + return True + + if self.menu.is_focused(): + return self.menu.action_performed(binding) + + def get_theme_properties(self): + props = g15plugin.G15MenuPlugin.get_theme_properties(self) + props["mode"] = MODES[self.menu.mode][1] + props["name"] = self.backend.get_name() if self.backend is not None else "" + props["channel"] = self.backend.get_current_channel().name if self.backend is not None else "" + + if self.menu.get_showing_count() == 0: + if self.menu.mode == MODE_ALL: + props["emptyMessage"] = _("Nobody connected") + elif self.menu.mode == MODE_ONLINE: + props["emptyMessage"] = _("Nobody online") + elif self.menu.mode == MODE_TALKING: + props["emptyMessage"] = _("Nobody talking") + else: + props["emptyMessage"] = "" + + # Get what mode to switch to + mode_index = MODE_LIST.index(self.menu.mode) + 1 + if mode_index >= len(MODE_LIST): + mode_index = 0 + + props["list"] = MODES[MODE_LIST[mode_index]][0] + + talking_buddy = self.backend.get_talking() + me = self.backend.get_me() + + props["talking"] = talking_buddy.nickname if talking_buddy is not None else "" + props["talking_avatar"] = talking_buddy.avatar if talking_buddy is not None else (me.avatar if me is not None and me.avatar is not None else self.backend.get_icon()) + props["talking_avatar_icon"] = g15icontools.get_icon_path(RECORD_ICONS) if talking_buddy is not None else None + + if self.screen.device.bpp == 1: + props["talking_icon"] = MONO_RECORD_ICON if me is not None and me.talking else "" + props["input_muted_icon"] = MONO_MIC_MUTED if me is not None and me.input_muted else MONO_MIC_UNMUTED + props["output_muted_icon"] = MONO_SPKR_MUTED if me is not None and me.output_muted else MONO_SPKR_UNMUTED + props["status_icon"] = MONO_ONLINE if me is not None and not me.away else MONO_AWAY + else: + props["status_icon"] = g15icontools.get_icon_path("user-available" if me is not None and not me.away else "user-away") + props["input_muted_icon"] = g15icontools.get_icon_path(MUTED_ICONS if me is not None and me.input_muted else UNMUTED_ICONS) + props["output_muted_icon"] = g15icontools.get_icon_path("audio-volume-muted" if me is not None and me.output_muted else "audio-volume-high") + props["talking_icon"] = g15icontools.get_icon_path(RECORD_ICONS) if me is not None and me.talking else "" + + return props + + + """ + Backends may call these functions when they get events internally + """ + def buddy_left(self, buddy_item): + if buddy_item.channel.name == self.backend.get_current_channel().name: + self.message_received(self.backend.get_name(), _("%s left channel" % buddy_item.nickname), True) + if self.menu is not None: + self.menu.remove_child(buddy_item) + + def redraw(self): + """ + Redraw the page + """ + if self.page is not None: + self.page.mark_dirty() + self.page.redraw() + + def message_received(self, sender, message, highlight = False): + """ + Add a message to the message list + + Keyword arguments: + sender -- sender + message -- message + """ + if self.message_menu is not None: + while self.message_menu.get_child_count() > 20: + self.message_menu.remove_child_at(0) + self.message_menu.add_child(MessageMenuItem(sender, message, highlight)) + self.message_menu.select_last_item() + self.page.mark_dirty() + if g15gconf.get_bool_or_default(self.gconf_client, "%s/raise_on_chat_message" % self.gconf_key, False): + self._popup() + + def activate_item(self, item): + """ + Activate a buddy item, showing the menu + + Keyword arugments: + item -- buddy menu item object + """ + BuddyOperationMenu(self.gconf_client, self.gconf_key, self.screen, self.backend, item, self) + + def reload_buddies(self): + """ + Reload all buddies + """ + if self.page is not None: + self._load_buddy_list() + self.redraw() + + def new_buddy(self, buddy_item): + """ + A new buddy has entered view + + Keyword arguments: + buddy_item -- new buddy + """ + if buddy_item.channel.name == self.backend.get_current_channel().name: + self.message_received(self.backend.get_name(), _("%s entered channel" % buddy_item.nickname), True) + if self.menu is not None: + items = self.menu.get_children() + items.append(buddy_item) + self.menu.set_children(sorted(items, cmp=compare_buddies)) + + self.redraw() + + def new_channel(self, channel_item): + """ + A new channel has been created + + Keyword arugments: + channel_item -- channel menu item object + """ + self.redraw() + + def channel_removed(self, channel_item): + """ + A channel has been removed + + Keyword arugments: + channel_item -- channel menu item object + """ + self.redraw() + + def channel_updated(self, channel_item): + """ + A channel has been updated + + Keyword arugments: + channel_item -- channel menu item object + """ + self.redraw() + + def channel_moved(self, channel_item): + """ + A channel has been moved + + Keyword arugments: + channel_item -- channel menu item object + """ + self.redraw() + + def moved_channels(self, buddy_item, old_channel, new_channel): + """ + A buddy has moved channels + + Keyword arugments: + buddy_item -- buddy menu item object + old_channel -- old channel menu item object + new_channel -- new channel menu item object + """ +# if buddy_item.channel.name == self.backend.get_current_channel().name: +# self.message_received(self.backend.get_name(), _("%s changed channels" % buddy_item.nickname), True) + self.redraw() + + def talking_status_changed(self, talking): + """ + Current talking buddy has changed. + + Keyword arguments: + talking -- new buddy talking + """ + if (self._talking is None and talking is not None) or \ + (talking is None and self._talking is not None) or \ + (talking is not None and talking != self._talking): + self._talking = talking + if self._backlight_acq is not None and self._talking is None: + self.screen.driver.release_control(self._backlight_acq) + self._backlight_acq = None + if self._talking is not None: + hex_color = g15gconf.get_string_or_default(self.gconf_client, get_backlight_key(self.gconf_key, self._talking), "") + if hex_color != "": + if self._backlight_acq is None: + self._backlight_acq = self.screen.driver.acquire_control(self._backlight_ctrl) + self._backlight_acq.set_value(g15convert.to_rgb(hex_color)) + + self.redraw() + if g15gconf.get_bool_or_default(self.gconf_client, "%s/raise_on_talk_status_change" % self.gconf_key, False): + self._popup() + + """ + Private + """ + + def _popup(self): + self._raise_timer = self.screen.set_priority(self.page, g15screen.PRI_HIGH, revert_after=6.0) + + def _load_buddy_list(self): + items = self.backend.get_buddies() + self.menu.set_children(sorted(items, cmp=compare_buddies)) + if len(items) > 0: + self.menu.selected = items[0] + else: + self.menu.selected = None + + def _disconnected(self): + self._connected = False + self.hide_menu() + self._attempt_connection() + +class BuddyActionMenuItem(g15theme.MenuItem): + def __init__(self, component_id, name, buddy, backend, icon=None): + g15theme.MenuItem.__init__(self, component_id, True, name, icon=icon) + self.buddy = buddy + self.backend = backend + +class KickBuddyMenuItem(BuddyActionMenuItem): + def __init__(self, buddy, backend): + BuddyActionMenuItem.__init__(self, 'kick', _('Kick'), buddy, backend, icon=g15icontools.get_icon_path(['force-exit', 'gnome-panel-force-quit'], include_missing=False)) + + def _confirm(self, arg): + self.backend.kick(self.buddy) + self.get_root().delete() + + def activate(self): + g15theme.ConfirmationScreen(self.get_screen(), _("Kick Buddy"), _("Are you sure you want to kick\n%s from the server") % self.buddy.nickname, + self.backend.get_icon(), self._confirm, None) + +class BanBuddyMenuItem(BuddyActionMenuItem): + def __init__(self, buddy, backend): + BuddyActionMenuItem.__init__(self, 'ban', _('Ban'), buddy, backend, icon=g15icontools.get_icon_path(['audio-volume-muted-blocked', 'mail_spam', 'stock_spam'], include_missing=False)) + + def _confirm(self, arg): + self.backend.ban(self.buddy) + self.get_root().delete() + + def activate(self): + g15theme.ConfirmationScreen(self.get_screen(), _("Ban Buddy"), _("Are you sure you want to ban\n%s from the server") % self.buddy.nickname, + self.backend.get_icon(), self._confirm, None) + + +class SelectChannelMenuItem(g15theme.MenuItem): + def __init__(self, gconf_client, gconf_key, backend, plugin): + g15theme.MenuItem.__init__(self, 'channel', True, _('Select channel / server'), icon=g15icontools.get_icon_path(['addressbook', 'office-address-book', 'stcok_addressbook', 'x-office-address-book' ], include_missing=False)) + self._gconf_client = gconf_client + self._gconf_key = gconf_key + self._backend = backend + self._plugin = plugin + + def activate(self): + SelectChannelMenu(self._gconf_client, self._gconf_key, self.get_screen(), self._backend, self._plugin) + +class BuddyBacklightMenuItem(BuddyActionMenuItem): + def __init__(self, gconf_client, gconf_key, buddy, backend, ctrl, plugin): + BuddyActionMenuItem.__init__(self, 'color', _('Select backlight'), buddy, backend, icon=g15icontools.get_icon_path(['preferences-color', 'gtk-select-color', 'color-picker' ], include_missing=False)) + self._ctrl = ctrl + self._gconf_client = gconf_client + self._gconf_key = gconf_key + self._plugin = plugin + + def activate(self): + BuddyBacklightMenu(self._gconf_client, self._gconf_key, self.get_screen(), self.backend, self.buddy, self._ctrl, self._plugin) + +class ReturnMenuItem(g15theme.MenuItem): + def __init__(self): + g15theme.MenuItem.__init__(self, 'return', True, _('Back to previous menu'), icon=g15icontools.get_icon_path(['back', 'gtk-go-back-ltr'])) + + def activate(self): + self.get_root().delete() + +class AudioInputMenuItem(g15theme.MenuItem): + def __init__(self, backend): + g15theme.MenuItem.__init__(self, 'audio-input', False, _("Input")) + self._backend = backend + + def get_theme_properties(self): + p = g15theme.MenuItem.get_theme_properties(self) + me = self._backend.get_me() + if self.get_screen().device.bpp == 1: + p["item_icon"] = MONO_MIC_MUTED if me.input_muted else MONO_MIC_UNMUTED + else: + p["item_icon"] = g15icontools.get_icon_path(MUTED_ICONS if me.input_muted else UNMUTED_ICONS) + p["item_name"] = _("Un-mute audio input") if me.input_muted else _("Mute audio input") + return p + + def activate(self): + self._backend.set_audio_input(not self._backend.get_me().input_muted) + self.get_root().delete() + +class AudioOutputMenuItem(g15theme.MenuItem): + def __init__(self, backend): + g15theme.MenuItem.__init__(self, 'audio-output', False, _("Output")) + self._backend = backend + + def get_theme_properties(self): + p = g15theme.MenuItem.get_theme_properties(self) + me = self._backend.get_me() + if self.get_screen().device.bpp == 1: + p["item_icon"] = MONO_SPKR_MUTED if me.output_muted else MONO_SPKR_UNMUTED + else: + p["item_icon"] = g15icontools.get_icon_path("audio-volume-muted" if me.output_muted else "audio-volume-high") + p["item_name"] = _("Un-mute audio output") if me.output_muted else _("Mute audio output") + return p + + def activate(self): + self._backend.set_audio_output(not self._backend.get_me().output_muted) + self.get_root().delete() + +class AwayStatusMenuItem(g15theme.MenuItem): + def __init__(self, backend): + g15theme.MenuItem.__init__(self, 'away', False, _("Away"), icon=g15icontools.get_icon_path("user-away")) + self._backend = backend + + def get_theme_properties(self): + p = g15theme.MenuItem.get_theme_properties(self) + p["item_radio"] = True + p["item_radio_selected"] = self._backend.get_me().away + return p + + def activate(self): + self._backend.away() + self.get_root().delete() + +class OnLineStatusMenuItem(g15theme.MenuItem): + def __init__(self, backend): + g15theme.MenuItem.__init__(self, 'online', False, _("Online"), icon=g15icontools.get_icon_path("user-available")) + self._backend = backend + + def activate(self): + self._backend.online() + self.get_root().delete() + + def get_theme_properties(self): + p = g15theme.MenuItem.get_theme_properties(self) + p["item_radio"] = True + p["item_radio_selected"] = not self._backend.get_me().away + return p + +class SelectModeMenuItem(g15theme.MenuItem): + def __init__(self, gconf_client, gconf_key, mode, mode_name, backend, buddy_menu): + g15theme.MenuItem.__init__(self, 'mode-%s' % mode, False, mode_name) + self._buddy_menu = buddy_menu + self._mode = mode + self._gconf_client = gconf_client + self._gconf_key = gconf_key + + def get_theme_properties(self): + p = g15theme.MenuItem.get_theme_properties(self) + p["item_radio"] = True + p["item_radio_selected"] = self._mode == g15gconf.get_string_or_default(self._gconf_client, "%s/mode" % self._gconf_key, MODE_ONLINE) + return p + + def activate(self): + self._buddy_menu.mode = self._mode + logger.info("Mode is now %s", self._mode) + self._gconf_client.set_string(self._gconf_key + "/mode", self._mode) + self._buddy_menu.get_screen().redraw(self._buddy_menu.get_root()) + self.get_root().delete() + +class ColorMenuItem(BuddyActionMenuItem): + def __init__(self, gconf_client, gconf_key, color, color_name, buddy, backend): + fmt_color = "%02x%02x%02xff" % color + BuddyActionMenuItem.__init__(self, 'color-%s' % fmt_color, color_name, buddy, backend) + self.icon = self.color_square(color, 16, 2) + self.color = color + self._gconf_client = gconf_client + self._gconf_key = gconf_key + + def activate(self): + self._gconf_client.set_string(get_backlight_key(self._gconf_key, self.buddy), g15convert.rgb_to_string(self.color)) + self.get_root().delete() + + def color_square(self, color, size, radius=0): + surface = cairo.ImageSurface (cairo.FORMAT_ARGB32, size, size) + cr = cairo.Context(surface) + cr.set_source_rgba(color[0] / 255.0, + color[1] / 255.0, + color[2] / 255.0, 1.0) + cr.move_to(radius, 0) + cr.line_to(size - radius, 0) + cr.arc(size - radius, radius, radius, 3 * pi / 2, 2 * pi) + cr.line_to(size, size - radius) + cr.arc(size - radius, size - radius, radius, 0, pi / 2) + cr.line_to(radius, size) + cr.arc(radius, size - radius, radius, pi / 2, pi) + cr.line_to(0, radius) + cr.arc(radius, radius, radius, pi, 3 * pi / 2) + cr.close_path() + cr.fill() + return surface + + +class SelectChannelMenu(g15theme.G15Page): + + def __init__(self, gconf_client, gconf_key, screen, backend, plugin): + g15theme.G15Page.__init__(self, _("Server/Channel"), screen, priority=g15screen.PRI_HIGH, \ + theme=g15theme.G15Theme(os.path.join(g15globals.themes_dir, "default"), "menu-screen"), + originating_plugin = plugin) + self.theme_properties = { + "title": _("Server/Channel"), + "icon": backend.get_icon(), + "alt_title": '' + } + self.menu = g15theme.Menu("menu") + self.get_screen().add_page(self) + self.add_child(self.menu) + for c in backend.get_channels(): + self.menu.add_child(c) + if c == backend.get_current_channel(): + self.menu.set_selected_item(c) + self.menu.add_child(ReturnMenuItem()) + self.add_child(g15theme.MenuScrollbar("viewScrollbar", self.menu)) + +class BuddyBacklightMenu(g15theme.G15Page): + + def __init__(self, gconf_client, gconf_key, screen, backend, buddy, ctrl, plugin): + g15theme.G15Page.__init__(self, _("Backlight"), screen, priority=g15screen.PRI_HIGH, \ + theme=g15theme.G15Theme(os.path.join(g15globals.themes_dir, "default"), "menu-screen"), + originating_plugin = plugin) + self.theme_properties = { + "title": _("Backlight"), + "icon": backend.get_icon() if buddy.avatar is None else buddy.avatar, + "alt_title": buddy.nickname + } + self.menu = g15theme.Menu("menu") + self.get_screen().add_page(self) + self.add_child(self.menu) + self.ctrl = ctrl + self.acq = None + + sel_color = g15convert.to_rgb(g15gconf.get_string_or_default( + gconf_client, get_backlight_key(gconf_key, buddy), + "255,255,255")) + for i, c in enumerate(colorpicker.COLORS_FULL): + c = (c[0], c[1], c[2]) + item = ColorMenuItem(gconf_client, gconf_key, c, + colorpicker.COLORS_NAMES[i], buddy, backend) + self.menu.add_child(item) + if c == sel_color: + self.menu.set_selected_item(item) + + self.menu.on_selected = self._handle_selected + self.on_deleted = self._release_control + self.menu.add_child(ReturnMenuItem()) + self.add_child(g15theme.MenuScrollbar("viewScrollbar", self.menu)) + self._handle_selected() + + def _handle_selected(self): + self._release_control() + if isinstance(self.menu.selected, ColorMenuItem): + self.acq = self.get_screen().driver.acquire_control(self.ctrl) + self.acq.set_value(self.menu.selected.color) + + def _release_control(self): + if self.acq is not None: + self.get_screen().driver.release_control(self.acq) + self.acq = None + +class MeOperationMenu(g15theme.G15Page): + """ + Me to select operations appropriate for the current local user. Includes + setting channel, status, buddy list mode and others + """ + + def __init__(self, gconf_client, gconf_key, screen, backend, buddy_menu, plugin): + g15theme.G15Page.__init__(self, _("Settings"), screen, priority=g15screen.PRI_HIGH, \ + theme=g15theme.G15Theme(os.path.join(g15globals.themes_dir, "default"), "menu-screen"), + originating_plugin = plugin) + me = backend.get_me() + self.theme_properties = { + "title": _("Settings"), + "icon": backend.get_icon() if me.avatar is None else me.avatar, + "alt_title": me.nickname + } + self.menu = g15theme.Menu("menu") + self.get_screen().add_page(self) + self.add_child(self.menu) + + + self.menu.add_child(SelectChannelMenuItem(gconf_client, gconf_key, backend, plugin)) + + self.menu.add_child(g15theme.MenuItem('audio-status', True, _('Audio'), activatable=False)) + self.menu.add_child(AudioInputMenuItem(backend)) + self.menu.add_child(AudioOutputMenuItem(backend)) + + self.menu.add_child(g15theme.MenuItem('select-status', True, _('Select Status'), activatable=False)) + self.menu.add_child(OnLineStatusMenuItem(backend)) + self.menu.add_child(AwayStatusMenuItem(backend)) + + self.menu.add_child(g15theme.MenuItem('select-mode', True, _('Select Buddy List Mode'), activatable=False)) + for i in MODE_LIST: + self.menu.add_child(SelectModeMenuItem(gconf_client, gconf_key, i, MODES[i][1], backend, buddy_menu)) + + self.menu.add_child(ReturnMenuItem()) + self.add_child(g15theme.MenuScrollbar("viewScrollbar", self.menu)) + +class BuddyOperationMenu(g15theme.G15Page): + """ + Menu for operations appropriate for other buddies including kick, ban + and select backlight + """ + + def __init__(self, gconf_client, gconf_key, screen, backend, buddy, plugin): + g15theme.G15Page.__init__(self, _("Actions"), screen, priority=g15screen.PRI_HIGH, \ + theme=g15theme.G15Theme(os.path.join(g15globals.themes_dir, "default"), "menu-screen"), + originating_plugin = plugin) + self.theme_properties = { + "title": _("Actions"), + "icon": backend.get_icon() if buddy.avatar is None else buddy.avatar, + "alt_title": buddy.nickname + } + self.menu = g15theme.Menu("menu") + self.get_screen().add_page(self) + self.add_child(self.menu) + self.menu.add_child(KickBuddyMenuItem(buddy, backend)) + self.menu.add_child(BanBuddyMenuItem(buddy, backend)) + ctrl = screen.driver.get_control("backlight_colour") + if ctrl is not None: + self.menu.add_child(BuddyBacklightMenuItem(gconf_client, gconf_key, buddy, backend, ctrl, plugin)) + self.menu.add_child(ReturnMenuItem()) + self.add_child(g15theme.MenuScrollbar("viewScrollbar", self.menu)) + diff --git a/src/plugins/voip/voip.ui b/src/plugins/voip/voip.ui new file mode 100644 index 0000000..eb9315c --- /dev/null +++ b/src/plugins/voip/voip.ui @@ -0,0 +1,115 @@ + + + + + + + + + + + + + zoom + + + tile + + + center + + + scale + + + stretch + + + + + False + 5 + VoIP Preferences + False + True + center-on-parent + dialog + + + True + False + 2 + + + True + False + 4 + + + Raise when talk status changes + True + True + False + True + + + True + True + 0 + + + + + Raise on chat message + True + True + False + True + + + True + True + 1 + + + + + False + False + 0 + + + + + True + False + end + + + gtk-close + True + True + True + True + + + False + False + 0 + + + + + False + True + end + 1 + + + + + + button9 + + + diff --git a/src/plugins/volume/Makefile.am b/src/plugins/volume/Makefile.am new file mode 100644 index 0000000..468eb2a --- /dev/null +++ b/src/plugins/volume/Makefile.am @@ -0,0 +1,8 @@ +SUBDIRS = default + +plugindir = $(datadir)/gnome15/plugins/volume +plugin_DATA = volume.py \ + volume.ui + +EXTRA_DIST = \ + $(plugin_DATA) \ No newline at end of file diff --git a/src/plugins/volume/default/Makefile.am b/src/plugins/volume/default/Makefile.am new file mode 100644 index 0000000..20da2ea --- /dev/null +++ b/src/plugins/volume/default/Makefile.am @@ -0,0 +1,7 @@ +themedir = $(datadir)/gnome15/plugins/volume/default +theme_DATA = g19.svg \ + mx5500.svg \ + default.svg + +EXTRA_DIST = \ + $(theme_DATA) diff --git a/src/plugins/volume/default/default.svg b/src/plugins/volume/default/default.svg new file mode 100644 index 0000000..7c4998f --- /dev/null +++ b/src/plugins/volume/default/default.svg @@ -0,0 +1,170 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + Muted + + diff --git a/src/plugins/volume/default/g19.svg b/src/plugins/volume/default/g19.svg new file mode 100644 index 0000000..40dbffc --- /dev/null +++ b/src/plugins/volume/default/g19.svg @@ -0,0 +1,154 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/src/plugins/volume/default/mx5500.svg b/src/plugins/volume/default/mx5500.svg new file mode 100644 index 0000000..243bd0d --- /dev/null +++ b/src/plugins/volume/default/mx5500.svg @@ -0,0 +1,171 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + Muted + + diff --git a/src/plugins/volume/i18n/volume.en_GB.po b/src/plugins/volume/i18n/volume.en_GB.po new file mode 100644 index 0000000..b707fba --- /dev/null +++ b/src/plugins/volume/i18n/volume.en_GB.po @@ -0,0 +1,26 @@ +# English translations for gnome15-plugins package. +# Copyright (C) 2012 THE gnome15-plugins'S COPYRIGHT HOLDER +# This file is distributed under the same license as the gnome15-plugins package. +# Automatically generated, 2012. +# +msgid "" +msgstr "" +"Project-Id-Version: gnome15-plugins 0.8.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: 2012-01-08 11:23+0000\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: en_GB\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=ASCII\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: i18n/volume.glade.h:1 +msgid "Device" +msgstr "Device" + +#: i18n/volume.glade.h:2 +msgid "Volume Preferences" +msgstr "Volume Preferences" diff --git a/src/plugins/volume/i18n/volume.glade.h b/src/plugins/volume/i18n/volume.glade.h new file mode 100644 index 0000000..b40ba0c --- /dev/null +++ b/src/plugins/volume/i18n/volume.glade.h @@ -0,0 +1,2 @@ +char *s = N_("Device"); +char *s = N_("Volume Preferences"); diff --git a/src/plugins/volume/i18n/volume.pot b/src/plugins/volume/i18n/volume.pot new file mode 100644 index 0000000..dd9423b --- /dev/null +++ b/src/plugins/volume/i18n/volume.pot @@ -0,0 +1,26 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: i18n/volume.glade.h:1 +msgid "Device" +msgstr "" + +#: i18n/volume.glade.h:2 +msgid "Volume Preferences" +msgstr "" diff --git a/src/plugins/volume/volume.py b/src/plugins/volume/volume.py new file mode 100644 index 0000000..76afbc1 --- /dev/null +++ b/src/plugins/volume/volume.py @@ -0,0 +1,415 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2010 Brett Smith +# +# 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 . + +import gnome15.g15locale as g15locale +_ = g15locale.get_translation("volume", modfile = __file__).ugettext + +import gnome15.g15screen as g15screen +import gnome15.util.g15scheduler as g15scheduler +import gnome15.util.g15uigconf as g15uigconf +import gnome15.util.g15gconf as g15gconf +import gnome15.util.g15icontools as g15icontools +import gnome15.g15theme as g15theme +import gnome15.g15driver as g15driver +import gnome15.g15devices as g15devices +import gnome15.g15actions as g15actions + +import alsaaudio +import select +import os +import gtk +import logging +logger = logging.getLogger(__name__) + +from threading import Thread + +# Custom actions +VOLUME_UP = "volume-up" +VOLUME_DOWN = "volume-down" +MUTE = "mute" + +# Register the action with all supported models +g15devices.g15_action_keys[VOLUME_UP] = g15actions.ActionBinding(VOLUME_UP, [ g15driver.G_KEY_VOL_UP ], g15driver.KEY_STATE_UP) +g15devices.g19_action_keys[VOLUME_UP] = g15actions.ActionBinding(VOLUME_UP, [ g15driver.G_KEY_VOL_UP ], g15driver.KEY_STATE_UP) +g15devices.g15_action_keys[VOLUME_DOWN] = g15actions.ActionBinding(VOLUME_DOWN, [ g15driver.G_KEY_VOL_DOWN ], g15driver.KEY_STATE_UP) +g15devices.g19_action_keys[VOLUME_DOWN] = g15actions.ActionBinding(VOLUME_DOWN, [ g15driver.G_KEY_VOL_DOWN ], g15driver.KEY_STATE_UP) +g15devices.g15_action_keys[MUTE] = g15actions.ActionBinding(MUTE, [ g15driver.G_KEY_MUTE ], g15driver.KEY_STATE_UP) +g15devices.g19_action_keys[MUTE] = g15actions.ActionBinding(MUTE, [ g15driver.G_KEY_MUTE ], g15driver.KEY_STATE_UP) + +# Plugin details - All of these must be provided +id="volume" +name=_("Volume Monitor") +description=_("Uses the M-Key lights as a volume meter. If your model has \ +a screen, a page will also popup showing the current volume. \ +You may choose the mixer that is monitored in the preferences for this plugin.\n\n \ +This plugin also registers some actions that may be assigned to macro keys. \ +The actions volume-up, volume-down and mute all work directly on the mixer, \ +so may be used control the master volume when full screen games are running too.") +author="Brett Smith " +copyright=_("Copyright (C)2010 Brett Smith") +site="http://www.russo79.com/gnome15" +has_preferences=True +default_enabled=True +unsupported_models = [ g15driver.MODEL_G930, g15driver.MODEL_G35 ] +actions={ + VOLUME_UP : "Increase the volume", + VOLUME_DOWN : "Decrease the volume", + MUTE : "Mute", + } + + +''' +This plugin displays a high priority screen when the volume is changed for a +fixed number of seconds +''' + +def create(gconf_key, gconf_client, screen): + return G15Volume(screen, gconf_client, gconf_key) + +def show_preferences(parent, driver, gconf_client, gconf_key): + def refresh_devices(widget): + new_soundcard_name = soundcard_model[widget.get_active()][0] + new_soundcard_index = alsa_soundcards.index(new_soundcard_name) + ''' + We temporarily block the handler for the mixer_combo 'changed' signal, since we are going + to change the combobox contents. + ''' + mixer_combo.handler_block(changed_handler_id) + mixer_model.clear() + for mixer in alsaaudio.mixers(new_soundcard_index): + mixer_model.append([mixer]) + # Now we can unblock the handler + mixer_combo.handler_unblock(changed_handler_id) + # And since the list of mixers has changed, we select the first one by default + mixer_combo.set_active(0) + + widget_tree = gtk.Builder() + widget_tree.add_from_file(os.path.join(os.path.dirname(__file__), "volume.ui")) + dialog = widget_tree.get_object("VolumeDialog") + soundcard_combo = widget_tree.get_object('SoundcardCombo') + mixer_combo = widget_tree.get_object('MixerCombo') + soundcard_model = widget_tree.get_object("SoundcardModel") + mixer_model = widget_tree.get_object("MixerModel") + alsa_soundcards = alsaaudio.cards() + soundcard_name = g15gconf.get_string_or_default(gconf_client, + gconf_key + "/soundcard", + str(alsa_soundcards[0])) + soundcard_index = alsa_soundcards.index(soundcard_name) + soundcard_mixers = alsaaudio.mixers(soundcard_index) + + for card in alsa_soundcards: + soundcard_model.append([card]) + for mixer in soundcard_mixers: + mixer_model.append([mixer]) + + g15uigconf.configure_combo_from_gconf(gconf_client, \ + gconf_key + "/soundcard", \ + "SoundcardCombo", \ + str(alsa_soundcards[0]), \ + widget_tree) + + changed_handler_id = g15uigconf.configure_combo_from_gconf(gconf_client, \ + gconf_key + "/mixer", \ + "MixerCombo", \ + str(soundcard_mixers[0]), \ + widget_tree) + soundcard_combo.connect('changed', refresh_devices) + + dialog.set_transient_for(parent) + dialog.run() + dialog.hide() + +class G15Volume(): + + def __init__(self, screen, gconf_client, gconf_key): + self._screen = screen + self._gconf_client = gconf_client + self._gconf_key = gconf_key + self._volume = 0.0 + self._volthread = None + self._mute = False + self._light_controls = None + self._lights_timer = None + self._reload_config_timer = None + + def activate(self): + self._screen.key_handler.action_listeners.append(self) + self._activated = True + self._read_config() + self._start_monitoring() + self._notify_handler = self._gconf_client.notify_add(self._gconf_key, self._config_changed); + + def deactivate(self): + self._screen.key_handler.action_listeners.remove(self) + self._activated = False + self._stop_monitoring() + self._gconf_client.notify_remove(self._notify_handler) + + def destroy(self): + pass + + def action_performed(self, binding): + if binding.action in [ VOLUME_UP, VOLUME_DOWN, MUTE ]: + vol_mixer = self._open_mixer() + try : + if binding.action == MUTE: + # Handle mute + mute = False + mutes = None + try : + mutes = vol_mixer.getmute() + except alsaaudio.ALSAAudioError as e: + logger.debug("Could not get mute channel. Trying PCM mixer", exc_info = e) + if vol_mixer is not None: + vol_mixer.close() + # Some pulse weirdness maybe? + vol_mixer = self._open_mixer("PCM", self.current_card_index) + try : + mutes = vol_mixer.getmute() + except alsaaudio.ALSAAudioError as e: + logger.warning("No mute switch found", exc_info = e) + if mutes != None: + for ch_mute in mutes: + if ch_mute: + mute = True + vol_mixer.setmute(1 if not mute else 0) + else: + volumes = vol_mixer.getvolume() + total = 0 + for vol in volumes: + total += vol + volume = total / len(volumes) + + if binding.action == VOLUME_UP and volume < 100: + volume += 10 + vol_mixer.setvolume(min(volume, 100)) + elif binding.action == VOLUME_DOWN and volume > 0: + volume -= 10 + vol_mixer.setvolume(max(volume, 0)) + + finally : + if vol_mixer is not None: + vol_mixer.close() + + + + ''' Functions specific to plugin + ''' + def _start_monitoring(self): + self._volthread = VolumeThread(self) + self._volthread.start() + + def _config_changed(self, client, connection_id, entry, args): + ''' + If the user changes the soundcard on the preferences dialog this method + would be called two times. A first time for the soundcard change, and a + second time because the first mixer of the newly selected soundcard is + automatically selected. + The volume monitoring would then be restarted twice, which makes no sense. + Instead of restarting the monitoring as soon as this method is called, + we put it as a task on a queue for 1 second. If during that time, any + other change happens to the configuration, the previous restart request + is cancelled, and another one takes it's place. + This way, the monitoring is only restarted once when the user selects another + sound card. + ''' + if self._reload_config_timer is not None: + if not self._reload_config_timer.is_complete(): + self._reload_config_timer.cancel() + self._reload_config_timer = None + + self._reload_config_timer = g15scheduler.queue('VolumeMonitorQueue', + 'RestartVolumeMonitoring', + 1.0, + self._restart_monitoring) + + def _restart_monitoring(self): + self._stop_monitoring() + self._read_config() + self._start_monitoring() + + def _read_config(self): + self.soundcard_name = g15gconf.get_string_or_default(self._gconf_client, \ + self._gconf_key + "/soundcard", \ + str(alsaaudio.cards()[0])) + self.soundcard_index = alsaaudio.cards().index(self.soundcard_name) + + self.mixer_name = g15gconf.get_string_or_default(self._gconf_client, \ + self._gconf_key + "/mixer", \ + str(alsaaudio.mixers(self.soundcard_index)[0])) + if not self.mixer_name in alsaaudio.mixers(self.soundcard_index): + self.mixer_name = str(alsaaudio.mixers(self.soundcard_index)[0]) + self._gconf_client.set_string(self._gconf_key + "/mixer", self.mixer_name) + + + def _stop_monitoring(self): + if self._volthread != None: + self._volthread._stop_monitoring() + self._volthread.join(1.0) + + def _get_theme_properties(self): + properties = {} + icon = "audio-volume-muted" + if not self._mute: + if self._volume < 34: + icon = "audio-volume-low" + elif self._volume < 67: + icon = "audio-volume-medium" + else: + icon = "audio-volume-high" + else: + properties [ "muted"] = True + icon_path = g15icontools.get_icon_path(icon, self._screen.driver.get_size()[0]) + properties["state"] = icon + properties["icon"] = icon_path + properties["vol_pc"] = self._volume + for i in range(0, int( self._volume / 10 ) + 1, 1): + properties["bar" + str(i)] = True + return properties + + def _release_lights(self): + if self._light_controls is not None: + self._screen.driver.release_control(self._light_controls) + self._light_controls = None + + def _open_mixer(self, mixer_name = None): + mixer_name = self.mixer_name if mixer_name is None else mixer_name + if not mixer_name or mixer_name == "": + mixer_name = "Master" + + logger.info("Opening soundcard %s mixer %s", self.soundcard_name, mixer_name) + + vol_mixer = alsaaudio.Mixer(mixer_name, cardindex=self.soundcard_index) + return vol_mixer + + def _popup(self): + if not self._activated: + logger.warning("Cannot popup volume when it is deactivated. This suggests the volume thread has not died.") + return + + if not self._light_controls: + self._light_controls = self._screen.driver.acquire_control_with_hint(g15driver.HINT_MKEYS) + if self._lights_timer is not None: + self._lights_timer.cancel() + if self._light_controls is not None: + self._lights_timer = g15scheduler.schedule("ReleaseMKeyLights", 3.0, self._release_lights) + + page = self._screen.get_page(id) + if page == None: + if self._screen.driver.get_bpp() != 0: + page = g15theme.G15Page(id, self._screen, priority=g15screen.PRI_HIGH, title="Volume", theme = g15theme.G15Theme(self), \ + theme_properties_callback = self._get_theme_properties, + originating_plugin = self) + self._screen.delete_after(3.0, page) + self._screen.add_page(page) + else: + self._screen.raise_page(page) + self._screen.delete_after(3.0, page) + + + vol_mixer = self._open_mixer() + mute_mixer = None + + try : + + # Handle mute + mute = False + mutes = None + try : + mutes = vol_mixer.getmute() + except alsaaudio.ALSAAudioError as e: + logger.debug("Could note get mute channel. Trying PCM", exc_info = e) + # Some pulse weirdness maybe? + mute_mixer = alsaaudio.Mixer("PCM", cardindex=self.soundcard_index) + try : + mutes = mute_mixer.getmute() + except alsaaudio.ALSAAudioError as e: + logger.warning("No mute switch found", exc_info = e) + if mutes != None: + for ch_mute in mutes: + if ch_mute: + mute = True + + + # TODO better way than averaging + volumes = vol_mixer.getvolume() + finally : + vol_mixer.close() + if mute_mixer: + mute_mixer.close() + + total = 0 + for vol in volumes: + total += vol + volume = total / len(volumes) + + self._volume = volume + + if self._light_controls is not None: + if self._volume > 90: + self._light_controls.set_value(g15driver.MKEY_LIGHT_MR | g15driver.MKEY_LIGHT_1 | g15driver.MKEY_LIGHT_2 | g15driver.MKEY_LIGHT_3) + elif self._volume > 75: + self._light_controls.set_value(g15driver.MKEY_LIGHT_1 | g15driver.MKEY_LIGHT_2 | g15driver.MKEY_LIGHT_3) + elif self._volume > 50: + self._light_controls.set_value(g15driver.MKEY_LIGHT_1 | g15driver.MKEY_LIGHT_2) + elif self._volume > 25: + self._light_controls.set_value(g15driver.MKEY_LIGHT_1) + else: + self._light_controls.set_value(0) + + self._mute = mute + + self._screen.redraw(page) + +class VolumeThread(Thread): + def __init__(self, volume): + Thread.__init__(self) + self.name = "VolumeThread" + self.setDaemon(True) + self._volume = volume + + logger.info("Opening soundcard %s mixer %s", volume.soundcard_name, volume.mixer_name) + + self._mixer = alsaaudio.Mixer(volume.mixer_name, cardindex=volume.soundcard_index) + self._poll_desc = self._mixer.polldescriptors() + self._poll = select.poll() + self._fd = self._poll_desc[0][0] + self._event_mask = self._poll_desc[0][1] + self._open = os.fdopen(self._fd) + self._poll.register(self._open, select.POLLIN) + self._stop = False + + def _stop_monitoring(self): + self._stop = True + self._open.close() + self._mixer.close() + + def run(self): + try : + while not self._stop: + if self._poll.poll(5): + if self._stop: + break + g15scheduler.schedule("popupVolume", 0, self._volume._popup) + if not self._open.read(): + break + finally: + try : + self._poll.unregister(self._open) + except Exception as e: + logger.debug("Error when unregistering", exc_info = e) + pass + self._open.close() diff --git a/src/plugins/volume/volume.ui b/src/plugins/volume/volume.ui new file mode 100644 index 0000000..58e9b76 --- /dev/null +++ b/src/plugins/volume/volume.ui @@ -0,0 +1,179 @@ + + + + + + + + + + + + + + + + + + 320 + False + 5 + Volume Preferences + False + True + center-on-parent + dialog + + + True + False + 2 + + + True + False + end + + + gtk-close + True + True + True + True + + + False + False + 0 + + + + + False + True + end + 0 + + + + + True + False + + + True + False + 0 + none + + + True + False + 12 + + + True + False + + + True + False + SoundcardModel + + + + 0 + + + + + True + True + 0 + + + + + + + + + True + False + <b>Soundcard</b> + True + + + + + True + False + 0 + + + + + True + False + 0 + none + + + True + False + 12 + + + True + False + + + True + False + MixerModel + + + + 0 + + + + + True + True + 0 + + + + + + + + + True + False + <b>Mixer</b> + True + + + + + True + True + 1 + + + + + False + False + 1 + + + + + + button9 + + + diff --git a/src/plugins/weather-noaa/Makefile.am b/src/plugins/weather-noaa/Makefile.am new file mode 100644 index 0000000..a573f5c --- /dev/null +++ b/src/plugins/weather-noaa/Makefile.am @@ -0,0 +1,7 @@ +plugindir = $(datadir)/gnome15/plugins/weather-noaa +plugin_DATA = weather-noaa.py \ + weather-noaa.ui \ + icon.png + +EXTRA_DIST = \ + $(plugin_DATA) \ No newline at end of file diff --git a/src/plugins/weather-noaa/icon.png b/src/plugins/weather-noaa/icon.png new file mode 100644 index 0000000..16a665a Binary files /dev/null and b/src/plugins/weather-noaa/icon.png differ diff --git a/src/plugins/weather-noaa/weather-noaa.py b/src/plugins/weather-noaa/weather-noaa.py new file mode 100644 index 0000000..bc41f73 --- /dev/null +++ b/src/plugins/weather-noaa/weather-noaa.py @@ -0,0 +1,156 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2012 Brett Smith +# +# 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 . + +import gnome15.g15locale as g15locale +_ = g15locale.get_translation("weather-noaa", modfile = __file__).ugettext + +import gnome15.g15accounts as g15accounts +import gnome15.g15globals as g15globals +import gnome15.util.g15uigconf as g15uigconf +import gnome15.util.g15pythonlang as g15pythonlang +import gnome15.util.g15gconf as g15gconf +import weather +import gtk +import os +import pywapi +import email.utils +import time +import datetime + +# Logging +import logging +logger = logging.getLogger(__name__) + +""" +Plugin definition +""" +backend_name="NOAA" +id="weather-noaa" +name=_("Weather (NOAA support)") +description=_("Adds the National Oceanic and Atmospheric Administration as a source of weather data.") +author="Brett Smith " +copyright=_("Copyright (C)2012 Brett Smith") +site="http://www.russo79.com/gnome15" +has_preferences=False +passive=True +needs_network=True +global_plugin=True +requires="weather" +unsupported_models=weather.unsupported_models + +""" +Weather Back-end module functions +""" +def create_options(gconf_client, gconf_key): + return NOAAWeatherOptions(gconf_client, gconf_key) + +def create_backend(gconf_client, gconf_key): + return NOAAWeatherBackend(gconf_client, gconf_key) + +class NOAAWeatherOptions(weather.WeatherOptions): + def __init__(self, gconf_client, gconf_key): + weather.WeatherOptions.__init__(self) + + self.widget_tree = gtk.Builder() + self.widget_tree.add_from_file(os.path.join(os.path.dirname(__file__), "weather-noaa.ui")) + self.component = self.widget_tree.get_object("OptionPanel") + + g15uigconf.configure_text_from_gconf(gconf_client, "%s/station_id" % gconf_key, "StationID", "KPEO", self.widget_tree) + +class NOAAWeatherData(weather.WeatherData): + + def __init__(self, station_id): + weather.WeatherData.__init__(self, station_id) + +class NOAAWeatherBackend(weather.WeatherBackend): + + def __init__(self, gconf_client, gconf_key): + weather.WeatherBackend.__init__(self, gconf_client, gconf_key) + + def get_weather_data(self): + station_id = g15gconf.get_string_or_default(self.gconf_client, "%s/station_id" % self.gconf_key, "KPEO") + p = pywapi.get_weather_from_noaa(station_id) + + tm = email.utils.parsedate_tz(p["observation_time_rfc822"])[:9] + data = { + "location" : p["location"], + "datetime" : datetime.datetime.fromtimestamp(time.mktime(tm)), + "current_conditions" : { + "wind_speed" : g15pythonlang.to_int_or_none(weather.mph_to_kph(float(p["wind_mph"]))) if "wind_mph" in p else None, + "wind_direction" : g15pythonlang.to_int_or_none(p["wind_degrees"]) if "wind_degrees" in p else None, + "pressure" : p["pressure_mb"] if "pressure_mb" in p else None, + "humidity" : p["relative_humidity"] if "relative_humidity" in p else None, + "condition" : p["weather"] if "weather" in p else None, + "temp_c" : p["temp_c"] if "temp_c" in p else None, + "icon" : self._get_icon(p["icon_url_name"]) if "icon_url_name" in p else None, + "fallback_icon" : "http://w1.weather.gov/images/fcicons/%s" % ( "%s.jpg" % os.path.splitext(p["icon_url_name"])[0] ) if "icon_url_name" in p else None + } + } + + return data + + def _get_icon(self, icon): + night = False + icon_name = icon + if icon.startswith("n"): + icon_name = icon_name[1:] + night = True + elif icon.startswith("hi_n"): + icon_name = "hi_" + icon_name[3:] + night = True + + name, extension = os.path.splitext(icon_name) + theme_icon = None + if name in [ "bkn" ]: + # Mostly Cloudy | Mostly Cloudy with Haze | Mostly Cloudy and Breezy + theme_icon = "weather-overcast" + elif name in [ "skc" ]: + # Fair | Clear | Fair with Haze | Clear with Haze | Fair and Breezy | Clear and Breezy + theme_icon = "weather-clear" + elif name in [ "few" ]: + # A Few Clouds | A Few Clouds with Haze | A Few Clouds and Breezy + theme_icon = "weather-few-clouds" + elif name in [ "sct" ]: + # Partly Cloudy | Partly Cloudy with Haze | Partly Cloudy and Breezy + theme_icon = "weather-clouds" + elif name in [ "ovc" ]: + # Overcast | Overcast with Haze | Overcast and Breezy + theme_icon = "weather-overcast" + elif name in [ "fg" ]: + # Fog/Mist | Fog | Freezing Fog | Shallow Fog | Partial Fog | Patches of Fog | Fog in Vicinity | Freezing Fog in Vicinity | Shallow Fog in Vicinity | Partial Fog in Vicinity | Patches of Fog in Vicinity | Showers in Vicinity Fog | Light Freezing Fog | Heavy Freezing Fog + theme_icon = "weather-fog" + elif name in [ "shra", "hi_shwrs", "ra1", "ra" ]: + # Rain Showers | Light Rain Showers | Light Rain and Breezy | Heavy Rain Showers | Rain Showers in Vicinity | Light Showers Rain | Heavy Showers Rain | Showers Rain | Showers Rain in Vicinity | Rain Showers Fog/Mist | Light Rain Showers Fog/Mist | Heavy Rain Showers Fog/Mist | Rain Showers in Vicinity Fog/Mist | Light Showers Rain Fog/Mist | Heavy Showers Rain Fog/Mist | Showers Rain Fog/Mist | Showers Rain in Vicinity Fog/Mist + theme_icon = "weather-showers" + elif name in [ "tsra" ]: + # Rain Showers | Light Rain Showers | Light Rain and Breezy | Heavy Rain Showers | Rain Showers in Vicinity | Light Showers Rain | Heavy Showers Rain | Showers Rain | Showers Rain in Vicinity | Rain Showers Fog/Mist | Light Rain Showers Fog/Mist | Heavy Rain Showers Fog/Mist | Rain Showers in Vicinity Fog/Mist | Light Showers Rain Fog/Mist | Heavy Showers Rain Fog/Mist | Showers Rain Fog/Mist | Showers Rain in Vicinity Fog/Mist + theme_icon = "weather-storm" + elif name in [ "sn" ]: + # Snow | Light Snow | Heavy Snow | Snow Showers | Light Snow Showers | Heavy Snow Showers | Showers Snow | Light Showers Snow | Heavy Showers Snow | Snow Fog/Mist | Light Snow Fog/Mist | Heavy Snow Fog/Mist | Snow Showers Fog/Mist | Light Snow Showers Fog/Mist | Heavy Snow Showers Fog/Mist | Showers Snow Fog/Mist | Light Showers Snow Fog/Mist | Heavy Showers Snow Fog/Mist | Snow Fog | Light Snow Fog | Heavy Snow Fog | Snow Showers Fog | Light Snow Showers Fog | Heavy Snow Showers Fog | Showers Snow Fog | Light Showers Snow Fog | Heavy Showers Snow Fog | Showers in Vicinity Snow | Snow Showers in Vicinity | Snow Showers in Vicinity Fog/Mist | Snow Showers in Vicinity Fog | Low Drifting Snow | Blowing Snow | Snow Low Drifting Snow | Snow Blowing Snow | Light Snow Low Drifting Snow | Light Snow Blowing Snow | Light Snow Blowing Snow Fog/Mist | Heavy Snow Low Drifting Snow | Heavy Snow Blowing Snow | Thunderstorm Snow | Light Thunderstorm Snow | Heavy Thunderstorm Snow | Snow Grains | Light Snow Grains | Heavy Snow Grains | Heavy Blowing Snow | Blowing Snow in Vicinity + theme_icon = "weather-snow" + elif name in [ "svrtsra" ]: + # Funnel Cloud | Funnel Cloud in Vicinity | Tornado/Water Spout + theme_icon = "weather-severe-alert" + + if theme_icon is not None and night: + theme_icon = "%s-night" % theme_icon + + if theme_icon is None: + # Fallback to using actual image + theme_icon = "http://w1.weather.gov/images/fcicons/%s.jpg" % name + + return theme_icon + diff --git a/src/plugins/weather-noaa/weather-noaa.ui b/src/plugins/weather-noaa/weather-noaa.ui new file mode 100644 index 0000000..0f35fdc --- /dev/null +++ b/src/plugins/weather-noaa/weather-noaa.ui @@ -0,0 +1,96 @@ + + + + + + False + + + True + False + + + True + False + 3 + 2 + 8 + 8 + + + True + False + 0 + Station ID: + + + 1 + 2 + GTK_FILL + + + + + + True + True + + False + False + True + True + + + 1 + 2 + 1 + 2 + GTK_FILL + + + + + + True + False + 0 + To use the NOAA feeds, you must know your station ID. +You can find your station ID by visiting the NOAA site using +the button below. The ID is usually at least 4 character code. + + + 2 + + + + + Visit NOAA for Stationd ID + True + True + True + True + none + http://w1.weather.gov/xml/current_obs/ + + + 2 + 2 + 3 + GTK_EXPAND + + + + + False + False + 8 + 0 + + + + + + + + + diff --git a/src/plugins/weather-yahoo/Makefile.am b/src/plugins/weather-yahoo/Makefile.am new file mode 100644 index 0000000..258b57e --- /dev/null +++ b/src/plugins/weather-yahoo/Makefile.am @@ -0,0 +1,7 @@ +plugindir = $(datadir)/gnome15/plugins/weather-yahoo +plugin_DATA = weather-yahoo.py \ + weather-yahoo.ui \ + icon.png + +EXTRA_DIST = \ + $(plugin_DATA) \ No newline at end of file diff --git a/src/plugins/weather-yahoo/icon.png b/src/plugins/weather-yahoo/icon.png new file mode 100644 index 0000000..16a665a Binary files /dev/null and b/src/plugins/weather-yahoo/icon.png differ diff --git a/src/plugins/weather-yahoo/weather-yahoo.py b/src/plugins/weather-yahoo/weather-yahoo.py new file mode 100644 index 0000000..132724b --- /dev/null +++ b/src/plugins/weather-yahoo/weather-yahoo.py @@ -0,0 +1,383 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2012 Brett Smith +# +# 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 . +# +# Based on bits of pywapi :- +# +#Copyright (c) 2009 Eugene Kaznacheev + +#Permission is hereby granted, free of charge, to any person +#obtaining a copy of this software and associated documentation +#files (the "Software"), to deal in the Software without +#restriction, including without limitation the rights to use, +#copy, modify, merge, publish, distribute, sublicense, and/or sell +#copies of the Software, and to permit persons to whom the +#Software is furnished to do so, subject to the following +#conditions: + +#The above copyright notice and this permission notice shall be +#included in all copies or substantial portions of the Software. + +#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +#EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +#OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +#NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +#HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +#WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +#FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +#OTHER DEALINGS IN THE SOFTWARE. + +import gnome15.g15locale as g15locale +_ = g15locale.get_translation("weather-yahoo", modfile = __file__).ugettext + +import gnome15.g15accounts as g15accounts +import gnome15.g15globals as g15globals +import gnome15.util.g15uigconf as g15uigconf +import gnome15.util.g15pythonlang as g15pythonlang +import gnome15.util.g15gconf as g15gconf +import weather +import gtk +import os +import datetime +import urllib2, re +import json +from xml.dom import minidom +from urllib import quote +import time + +#select * from xml where url="http://weather.yahooapis.com/forecastrss?w=26350898" + +YAHOO_WEATHER_URL = 'http://xml.weather.yahoo.com/forecastrss?w=%s&u=%s&d=5' +YAHOO_WEATHER_URL_JSON = 'http://query.yahooapis.com/v1/public/yql?q=select%20item%20from%20weather.forecast%20where%20location%3D%2248907%22&format=json' +YAHOO_WEATHER_NS = 'http://xml.weather.yahoo.com/ns/rss/1.0' + +# Logging +import logging +logger = logging.getLogger(__name__) + +""" +Plugin definition +""" +backend_name="Yahoo" +id="weather-yahoo" +name=_("Weather (Yahoo support)") +description=_("Adds Yahoo as a source of weather data.") +author="Brett Smith " +copyright=_("Copyright (C)2012 Brett Smith") +site="http://www.russo79.com/gnome15" +has_preferences=False +passive=True +needs_network=True +global_plugin=True +requires="weather" +unsupported_models=weather.unsupported_models + +""" +Weather Back-end module functions +""" +def create_options(gconf_client, gconf_key): + return YahooWeatherOptions(gconf_client, gconf_key) + +def create_backend(gconf_client, gconf_key): + return YahooWeatherBackend(gconf_client, gconf_key) + +""" +Utilities for parsing +""" + +def xml_get_ns_yahoo_tag(dom, ns, tag, attrs): + """ + Parses the necessary tag and returns the dictionary with values + + Parameters: + dom - DOM + ns - namespace + tag - necessary tag + attrs - tuple of attributes + + Returns: a dictionary of elements + """ + element = dom.getElementsByTagNameNS(ns, tag)[0] + return xml_get_attrs(element,attrs) + + +def xml_get_attrs(xml_element, attrs): + """ + Returns the list of necessary attributes + + Parameters: + element: xml element + attrs: tuple of attributes + + Return: a dictionary of elements + """ + + result = {} + for attr in attrs: + result[attr] = xml_element.getAttribute(attr) + return result + + +class YahooWeatherOptions(weather.WeatherOptions): + def __init__(self, gconf_client, gconf_key): + weather.WeatherOptions.__init__(self) + + self.widget_tree = gtk.Builder() + self.widget_tree.add_from_file(os.path.join(os.path.dirname(__file__), "weather-yahoo.ui")) + self.component = self.widget_tree.get_object("OptionPanel") + + g15uigconf.configure_text_from_gconf(gconf_client, "%s/location_id" % gconf_key, "LocationID", "", self.widget_tree) + +class YahooWeatherData(weather.WeatherData): + + def __init__(self, station_id): + weather.WeatherData.__init__(self, station_id) + +class YahooWeatherBackend(weather.WeatherBackend): + + def __init__(self, gconf_client, gconf_key): + weather.WeatherBackend.__init__(self, gconf_client, gconf_key) + + def get_weather_data(self): + return self._do_get_weather_data_xml() + + def _do_get_weather_data_json(self): + location_id = quote(location_id) + if units == 'metric': + unit = 'c' + else: + unit = 'f' + url = YAHOO_WEATHER_URL_JSON % (location_id, unit) + handler = urllib2.urlopen(url) + jobj = json.load(handler) + handler.close() + + def _do_get_weather_data_xml(self): + location_id = g15gconf.get_string_or_default(self.gconf_client, "%s/location_id" % self.gconf_key, "2487956") + p = self._get_weather_from_yahoo(location_id) + if p is None: + return None + + # Get location + location_el = p["location"] + location = g15pythonlang.append_if_exists(location_el, "city", "") + location = g15pythonlang.append_if_exists(location_el, "region", location) + location = g15pythonlang.append_if_exists(location_el, "country", location) + + # Get current condition + condition_el = p["condition"] + wind_el = p["wind"] if "wind" in p else None + + # Observed date + try: + observed_datetime = datetime.datetime.strptime(condition_el["date"], "%a, %d %b %Y %H:%M %p %Z") + except ValueError as v: + logger.debug("Error parsing date, trying alternative method.", exc_info = v) + import email.utils + dxt = email.utils.parsedate_tz(condition_el["date"]) + class TZ(datetime.tzinfo): + def dst(self, dt): + return datetime.timedelta(0) + + def tzname(self, dt): + return dxt[9] + + def utcoffset(self, dt): return datetime.timedelta(seconds=dxt[9]) + observed_datetime = datetime.datetime(*dxt[:7], tzinfo=TZ()) + + # Forecasts (we only get 2 from yahoo) + forecasts_el = p["forecasts"] + forecasts = [] + today_low = None + today_high = None + for f in forecasts_el: + condition_code = g15pythonlang.to_int_or_none(f["code"]) + high = g15pythonlang.to_float_or_none(f["high"]) + low = g15pythonlang.to_float_or_none(f["low"]) + if today_low is None: + today_low = low + today_high = high + forecasts.append({ + "condition" : f["text"], + "high" : high, + "low" : low, + "day_of_week" : f["day"], + "icon" : self._translate_icon(condition_code), + "fallback_icon" : "http://l.yimg.com/a/i/us/we/52/%s.gif" % condition_code + }) + + # Sunset and sunrise + sunset = None + sunrise = None + if "astronomy" in p: + astronomy = p["astronomy"] + if "sunset" in astronomy: + sunset = g15locale.parse_US_time_or_none(astronomy["sunset"]) + if "sunrise" in astronomy: + sunrise = g15locale.parse_US_time_or_none(astronomy["sunrise"]) + + # Pressure, Visibility and Humidity + pressure = None + if "atmosphere" in p: + atmosphere = p["atmosphere"] + if "pressure" in atmosphere: + pressure = g15pythonlang.to_float_or_none(atmosphere["pressure"]) + if "visibility" in atmosphere: + visibility = g15pythonlang.to_float_or_none(atmosphere["visibility"]) + if "humidity" in atmosphere: + humidity = g15pythonlang.to_float_or_none(atmosphere["humidity"]) + + # Build data structure + condition_code = g15pythonlang.to_int_or_none(condition_el["code"]) + data = { + "location" : location, + "forecasts" : forecasts, + "datetime": observed_datetime, + "current_conditions" : { + "wind_chill": wind_el["chill"] if wind_el is not None and "chill" in wind_el else None, + "wind_direction": wind_el["direction"] if wind_el is not None and "direction" in wind_el else None, + "wind_speed": wind_el["speed"] if wind_el is not None and "speed" in wind_el else None, + "condition" : condition_el["text"], + "sunset" : sunset, + "sunrise" : sunrise, + "pressure" : pressure, + "visibility" : visibility, + "humidity" : humidity, + "low" : today_low, + "high" : today_high, + "temp_c" : g15pythonlang.to_float_or_none(condition_el["temp"]), + "icon" : self._translate_icon(condition_code), + "fallback_icon" : "http://l.yimg.com/a/i/us/we/52/%s.gif" % condition_code if condition_code is not None else None + } + } + + return data + + def _translate_icon(self, code): + + theme_icon = None + if code in [ 0, 1, 2, 3, 4 ]: + theme_icon = "weather-severe-alert" + elif code in [ 8, 9, 10, 11, 12, 35 ]: + theme_icon = "weather-showers" + elif code in [ 5, 6, 7, 13, 14, 15, 16, 41, 42, 43, 46 ]: + theme_icon = "weather-snow" + elif code in [ 20 ]: + theme_icon = "weather-fog" + elif code in [ 37, 38, 39, 45, 47 ]: + theme_icon = "weather-storm" + elif code in [ 40 ]: + theme_icon = "weather-showers-scattered" + elif code in [ 31 ]: + theme_icon = "weather-clear-night" + elif code in [ 30,44 ]: + theme_icon = "weather-few-clouds" + elif code in [ 29 ]: + theme_icon = "weather-few-clouds-night" + elif code in [ 28, 26 ]: + theme_icon = "weather-overcast" + elif code in [ 27 ]: + theme_icon = "weather-overcast-night" + elif code in [ 34, 21 ]: + theme_icon = "weather-clouds" + elif code in [ 33 ]: + theme_icon = "weather-clouds-night" + elif code in [ 32, 36 ]: + theme_icon = "weather-clear" + + + if theme_icon is None: + # Fallback to using image extracted from data + theme_icon = "http://l.yimg.com/a/i/us/we/52/%s.gif" % code + + """ + The following will always use yahoo images + + + + + + + + + + """ + + return theme_icon + + + def _get_weather_from_yahoo(self, location_id, units = 'metric'): + """ + Fetches weather report from Yahoo! + + Parameters + location_id: A five digit US zip code or location ID. To find your location ID, + browse or search for your city from the Weather home page(http://weather.yahoo.com/) + The weather ID is in the URL for the forecast page for that city. You can also get the location ID by entering your zip code on the home page. For example, if you search for Los Angeles on the Weather home page, the forecast page for that city is http://weather.yahoo.com/forecast/USCA0638.html. The location ID is USCA0638. + + units: type of units. 'metric' for metric and '' for non-metric + Note that choosing metric units changes all the weather units to metric, for example, wind speed will be reported as kilometers per hour and barometric pressure as millibars. + + Returns: + weather_data: a dictionary of weather data that exists in XML feed. See http://developer.yahoo.com/weather/#channel + """ + location_id = quote(location_id) + if units == 'metric': + unit = 'c' + else: + unit = 'f' + url = YAHOO_WEATHER_URL % (location_id, unit) + handler = urllib2.urlopen(url) + dom = minidom.parse(handler) + handler.close() + + weather_data = {} + weather_data['title'] = dom.getElementsByTagName('title')[0].firstChild.data + linkel = dom.getElementsByTagName('link') + + if len(linkel) < 1: + return None + + weather_data['link'] = linkel[0].firstChild.data + + ns_data_structure = { + 'location': ('city', 'region', 'country'), + 'units': ('temperature', 'distance', 'pressure', 'speed'), + 'wind': ('chill', 'direction', 'speed'), + 'atmosphere': ('humidity', 'visibility', 'pressure', 'rising'), + 'astronomy': ('sunrise', 'sunset'), + 'condition': ('text', 'code', 'temp', 'date', 'day') + } + + for (tag, attrs) in ns_data_structure.iteritems(): + weather_data[tag] = xml_get_ns_yahoo_tag(dom, YAHOO_WEATHER_NS, tag, attrs) + + weather_data['geo'] = {} + weather_data['geo']['lat'] = dom.getElementsByTagName('geo:lat')[0].firstChild.data + weather_data['geo']['long'] = dom.getElementsByTagName('geo:long')[0].firstChild.data + + weather_data['condition']['title'] = dom.getElementsByTagName('item')[0].getElementsByTagName('title')[0].firstChild.data + weather_data['html_description'] = dom.getElementsByTagName('item')[0].getElementsByTagName('description')[0].firstChild.data + + forecasts = [] + for forecast in dom.getElementsByTagNameNS(YAHOO_WEATHER_NS, 'forecast'): + forecasts.append(xml_get_attrs(forecast,('date', 'low', 'high', 'text', 'code', 'day'))) + weather_data['forecasts'] = forecasts + + dom.unlink() + + return weather_data + + diff --git a/src/plugins/weather-yahoo/weather-yahoo.ui b/src/plugins/weather-yahoo/weather-yahoo.ui new file mode 100644 index 0000000..d612ae4 --- /dev/null +++ b/src/plugins/weather-yahoo/weather-yahoo.ui @@ -0,0 +1,105 @@ + + + + + + False + + + True + False + + + True + False + 3 + 2 + 8 + 8 + + + True + False + 0 + Location ID: + + + 1 + 2 + GTK_FILL + + + + + + True + True + + False + False + True + True + + + 1 + 2 + 1 + 2 + GTK_FILL + + + + + + True + False + 0 + A five digit US zip code or location ID. To find your location ID, +browse or search for your city from the Weather home page. +The weather ID is in the URL for the forecast page for that city. +You can also get the location ID by entering your zip code on +the home page. + +For example, if you search for San Francisco on the Weather +home page, the forecast page for that city is :- + +../united-states/california/san-francisco-2487956, +this means the location ID is 2487956. + + + 2 + GTK_FILL + + + + + Visit Yahoo Weather For Location ID + True + True + True + True + none + http://weather.yahoo.com/ + + + 2 + 2 + 3 + + + + + + False + False + 8 + 0 + + + + + + + + + diff --git a/src/plugins/weather/Makefile.am b/src/plugins/weather/Makefile.am new file mode 100644 index 0000000..28dbcda --- /dev/null +++ b/src/plugins/weather/Makefile.am @@ -0,0 +1,8 @@ +SUBDIRS = default forecasts +plugindir = $(datadir)/gnome15/plugins/weather +plugin_DATA = weather.py \ + pywapi.py \ + weather.ui + +EXTRA_DIST = \ + $(plugin_DATA) \ No newline at end of file diff --git a/src/plugins/weather/default/Makefile.am b/src/plugins/weather/default/Makefile.am new file mode 100644 index 0000000..c26d99d --- /dev/null +++ b/src/plugins/weather/default/Makefile.am @@ -0,0 +1,16 @@ +themedir = $(datadir)/gnome15/plugins/weather/default +theme_DATA = g19.svg \ + default.svg \ + mx5500.svg \ + mono-clouds.gif \ + mono-dark-clouds.gif \ + mono-few-clouds.gif \ + mono-fog.gif \ + mono-more-clouds.gif \ + mono-rain.gif \ + mono-snow.gif \ + mono-sunny.gif \ + mono-thunder.gif + +EXTRA_DIST = \ + $(theme_DATA) diff --git a/src/plugins/weather/default/default.svg b/src/plugins/weather/default/default.svg new file mode 100644 index 0000000..50d4587 --- /dev/null +++ b/src/plugins/weather/default/default.svg @@ -0,0 +1,248 @@ + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + ${temp} + ${condition} + Hum:${humidity} + ${pressure} + Wnd:${wind} + Vis:${visibility} + S${sunset_time} + R${sunrise_time} + + + + ${message} + + diff --git a/src/plugins/weather/default/g19.svg b/src/plugins/weather/default/g19.svg new file mode 100644 index 0000000..6968ef2 --- /dev/null +++ b/src/plugins/weather/default/g19.svg @@ -0,0 +1,331 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + ${condition} + ${temp} + ${lo} Low + ${hi} High + Wind: + ${humidity} + ${pressure} + ${sunrise_time} + ${location} @ ${time} + ${wind} + Humidity: + Pressure: + Sunrise at + ${sunset_time} + Sunset at + ${visibility} + Visibility: + + + ${message} + + diff --git a/src/plugins/weather/default/mono-clouds.gif b/src/plugins/weather/default/mono-clouds.gif new file mode 100644 index 0000000..6dce86a Binary files /dev/null and b/src/plugins/weather/default/mono-clouds.gif differ diff --git a/src/plugins/weather/default/mono-dark-clouds.gif b/src/plugins/weather/default/mono-dark-clouds.gif new file mode 100644 index 0000000..f657814 Binary files /dev/null and b/src/plugins/weather/default/mono-dark-clouds.gif differ diff --git a/src/plugins/weather/default/mono-few-clouds.gif b/src/plugins/weather/default/mono-few-clouds.gif new file mode 100644 index 0000000..d2e5997 Binary files /dev/null and b/src/plugins/weather/default/mono-few-clouds.gif differ diff --git a/src/plugins/weather/default/mono-fog.gif b/src/plugins/weather/default/mono-fog.gif new file mode 100644 index 0000000..140fe98 Binary files /dev/null and b/src/plugins/weather/default/mono-fog.gif differ diff --git a/src/plugins/weather/default/mono-more-clouds.gif b/src/plugins/weather/default/mono-more-clouds.gif new file mode 100644 index 0000000..662a79c Binary files /dev/null and b/src/plugins/weather/default/mono-more-clouds.gif differ diff --git a/src/plugins/weather/default/mono-rain.gif b/src/plugins/weather/default/mono-rain.gif new file mode 100644 index 0000000..e37c1f1 Binary files /dev/null and b/src/plugins/weather/default/mono-rain.gif differ diff --git a/src/plugins/weather/default/mono-snow.gif b/src/plugins/weather/default/mono-snow.gif new file mode 100644 index 0000000..feaaed0 Binary files /dev/null and b/src/plugins/weather/default/mono-snow.gif differ diff --git a/src/plugins/weather/default/mono-sunny.gif b/src/plugins/weather/default/mono-sunny.gif new file mode 100644 index 0000000..7b88ca4 Binary files /dev/null and b/src/plugins/weather/default/mono-sunny.gif differ diff --git a/src/plugins/weather/default/mono-thunder.gif b/src/plugins/weather/default/mono-thunder.gif new file mode 100644 index 0000000..a452327 Binary files /dev/null and b/src/plugins/weather/default/mono-thunder.gif differ diff --git a/src/plugins/weather/default/mx5500.svg b/src/plugins/weather/default/mx5500.svg new file mode 100644 index 0000000..9f8ff35 --- /dev/null +++ b/src/plugins/weather/default/mx5500.svg @@ -0,0 +1,290 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + ${day_letter1} + ${day_letter2} + ${day_letter3} + ${day_letter4} + + + ${condition1} + ${condition2} + ${condition3} + ${condition4} + + + ${hi1} + ${hi2} + ${hi3} + ${hi4} + + ${temp} + ${condition} + + + ${message} + + diff --git a/src/plugins/weather/forecasts/Makefile.am b/src/plugins/weather/forecasts/Makefile.am new file mode 100644 index 0000000..a1e30fa --- /dev/null +++ b/src/plugins/weather/forecasts/Makefile.am @@ -0,0 +1,17 @@ +themedir = $(datadir)/gnome15/plugins/weather/forecasts +theme_DATA = g19.svg \ + default.svg \ + mx5500.svg \ + mono-clouds.gif \ + mono-dark-clouds.gif \ + mono-few-clouds.gif \ + mono-fog.gif \ + mono-more-clouds.gif \ + mono-rain.gif \ + mono-snow.gif \ + mono-sunny.gif \ + mono-thunder.gif \ + forecasts.theme + +EXTRA_DIST = \ + $(theme_DATA) diff --git a/src/plugins/weather/forecasts/default.svg b/src/plugins/weather/forecasts/default.svg new file mode 100644 index 0000000..144a0c3 --- /dev/null +++ b/src/plugins/weather/forecasts/default.svg @@ -0,0 +1,241 @@ + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + ${day_letter1} + ${condition1} + ${hi1} + ${day_letter2} + ${condition2} + ${hi2} + ${temp} + ${condition} + ${wind} + ${humidity} ${pressure} ${visibility} + + + ${message} + + diff --git a/src/plugins/weather/forecasts/forecasts.theme b/src/plugins/weather/forecasts/forecasts.theme new file mode 100644 index 0000000..39869c5 --- /dev/null +++ b/src/plugins/weather/forecasts/forecasts.theme @@ -0,0 +1,4 @@ +[theme] +name=Forecasts +description=Displays a two day forecast when available +unsupported_models=g110,g11,mx5500,g930,g35 diff --git a/src/plugins/weather/forecasts/g19.svg b/src/plugins/weather/forecasts/g19.svg new file mode 100644 index 0000000..fd308ce --- /dev/null +++ b/src/plugins/weather/forecasts/g19.svg @@ -0,0 +1,378 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + ${condition} + ${temp} + ${lo} Low + ${hi} High + ${sunrise_time} + ${location} @ ${time} + Sunrise at + ${sunset_time} + Sunset at + + ${day1} + ${lo1} - ${hi1} + ${condition1} + + ${day2} + ${lo2} - ${hi2} + ${condition2} + + ${wind} + ${humidity} + ${pressure} + ${visibility} + + + ${message} + + diff --git a/src/plugins/weather/forecasts/mono-clouds.gif b/src/plugins/weather/forecasts/mono-clouds.gif new file mode 100644 index 0000000..6dce86a Binary files /dev/null and b/src/plugins/weather/forecasts/mono-clouds.gif differ diff --git a/src/plugins/weather/forecasts/mono-dark-clouds.gif b/src/plugins/weather/forecasts/mono-dark-clouds.gif new file mode 100644 index 0000000..f657814 Binary files /dev/null and b/src/plugins/weather/forecasts/mono-dark-clouds.gif differ diff --git a/src/plugins/weather/forecasts/mono-few-clouds.gif b/src/plugins/weather/forecasts/mono-few-clouds.gif new file mode 100644 index 0000000..d2e5997 Binary files /dev/null and b/src/plugins/weather/forecasts/mono-few-clouds.gif differ diff --git a/src/plugins/weather/forecasts/mono-fog.gif b/src/plugins/weather/forecasts/mono-fog.gif new file mode 100644 index 0000000..140fe98 Binary files /dev/null and b/src/plugins/weather/forecasts/mono-fog.gif differ diff --git a/src/plugins/weather/forecasts/mono-more-clouds.gif b/src/plugins/weather/forecasts/mono-more-clouds.gif new file mode 100644 index 0000000..662a79c Binary files /dev/null and b/src/plugins/weather/forecasts/mono-more-clouds.gif differ diff --git a/src/plugins/weather/forecasts/mono-rain.gif b/src/plugins/weather/forecasts/mono-rain.gif new file mode 100644 index 0000000..e37c1f1 Binary files /dev/null and b/src/plugins/weather/forecasts/mono-rain.gif differ diff --git a/src/plugins/weather/forecasts/mono-snow.gif b/src/plugins/weather/forecasts/mono-snow.gif new file mode 100644 index 0000000..feaaed0 Binary files /dev/null and b/src/plugins/weather/forecasts/mono-snow.gif differ diff --git a/src/plugins/weather/forecasts/mono-sunny.gif b/src/plugins/weather/forecasts/mono-sunny.gif new file mode 100644 index 0000000..7b88ca4 Binary files /dev/null and b/src/plugins/weather/forecasts/mono-sunny.gif differ diff --git a/src/plugins/weather/forecasts/mono-thunder.gif b/src/plugins/weather/forecasts/mono-thunder.gif new file mode 100644 index 0000000..a452327 Binary files /dev/null and b/src/plugins/weather/forecasts/mono-thunder.gif differ diff --git a/src/plugins/weather/forecasts/mx5500.svg b/src/plugins/weather/forecasts/mx5500.svg new file mode 100644 index 0000000..9f8ff35 --- /dev/null +++ b/src/plugins/weather/forecasts/mx5500.svg @@ -0,0 +1,290 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + ${day_letter1} + ${day_letter2} + ${day_letter3} + ${day_letter4} + + + ${condition1} + ${condition2} + ${condition3} + ${condition4} + + + ${hi1} + ${hi2} + ${hi3} + ${hi4} + + ${temp} + ${condition} + + + ${message} + + diff --git a/src/plugins/weather/i18n/weather.en_GB.po b/src/plugins/weather/i18n/weather.en_GB.po new file mode 100644 index 0000000..0bd5a8f --- /dev/null +++ b/src/plugins/weather/i18n/weather.en_GB.po @@ -0,0 +1,74 @@ +# English translations for gnome15-plugins package. +# Copyright (C) 2012 THE gnome15-plugins'S COPYRIGHT HOLDER +# This file is distributed under the same license as the gnome15-plugins package. +# Automatically generated, 2012. +# +msgid "" +msgstr "" +"Project-Id-Version: gnome15-plugins 0.8.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: 2012-01-08 11:23+0000\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: en_GB\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=ASCII\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: i18n/weather.glade.h:1 +msgid "Display" +msgstr "Display" + +#: i18n/weather.glade.h:2 +msgid "Location" +msgstr "Location" + +#: i18n/weather.glade.h:3 +msgid "Update" +msgstr "Update" + +#: i18n/weather.glade.h:4 +msgid "Automatically update every" +msgstr "Automatically update every" + +#: i18n/weather.glade.h:5 +msgid "Celsius" +msgstr "Celsius" + +#: i18n/weather.glade.h:6 +msgid "" +"Enter your location. This may be a town and\n" +"or country name. such as London, England ,\n" +"a zip or post code, or a longitude and latitude \n" +"such as 30670000,104019996." +msgstr "" +"Enter your location. This may be a town and\n" +"or country name. such as London, England ,\n" +"a zip or post code, or a longitude and latitude \n" +"such as 30670000,104019996." + +#: i18n/weather.glade.h:10 +msgid "Faranheit" +msgstr "Faranheit" + +#: i18n/weather.glade.h:11 +msgid "Kelvin" +msgstr "Kelvin" + +#: i18n/weather.glade.h:12 +msgid "Location:" +msgstr "Location:" + +#: i18n/weather.glade.h:13 +msgid "Tempature Unit:" +msgstr "Tempature Unit:" + +#: i18n/weather.glade.h:14 +msgid "Weather Preferences" +msgstr "Weather Preferences" + +#: i18n/weather.glade.h:15 +msgid "minutes" +msgstr "minutes" diff --git a/src/plugins/weather/i18n/weather.glade.h b/src/plugins/weather/i18n/weather.glade.h new file mode 100644 index 0000000..3830a73 --- /dev/null +++ b/src/plugins/weather/i18n/weather.glade.h @@ -0,0 +1,15 @@ +char *s = N_("Display"); +char *s = N_("Location"); +char *s = N_("Update"); +char *s = N_("Automatically update every"); +char *s = N_("Celsius"); +char *s = N_("Enter your location. This may be a town and\n" + "or country name. such as London, England ,\n" + "a zip or post code, or a longitude and latitude \n" + "such as 30670000,104019996."); +char *s = N_("Faranheit"); +char *s = N_("Kelvin"); +char *s = N_("Location:"); +char *s = N_("Tempature Unit:"); +char *s = N_("Weather Preferences"); +char *s = N_("minutes"); diff --git a/src/plugins/weather/i18n/weather.pot b/src/plugins/weather/i18n/weather.pot new file mode 100644 index 0000000..42fade3 --- /dev/null +++ b/src/plugins/weather/i18n/weather.pot @@ -0,0 +1,70 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: i18n/weather.glade.h:1 +msgid "Display" +msgstr "" + +#: i18n/weather.glade.h:2 +msgid "Location" +msgstr "" + +#: i18n/weather.glade.h:3 +msgid "Update" +msgstr "" + +#: i18n/weather.glade.h:4 +msgid "Automatically update every" +msgstr "" + +#: i18n/weather.glade.h:5 +msgid "Celsius" +msgstr "" + +#: i18n/weather.glade.h:6 +msgid "" +"Enter your location. This may be a town and\n" +"or country name. such as London, England ,\n" +"a zip or post code, or a longitude and latitude \n" +"such as 30670000,104019996." +msgstr "" + +#: i18n/weather.glade.h:10 +msgid "Faranheit" +msgstr "" + +#: i18n/weather.glade.h:11 +msgid "Kelvin" +msgstr "" + +#: i18n/weather.glade.h:12 +msgid "Location:" +msgstr "" + +#: i18n/weather.glade.h:13 +msgid "Tempature Unit:" +msgstr "" + +#: i18n/weather.glade.h:14 +msgid "Weather Preferences" +msgstr "" + +#: i18n/weather.glade.h:15 +msgid "minutes" +msgstr "" diff --git a/src/plugins/weather/pywapi.py b/src/plugins/weather/pywapi.py new file mode 100644 index 0000000..5c21100 --- /dev/null +++ b/src/plugins/weather/pywapi.py @@ -0,0 +1,334 @@ +#Copyright (c) 2009 Eugene Kaznacheev + +#Permission is hereby granted, free of charge, to any person +#obtaining a copy of this software and associated documentation +#files (the "Software"), to deal in the Software without +#restriction, including without limitation the rights to use, +#copy, modify, merge, publish, distribute, sublicense, and/or sell +#copies of the Software, and to permit persons to whom the +#Software is furnished to do so, subject to the following +#conditions: + +#The above copyright notice and this permission notice shall be +#included in all copies or substantial portions of the Software. + +#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +#EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +#OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +#NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +#HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +#WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +#FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +#OTHER DEALINGS IN THE SOFTWARE. + +""" +Fetches weather reports from Google Weather, Yahoo Wheather and NOAA +""" + +import urllib2, re +from xml.dom import minidom +from urllib import quote + +GOOGLE_WEATHER_URL = 'http://www.google.com/ig/api?weather=%s&hl=%s' +GOOGLE_COUNTRIES_URL = 'http://www.google.com/ig/countries?output=xml&hl=%s' +GOOGLE_CITIES_URL = 'http://www.google.com/ig/cities?output=xml&country=%s&hl=%s' + +YAHOO_WEATHER_URL = 'http://xml.weather.yahoo.com/forecastrss?w=%s&u=%s&d=5' +YAHOO_WEATHER_NS = 'http://xml.weather.yahoo.com/ns/rss/1.0' + +NOAA_WEATHER_URL = 'http://www.weather.gov/xml/current_obs/%s.xml' + +def get_weather_from_google(location_id, hl = ''): + """ + Fetches weather report from Google + + Parameters + location_id: a zip code (10001); city name, state (weather=woodland,PA); city name, country (weather=london, england); + latitude/longitude(weather=,,,30670000,104019996) or possibly other. + hl: the language parameter (language code). Default value is empty string, in this case Google will use English. + + Returns: + weather_data: a dictionary of weather data that exists in XML feed. + """ + location_id, hl = map(quote, (location_id, hl)) + url = GOOGLE_WEATHER_URL % (location_id, hl) + handler = urllib2.urlopen(url) + content_type = handler.info().dict['content-type'] + charset = re.search('charset\=(.*)',content_type).group(1) + if not charset: + charset = 'utf-8' + if charset.lower() != 'utf-8': + xml_response = handler.read().decode(charset).encode('utf-8') + else: + xml_response = handler.read() + dom = minidom.parseString(xml_response) + handler.close() + + weather_data = {} + weather_dom = dom.getElementsByTagName('weather')[0] + + data_structure = { + 'forecast_information': ('city', 'postal_code', 'latitude_e6', 'longitude_e6', 'forecast_date', 'current_date_time', 'unit_system'), + 'current_conditions': ('condition','temp_f', 'temp_c', 'humidity', 'wind_condition', 'icon') + } + for (tag, list_of_tags2) in data_structure.iteritems(): + tmp_conditions = {} + for tag2 in list_of_tags2: + try: + tmp_conditions[tag2] = weather_dom.getElementsByTagName(tag)[0].getElementsByTagName(tag2)[0].getAttribute('data') + except IndexError: + pass + weather_data[tag] = tmp_conditions + + forecast_conditions = ('day_of_week', 'low', 'high', 'icon', 'condition') + forecasts = [] + + for forecast in dom.getElementsByTagName('forecast_conditions'): + tmp_forecast = {} + for tag in forecast_conditions: + tmp_forecast[tag] = forecast.getElementsByTagName(tag)[0].getAttribute('data') + forecasts.append(tmp_forecast) + + weather_data['forecasts'] = forecasts + dom.unlink() + + return weather_data + +def get_countries_from_google(hl = ''): + """ + Get list of countries in specified language from Google + + Parameters + hl: the language parameter (language code). Default value is empty string, in this case Google will use English. + Returns: + countries: a list of elements(all countries that exists in XML feed). Each element is a dictionary with 'name' and 'iso_code' keys. + For example: [{'iso_code': 'US', 'name': 'USA'}, {'iso_code': 'FR', 'name': 'France'}] + """ + url = GOOGLE_COUNTRIES_URL % hl + + handler = urllib2.urlopen(url) + content_type = handler.info().dict['content-type'] + charset = re.search('charset\=(.*)',content_type).group(1) + if not charset: + charset = 'utf-8' + if charset.lower() != 'utf-8': + xml_response = handler.read().decode(charset).encode('utf-8') + else: + xml_response = handler.read() + dom = minidom.parseString(xml_response) + handler.close() + + countries = [] + countries_dom = dom.getElementsByTagName('country') + + for country_dom in countries_dom: + country = {} + country['name'] = country_dom.getElementsByTagName('name')[0].getAttribute('data') + country['iso_code'] = country_dom.getElementsByTagName('iso_code')[0].getAttribute('data') + countries.append(country) + + dom.unlink() + return countries + +def get_cities_from_google(country_code, hl = ''): + """ + Get list of cities of necessary country in specified language from Google + + Parameters + country_code: code of the necessary country. For example 'de' or 'fr'. + hl: the language parameter (language code). Default value is empty string, in this case Google will use English. + Returns: + cities: a list of elements(all cities that exists in XML feed). Each element is a dictionary with 'name', 'latitude_e6' and 'longitude_e6' keys. For example: [{'longitude_e6': '1750000', 'name': 'Bourges', 'latitude_e6': '47979999'}] + """ + url = GOOGLE_CITIES_URL % (country_code.lower(), hl) + + handler = urllib2.urlopen(url) + content_type = handler.info().dict['content-type'] + charset = re.search('charset\=(.*)',content_type).group(1) + if not charset: + charset = 'utf-8' + if charset.lower() != 'utf-8': + xml_response = handler.read().decode(charset).encode('utf-8') + else: + xml_response = handler.read() + dom = minidom.parseString(xml_response) + handler.close() + + cities = [] + cities_dom = dom.getElementsByTagName('city') + + for city_dom in cities_dom: + city = {} + city['name'] = city_dom.getElementsByTagName('name')[0].getAttribute('data') + city['latitude_e6'] = city_dom.getElementsByTagName('latitude_e6')[0].getAttribute('data') + city['longitude_e6'] = city_dom.getElementsByTagName('longitude_e6')[0].getAttribute('data') + cities.append(city) + + dom.unlink() + + return cities + +def get_weather_from_yahoo(location_id, units = 'metric'): + """ + Fetches weather report from Yahoo! + + Parameters + location_id: A five digit US zip code or location ID. To find your location ID, + browse or search for your city from the Weather home page(http://weather.yahoo.com/) + The weather ID is in the URL for the forecast page for that city. You can also get the location ID by entering your zip code on the home page. For example, if you search for Los Angeles on the Weather home page, the forecast page for that city is http://weather.yahoo.com/forecast/USCA0638.html. The location ID is USCA0638. + + units: type of units. 'metric' for metric and '' for non-metric + Note that choosing metric units changes all the weather units to metric, for example, wind speed will be reported as kilometers per hour and barometric pressure as millibars. + + Returns: + weather_data: a dictionary of weather data that exists in XML feed. See http://developer.yahoo.com/weather/#channel + """ + location_id = quote(location_id) + if units == 'metric': + unit = 'c' + else: + unit = 'f' + url = YAHOO_WEATHER_URL % (location_id, unit) + handler = urllib2.urlopen(url) + dom = minidom.parse(handler) + handler.close() + + weather_data = {} + weather_data['title'] = dom.getElementsByTagName('title')[0].firstChild.data + weather_data['link'] = dom.getElementsByTagName('link')[0].firstChild.data + + ns_data_structure = { + 'location': ('city', 'region', 'country'), + 'units': ('temperature', 'distance', 'pressure', 'speed'), + 'wind': ('chill', 'direction', 'speed'), + 'atmosphere': ('humidity', 'visibility', 'pressure', 'rising'), + 'astronomy': ('sunrise', 'sunset'), + 'condition': ('text', 'code', 'temp', 'date') + } + + for (tag, attrs) in ns_data_structure.iteritems(): + weather_data[tag] = xml_get_ns_yahoo_tag(dom, YAHOO_WEATHER_NS, tag, attrs) + + weather_data['geo'] = {} + weather_data['geo']['lat'] = dom.getElementsByTagName('geo:lat')[0].firstChild.data + weather_data['geo']['long'] = dom.getElementsByTagName('geo:long')[0].firstChild.data + + weather_data['condition']['title'] = dom.getElementsByTagName('item')[0].getElementsByTagName('title')[0].firstChild.data + weather_data['html_description'] = dom.getElementsByTagName('item')[0].getElementsByTagName('description')[0].firstChild.data + + forecasts = [] + for forecast in dom.getElementsByTagNameNS(YAHOO_WEATHER_NS, 'forecast'): + forecasts.append(xml_get_attrs(forecast,('date', 'low', 'high', 'text', 'code'))) + weather_data['forecasts'] = forecasts + + dom.unlink() + + return weather_data + + + +def get_weather_from_noaa(station_id): + """ + Fetches weather report from NOAA: National Oceanic and Atmospheric Administration (United States) + + Parameter: + station_id: the ID of the weather station near the necessary location + To find your station ID, perform the following steps: + 1. Open this URL: http://www.weather.gov/xml/current_obs/seek.php?state=az&Find=Find + 2. Select the necessary state state. Click 'Find'. + 3. Find the necessary station in the 'Observation Location' column. + 4. The station ID is in the URL for the weather page for that station. + For example if the weather page is http://weather.noaa.gov/weather/current/KPEO.html -- the station ID is KPEO. + + Other way to get the station ID: use this library: http://code.google.com/p/python-weather/ and 'Weather.location2station' function. + + Returns: + weather_data: a dictionary of weather data that exists in XML feed. + + (useful icons: http://www.weather.gov/xml/current_obs/weather.php) + """ + station_id = quote(station_id) + url = NOAA_WEATHER_URL % (station_id) + handler = urllib2.urlopen(url) + dom = minidom.parse(handler) + handler.close() + + data_structure = ('suggested_pickup', + 'suggested_pickup_period', + 'location', + 'station_id', + 'latitude', + 'longitude', + 'observation_time', + 'observation_time_rfc822', + 'weather', + 'temperature_string', + 'temp_f', + 'temp_c', + 'relative_humidity', + 'wind_string', + 'wind_dir', + 'wind_degrees', + 'wind_mph', + 'wind_gust_mph', + 'pressure_string', + 'pressure_mb', + 'pressure_in', + 'dewpoint_string', + 'dewpoint_f', + 'dewpoint_c', + 'heat_index_string', + 'heat_index_f', + 'heat_index_c', + 'windchill_string', + 'windchill_f', + 'windchill_c', + 'icon_url_base', + 'icon_url_name', + 'two_day_history_url', + 'ob_url' + ) + weather_data = {} + current_observation = dom.getElementsByTagName('current_observation')[0] + for tag in data_structure: + try: + weather_data[tag] = current_observation.getElementsByTagName(tag)[0].firstChild.data + except IndexError: + pass + + dom.unlink() + return weather_data + + + +def xml_get_ns_yahoo_tag(dom, ns, tag, attrs): + """ + Parses the necessary tag and returns the dictionary with values + + Parameters: + dom - DOM + ns - namespace + tag - necessary tag + attrs - tuple of attributes + + Returns: a dictionary of elements + """ + element = dom.getElementsByTagNameNS(ns, tag)[0] + return xml_get_attrs(element,attrs) + + +def xml_get_attrs(xml_element, attrs): + """ + Returns the list of necessary attributes + + Parameters: + element: xml element + attrs: tuple of attributes + + Return: a dictionary of elements + """ + + result = {} + for attr in attrs: + result[attr] = xml_element.getAttribute(attr) + return result diff --git a/src/plugins/weather/weather.py b/src/plugins/weather/weather.py new file mode 100644 index 0000000..0da15df --- /dev/null +++ b/src/plugins/weather/weather.py @@ -0,0 +1,537 @@ +# coding=UTF-8 +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2010 Brett Smith +# +# 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 . + +import gnome15.g15locale as g15locale +_ = g15locale.get_translation("weather", modfile = __file__).ugettext + +import gnome15.g15screen as g15screen +import gnome15.util.g15convert as g15convert +import gnome15.util.g15scheduler as g15scheduler +import gnome15.util.g15uigconf as g15uigconf +import gnome15.util.g15pythonlang as g15pythonlang +import gnome15.util.g15gconf as g15gconf +import gnome15.util.g15cairo as g15cairo +import gnome15.util.g15icontools as g15icontools +import gnome15.g15driver as g15driver +import gnome15.g15globals as g15globals +import gnome15.g15text as g15text +import gnome15.g15plugin as g15plugin +import gtk +import os +import pango +import logging +import time +import sys +logger = logging.getLogger(__name__) + + +# Plugin details - All of these must be provided +id="weather" +name=_("Weather") +description=_("Displays the current weather at a location. It can currently use NOAA and Yahoo as sources \ +of weather information.") +author="Brett Smith " +copyright=_("Copyright (C)2010 Brett Smith") +site="http://www.russo79.com/gnome15" +has_preferences=True +default_enabled=True +needs_network=True +unsupported_models = [ g15driver.MODEL_G110, g15driver.MODEL_G11, g15driver.MODEL_G930, g15driver.MODEL_G35 ] + +DEFAULT_LOCATION="london,england" + +''' +This simple plugin displays the current weather at a location +''' + +CELSIUS=0 +FARANHEIT=1 +KELVIN=2 + +DEFAULT_UPDATE_INTERVAL = 60 # minutes + +def create(gconf_key, gconf_client, screen): + return G15Weather(gconf_key, gconf_client, screen) + +def show_preferences(parent, driver, gconf_client, gconf_key): + G15WeatherPreferences(parent, gconf_client, gconf_key) + +def get_location(gconf_client, gconf_key): + loc = gconf_client.get_string(gconf_key + "/location") + if loc == None: + return DEFAULT_LOCATION + return loc + +def get_backend(account_type): + """ + Get the backend plugin module, given the account_type + + Keyword arguments: + account_type -- account type + """ + import gnome15.g15pluginmanager as g15pluginmanager + return g15pluginmanager.get_module_for_id("weather-%s" % account_type) + +def get_available_backends(): + """ + Get the "account type" names that are available by listing all of the + backend plugins that are installed + """ + l = [] + import gnome15.g15pluginmanager as g15pluginmanager + for p in g15pluginmanager.imported_plugins: + if p.id.startswith("weather-"): + l.append(p.id[8:]) + return l + +def c_to_f(c): + return c * 9/5.0 + 32 + +def c_to_k(c): + return c + 273.15 + +def mph_to_kph(mph): + return mph * 1.609344 + +def kph_to_mph(mph): + return mph * 0.621371192 + +class G15WeatherPreferences(): + ''' + Configuration UI + ''' + + def __init__(self, parent, gconf_client, gconf_key): + self._gconf_client = gconf_client + self._gconf_key = gconf_key + self._visible_options = None + + self._widget_tree = gtk.Builder() + self._widget_tree.add_from_file(os.path.join(os.path.dirname(__file__), "weather.ui")) + + dialog = self._widget_tree.get_object("WeatherDialog") + dialog.set_transient_for(parent) + + self._source = self._widget_tree.get_object("Source") + self._source.connect("changed", self._load_options_for_source) + + self._sources_model = self._widget_tree.get_object("SourcesModel") + for b in get_available_backends(): + l = [b, get_backend(b).backend_name ] + self._sources_model.append(l) + g15uigconf.configure_combo_from_gconf(gconf_client, "%s/source" % gconf_key, "Source", self._sources_model[0][0] if len(self._sources_model) > 0 else None, self._widget_tree) + self._load_options_for_source() + + update = self._widget_tree.get_object("UpdateAdjustment") + update.set_value(g15gconf.get_int_or_default(gconf_client, gconf_key + "/update", DEFAULT_UPDATE_INTERVAL)) + update.connect("value-changed", self._value_changed, update, gconf_key + "/update") + + unit = self._widget_tree.get_object("UnitCombo") + unit.set_active(gconf_client.get_int(gconf_key + "/units")) + unit.connect("changed", self._unit_changed, unit, gconf_key + "/units") + + g15uigconf.configure_checkbox_from_gconf(gconf_client, "%s/use_theme_icons" % gconf_key, "UseThemeIcons", True, self._widget_tree) + g15uigconf.configure_checkbox_from_gconf(gconf_client, "%s/twenty_four_hour_times" % gconf_key, "TwentyFourHourTimes", True, self._widget_tree) + + dialog.run() + dialog.hide() + + def _create_options_for_source(self, source): + backend = get_backend(source) + if backend is None: + logger.warning("No backend for weather source %s", source) + return None + return backend.create_options(self._gconf_client, "%s/%s" % ( self._gconf_key, source ) ) + + def _get_selected_source(self): + active = self._source.get_active() + return None if active == -1 else self._sources_model[active][0] + + def _load_options_for_source(self, widget = None): + source = self._get_selected_source() + if source: + options = self._create_options_for_source(source) + else: + options = None + if self._visible_options != None: + self._visible_options.component.destroy() + self._visible_options = options + place_holder = self._widget_tree.get_object("PlaceHolder") + for c in place_holder.get_children(): + place_holder.remove(c) + if self._visible_options is not None: + self._visible_options.component.reparent(place_holder) + else: + l = gtk.Label("No options found for this source\n") + l.xalign = 0.5 + l.show() + place_holder.add(l) + + def _changed(self, widget, location, gconf_key): + self._gconf_client.set_string(gconf_key, widget.get_text()) + + def _unit_changed(self, widget, location, gconf_key): + self._gconf_client.set_int(gconf_key, widget.get_active()) + + def _value_changed(self, widget, location, gconf_key): + self._gconf_client.set_int(gconf_key, int(widget.get_value())) + + +class WeatherOptions(): + + def __init__(self): + pass + +class WeatherData(): + + def __init__(self, location): + self.location = location + +class WeatherBackend(): + + def __init__(self, gconf_client, gconf_key): + self.gconf_client = gconf_client + self.gconf_key = gconf_key + + def get_weather_data(self): + raise Exception("Not implemented") + +class G15Weather(g15plugin.G15RefreshingPlugin): + + def __init__(self, gconf_key, gconf_client, screen): + g15plugin.G15RefreshingPlugin.__init__(self, gconf_client, gconf_key, screen, "weather-few-clouds", id, name) + self.only_refresh_when_visible = False + + def activate(self): + self._page_properties = {} + self._page_attributes = {} + self._weather = None + self._config_change_handle = None + self._load_config() + self._text = g15text.new_text(self.screen) + g15plugin.G15RefreshingPlugin.activate(self) + self.watch(None, self._loc_changed) + + def deactivate(self): + self._page_properties = {} + self._page_attributes = {} + if self._config_change_handle is not None: + self._config_change_handle.cancel() + g15plugin.G15RefreshingPlugin.deactivate(self) + + def destroy(self): + pass + + def refresh(self): + try : + backend_type = g15gconf.get_string_or_default(self.gconf_client, "%s/source" % self.gconf_key, None) + if backend_type: + backend = get_backend(backend_type).create_backend(self.gconf_client, "%s/%s" % (self.gconf_key, backend_type) ) + self._weather = backend.get_weather_data() + else: + self._weather = None + self._page_properties, self._page_attributes = self._build_properties() + except Exception as e: + logger.debug("Error while refreshing", exc_info = e) + self._weather = None + self._page_properties = {} + self._page_attributes = {} + self._page_properties['message'] = _("Error parsing weather data!") + + def get_theme_properties(self): + return self._page_properties + + def get_theme_attributes(self): + return self._page_properties + + """ + Private + """ + + def _load_config(self): + val = g15gconf.get_int_or_default(self.gconf_client, self.gconf_key + "/update", DEFAULT_UPDATE_INTERVAL) + self.refresh_interval = val * 60.0 + + def _loc_changed(self, client, connection_id, entry, args): + if not entry.get_key().endswith("/theme") and not entry.get_key().endswith("/enabled"): + if self._config_change_handle is not None: + self._config_change_handle.cancel() + self._config_change_handle = g15scheduler.schedule("ApplyConfig", 3.0, self._config_changed) + + def _config_changed(self): + self.reload_theme() + self._load_config() + self._refresh() + self.screen.set_priority(self.page, g15screen.PRI_HIGH, revert_after = 6.0) + + def _get_icons(self, current): + c_icon = current['icon'] if 'icon' in current else None + f_icon = current['fallback_icon'] if 'fallback_icon' in current else None + t_icon = self._translate_icon(c_icon, f_icon) + return c_icon, f_icon, t_icon + + def _build_properties(self): + properties = {} + attributes = {} + use_twenty_four_hour = g15gconf.get_bool_or_default(self.gconf_client, "%s/twenty_four_hour_times" % self.gconf_key, True) + if self._weather is None: + properties["message"] = _("No weather source configuration") + else: + current = self._weather['current_conditions'] + if len(current) == 0: + properties["message"] = _("No weather data for location:-\n%s") % self._weather['location'] + else: + properties["location"] = self._weather['location'] + dt = self._weather['datetime'] + if use_twenty_four_hour: + properties["time"] = g15locale.format_time_24hour(dt, self.gconf_client, False) + else: + properties["time"] = g15locale.format_time(dt, self.gconf_client, False) + properties["date"] = g15locale.format_date(dt, self.gconf_client) + properties["datetime"] = g15locale.format_date_time(dt, self.gconf_client, False) + properties["message"] = "" + c_icon, f_icon, t_icon = self._get_icons(current) + if t_icon != None: + attributes["icon"] = g15cairo.load_surface_from_file(t_icon) + properties["icon"] = g15icontools.get_embedded_image_url(attributes["icon"]) + else: + logger.warning("No translated weather icon for %s", c_icon) + mono_thumb = self._get_mono_thumb_icon(c_icon) + if mono_thumb != None: + attributes["mono_thumb_icon"] = g15cairo.load_surface_from_file(os.path.join(os.path.join(os.path.dirname(__file__), "default"), mono_thumb)) + properties["condition"] = current['condition'] + + temp_c = g15pythonlang.to_float_or_none(current['temp_c']) + if temp_c is not None: + temp_f = c_to_f(temp_c) + temp_k = c_to_k(temp_c) + low_c = g15pythonlang.to_float_or_none(current['low']) if 'low' in current else None + if low_c is not None : + low_f = c_to_f(low_c) + low_k = c_to_k(low_c) + high_c = g15pythonlang.to_float_or_none(current['high']) if 'high' in current else None + if high_c is not None : + high_f = c_to_f(high_c) + high_k = c_to_k(high_c) + + properties["temp_c"] = "%3.1f°C" % temp_c if temp_c is not None else "" + properties["hi_c"] = "%3.1f°C" % high_c if high_c is not None else "" + properties["lo_c"] = "%3.1f°C" % low_c if low_c is not None else "" + properties["temp_f"] = "%3.1f°F" % temp_f if temp_c is not None else "" + properties["lo_f"] = "%3.1f°F" % low_f if low_c is not None else "" + properties["high_f"] = "%3.1f°F" % high_f if high_c is not None else "" + properties["temp_k"] = "%3.1f°K" % temp_k if temp_c is not None else "" + properties["lo_k"] = "%3.1f°K" % low_k if low_c is not None else "" + properties["high_k"] = "%3.1f°K" % high_k if high_c is not None else "" + + units = self.gconf_client.get_int(self.gconf_key + "/units") + if units == CELSIUS: + unit = "C" + properties["temp"] = properties["temp_c"] + properties["temp_short"] = "%2.0f°" % temp_c if temp_c else "" + properties["hi"] = properties["hi_c"] + properties["hi_short"] = "%2.0f°" % high_c if high_c else "" + properties["lo"] = properties["lo_c"] + properties["lo_short"] = "%2.0f°" % low_c if low_c else "" + elif units == FARANHEIT: + unit = "F" + properties["lo"] = properties["lo_f"] + properties["lo_short"] = "%2.0f°" % low_f if low_c is not None else "" + properties["hi"] = properties["high_f"] + properties["hi_short"] = "%2.0f°" % high_f if high_c is not None else "" + properties["temp"] = properties["temp_f"] + properties["temp_short"] = "%2.0f°" % temp_f if temp_c is not None else "" + else: + unit = "K" + properties["lo"] = properties["lo_k"] + properties["lo_short"] = "%2.0f°" % low_k if low_c is not None else "" + properties["hi"] = properties["high_k"] + properties["hi_short"] = "%2.0f°" % high_k if high_c is not None else "" + properties["temp"] = properties["temp_k"] + properties["temp_short"] = "%2.0f°" % temp_k if temp_c is not None else "" + + + # Wind + wind = g15pythonlang.append_if_exists(current, "wind_chill", "", "%sC") + wind = g15pythonlang.append_if_exists(current, "wind_speed", wind, "%sKph") + wind = g15pythonlang.append_if_exists(current, "wind_direction", wind, "%sdeg") + properties["wind"] = wind + + # Visibility + visibility = g15pythonlang.append_if_exists(current, "visibility", "", "%sM") + properties["visibility"] = visibility + + # Pressure + pressure = g15pythonlang.append_if_exists(current, "pressure", "", "%smb") + properties["pressure"] = pressure + + # Humidity + humidity = g15pythonlang.append_if_exists(current, "humidity", "", "%s%%") + properties["humidity"] = humidity + + # Sunrise + dt = current['sunrise'] if 'sunrise' in current else None + if dt is None: + properties["sunrise_time"] = "" + elif use_twenty_four_hour: + properties["sunrise_time"] = g15locale.format_time_24hour(dt, self.gconf_client, False) + else: + properties["sunrise_time"] = g15locale.format_time(dt, self.gconf_client, False) + + # Sunset + dt = current['sunset'] if 'sunset' in current else None + if dt is None: + properties["sunset_time"] = "" + elif use_twenty_four_hour: + properties["sunset_time"] = g15locale.format_time_24hour(dt, self.gconf_client, False) + else: + properties["sunset_time"] = g15locale.format_time(dt, self.gconf_client, False) + + # Blank all the forecasts by default + for y in range(1, 10): + properties["condition" + str(y)] = "" + properties["hi" + str(y)] = "" + properties["lo" + str(y)] = "" + properties["day" + str(y)] = "" + properties["day_letter" + str(y)] = "" + properties["icon" + str(y)] = "" + + # Forecasts + y = 1 + if 'forecasts' in self._weather: + for forecast in self._weather['forecasts']: + properties["condition" + str(y)] = forecast['condition'] + + lo_c = g15pythonlang.to_float_or_none(forecast['low']) + if lo_c is not None: + lo_f = c_to_f(temp_c) + lo_k = c_to_k(temp_c) + hi_c = g15pythonlang.to_float_or_none(forecast['high']) + if hi_c is not None: + hi_f = c_to_f(hi_c) + hi_k = c_to_k(hi_c) + + if units == CELSIUS: + properties["hi" + str(y)] = "%3.0f°C" % hi_c + properties["lo" + str(y)] = "%3.0f°C" % lo_c + elif units == FARANHEIT: + properties["hi" + str(y)] = "%3.0f°F" % hi_f + properties["lo" + str(y)] = "%3.0f°F" % lo_f + else: + properties["hi" + str(y)] = "%3.0f°K" % hi_k + properties["lo" + str(y)] = "%3.0f°K" % lo_k + + properties["day" + str(y)] = forecast['day_of_week'] + properties["day_letter" + str(y)] = forecast['day_of_week'][:1] + + c_icon, f_icon, t_icon = self._get_icons(forecast) + properties["icon" + str(y)] = g15icontools.get_embedded_image_url(g15cairo.load_surface_from_file(t_icon)) + + y += 1 + + return properties, attributes + + def _get_mono_thumb_icon(self, icon): + if icon == None or icon == "": + return None + else : + base_icon= self._get_base_icon(icon) + + if base_icon in [ "chanceofrain", "scatteredshowers" ]: + return "weather-showers-scattered.gif" + elif base_icon in [ "sunny", "haze" ]: + return "mono-sunny.gif" + elif base_icon == "mostlysunny": + return "mono-few-clouds.gif" + elif base_icon == "partlycloudy": + return "mono-clouds.gif" + elif base_icon in [ "mostlycloudy", "cloudy" ]: + return "mono-more-clouds.gif" + elif base_icon == "rain": + return "mono-rain.gif" + elif base_icon in [ "mist", "fog" ]: + return "mono-fog.gif" + elif base_icon in [ "chanceofsnow", "snow", "sleet", "flurries" ]: + return "mono-snow.gif" + elif base_icon in [ "storm", "chanceofstorm" ]: + return "mono-dark-clouds.gif" + elif base_icon in [ "thunderstorm", "chanceoftstorm" ]: + return "mono-thunder.gif" + + def _translate_icon(self, icon, fallback_icon): + theme_icon = icon + if theme_icon == None or theme_icon == "": + return None + else: + if not g15gconf.get_bool_or_default(self.gconf_client, "%s/use_theme_icons" % self.gconf_key, True): + return fallback_icon + + if theme_icon != None: + icon_path = g15icontools.get_icon_path(theme_icon, warning = False, include_missing = False) + if icon_path == None and theme_icon.endswith("-night"): + icon_path = g15icontools.get_icon_path(theme_icon[:len(theme_icon) - 6], include_missing = False) + + if icon_path != None: + return icon_path + + return g15icontools.get_icon_path(icon) + + def _get_base_icon(self, icon): + # Strips off URL path, image extension, size and weather prefix if present + base_icon = os.path.splitext(os.path.basename(icon))[0].rsplit("-")[0] + if base_icon.startswith("weather_"): + base_icon = base_icon[8:] + base_icon = base_icon.replace('_','') + return base_icon + + def _paint_panel(self, canvas, allocated_size, horizontal): + return self._paint_thumbnail(canvas, allocated_size, horizontal) + + def _paint_thumbnail(self, canvas, allocated_size, horizontal): + total_taken = 0 + self._text.set_canvas(canvas) + if self.screen.driver.get_bpp() == 1: + if "mono_thumb_icon" in self._page_attributes: + size = g15cairo.paint_thumbnail_image(allocated_size, self._page_attributes["mono_thumb_icon"], canvas) + canvas.translate(size + 2, 0) + total_taken += size + 2 + if "temp_short" in self._page_properties: + self._text.set_attributes(self._page_properties["temp_short"], \ + font_desc = g15globals.fixed_size_font_name, \ + font_absolute_size = 6 * pango.SCALE / 2) + x, y, width, height = self._text.measure() + total_taken += width + self._text.draw(x, y) + else: + rgb = self.screen.driver.get_color_as_ratios(g15driver.HINT_FOREGROUND, ( 0, 0, 0 )) + canvas.set_source_rgb(rgb[0],rgb[1],rgb[2]) + if "icon" in self._page_attributes: + size = g15cairo.paint_thumbnail_image(allocated_size, self._page_attributes["icon"], canvas) + total_taken += size + if "temp" in self._page_properties: + if horizontal: + self._text.set_attributes(self._page_properties["temp"], font_desc = "Sans", font_absolute_size = allocated_size * pango.SCALE / 2) + x, y, width, height = self._text.measure() + self._text.draw(total_taken, (allocated_size / 2) - height / 2) + total_taken += width + 4 + else: + self._text.set_attributes(self._page_properties["temp"], font_desc = "Sans", font_absolute_size = allocated_size * pango.SCALE / 4) + x, y, width, height = self._text.measure() + self._text.draw((allocated_size / 2) - width / 2, total_taken) + total_taken += height + 4 + return total_taken + diff --git a/src/plugins/weather/weather.ui b/src/plugins/weather/weather.ui new file mode 100644 index 0000000..33706fa --- /dev/null +++ b/src/plugins/weather/weather.ui @@ -0,0 +1,356 @@ + + + + + + + + + + + + + + + + + + + + Celsius + + + Faranheit + + + Kelvin + + + + + 1 + 99999 + 1 + 1 + + + 450 + False + 5 + Weather Preferences + False + True + center-on-parent + dialog + + + True + False + 2 + + + True + False + end + + + gtk-close + True + True + True + True + + + False + False + 0 + + + + + False + True + end + 0 + + + + + True + False + 4 + + + True + False + 0 + none + + + True + False + 12 + + + True + False + 4 + + + True + False + SourcesModel + + + + 1 + + + + + True + True + 0 + + + + + True + False + + + + + + True + True + 1 + + + + + + + + + True + False + <b>Source</b> + True + + + + + True + True + 0 + + + + + True + False + 0 + none + + + True + False + 12 + + + True + False + 8 + + + True + False + Automatically update every + + + True + True + 0 + + + + + True + True + 6 + + False + False + True + True + UpdateAdjustment + + + True + True + 1 + + + + + True + False + minutes + + + True + True + 2 + + + + + + + + + True + False + <b>Update</b> + True + + + + + True + True + 1 + + + + + True + False + 0 + none + + + True + False + 12 + + + True + False + 4 + + + True + False + 8 + + + True + False + 0 + Tempature Unit: + + + False + False + 0 + + + + + True + False + Units + + + + 0 + + + + + True + True + 1 + + + + + True + True + 0 + + + + + True + False + + + Use theme icons + True + True + False + True + + + True + True + 0 + + + + + Show time in 24 hour format + True + True + False + True + + + True + True + 1 + + + + + True + True + 1 + + + + + + + + + True + False + <b>Display</b> + True + + + + + True + True + 2 + + + + + False + False + 1 + + + + + + button9 + + + diff --git a/src/plugins/webkitbrowser/Makefile.am b/src/plugins/webkitbrowser/Makefile.am new file mode 100644 index 0000000..a94c1ec --- /dev/null +++ b/src/plugins/webkitbrowser/Makefile.am @@ -0,0 +1,6 @@ +SUBDIRS = default +plugindir = $(datadir)/gnome15/plugins/webkitbrowser +plugin_DATA = webkitbrowser.py + +EXTRA_DIST = \ + $(plugin_DATA) diff --git a/src/plugins/webkitbrowser/default/Makefile.am b/src/plugins/webkitbrowser/default/Makefile.am new file mode 100644 index 0000000..abea5b9 --- /dev/null +++ b/src/plugins/webkitbrowser/default/Makefile.am @@ -0,0 +1,5 @@ +themedir = $(datadir)/gnome15/plugins/webkitbrowser/default +theme_DATA = g19.svg + +EXTRA_DIST = \ + $(theme_DATA) diff --git a/src/plugins/webkitbrowser/default/g19.svg b/src/plugins/webkitbrowser/default/g19.svg new file mode 100644 index 0000000..285635a --- /dev/null +++ b/src/plugins/webkitbrowser/default/g19.svg @@ -0,0 +1,223 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + ${url} + + + diff --git a/src/plugins/webkitbrowser/webkitbrowser.py b/src/plugins/webkitbrowser/webkitbrowser.py new file mode 100644 index 0000000..b2e29b9 --- /dev/null +++ b/src/plugins/webkitbrowser/webkitbrowser.py @@ -0,0 +1,96 @@ +#!/usr/bin/env python + +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2011 Brett Smith +# +# 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 . + +import gnome15.g15driver as g15driver +import gnome15.g15gtk as g15gtk +import gnome15.g15plugin as g15plugin +import gtk +import gobject +import webkit + +# Plugin details - All of these must be provided +id="webkitbrowser" +name="Webkit Browser" +description="Webkit based browser." +author="Brett Smith " +copyright="Copyright (C)2010 Brett Smith" +site="http://www.gnome15.org/" +has_preferences=False +supported_models = [ g15driver.MODEL_G19 ] + +def create(gconf_key, gconf_client, screen): + return G15WebkitBrowser(gconf_client, gconf_key, screen) + +class G15WebkitBrowser(g15plugin.G15PagePlugin): + + def __init__(self, gconf_client, gconf_key, screen): + g15plugin.G15PagePlugin.__init__(self, gconf_client, gconf_key, screen, \ + [ "browser", "gnome-web-browser", "web-browser", "www-browser", \ + "redhat-web-browser", "internet-web-browser" ], id, name) + self.add_page_on_activate = False + + def populate_page(self): + g15plugin.G15PagePlugin.populate_page(self) + self.window = g15gtk.G15OffscreenWindow("offscreenWindow") + self.page.add_child(self.window) + gobject.idle_add(self._create_browser) + + def activate(self): + g15plugin.G15PagePlugin.activate(self) + self.screen.key_handler.action_listeners.append(self) + + def deactivate(self): + g15plugin.G15PagePlugin.deactivate(self) + self.screen.key_handler.action_listeners.remove(self) + + def action_performed(self, binding): + if self.page is not None and self.page.is_visible(): + if binding.action == g15driver.PREVIOUS_PAGE: + gobject.idle_add(self._scroll_up) + return True + elif binding.action == g15driver.NEXT_PAGE: + gobject.idle_add(self._scroll_down) + return True + + ''' + Private + ''' + + def get_theme_properties(self): + return dict(g15plugin.G15PagePlugin.get_theme_properties(self).items() + { + "url" : "www.somewhere.com" + }.items()) + + def _scroll_up(self): + adj = self.scroller.get_vadjustment() + adj.set_value(adj.get_value() - adj.get_page_increment()) + self.screen.redraw(self.page) + + def _scroll_down(self): + adj = self.scroller.get_vadjustment() + adj.set_value(adj.get_value() + adj.get_page_increment()) + self.screen.redraw(self.page) + + def _create_browser(self): + view = webkit.WebView() + self.scroller = gtk.ScrolledWindow() + self.scroller.add(view) + view.open("http://www.youtube.com") + self.window.set_content(self.scroller) + self.screen.add_page(self.page) + self.screen.redraw(self.page) \ No newline at end of file diff --git a/src/pylibg19/Makefile.am b/src/pylibg19/Makefile.am new file mode 100644 index 0000000..8443237 --- /dev/null +++ b/src/pylibg19/Makefile.am @@ -0,0 +1 @@ +SUBDIRS = g19 \ No newline at end of file diff --git a/src/pylibg19/g19/Makefile.am b/src/pylibg19/g19/Makefile.am new file mode 100644 index 0000000..17319b8 --- /dev/null +++ b/src/pylibg19/g19/Makefile.am @@ -0,0 +1,11 @@ +g19dir = $(pkgpythondir)/../g19 +g19_PYTHON = \ + __init__.py \ + g19.py \ + globals.py \ + keys.py \ + receivers.py \ + runnable.py + +EXTRA_DIST = \ + $(g19_PYTHON) \ No newline at end of file diff --git a/src/pylibg19/g19/__init__.py b/src/pylibg19/g19/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/pylibg19/g19/g19.py b/src/pylibg19/g19/g19.py new file mode 100644 index 0000000..17f0376 --- /dev/null +++ b/src/pylibg19/g19/g19.py @@ -0,0 +1,442 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2010 Brett Smith +# +# 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 . + +from receivers import G19Receiver + +import sys +import threading +import time +import usb +from PIL import Image as Img +import logging +import array +logger = logging.getLogger(__name__) + +class G19(object): + '''Simple access to Logitech G19 features. + + All methods are thread-safe if not denoted otherwise. + + ''' + + def __init__(self, resetOnStart=False, enable_mm_keys=False, write_timeout = 10000, reset_wait = 0): + '''Initializes and opens the USB device.''' + + logger.info("Setting up G19 with write timeout of %d", write_timeout) + self.enable_mm_keys = enable_mm_keys + self.__write_timeout = write_timeout + self.__usbDevice = G19UsbController(resetOnStart, enable_mm_keys, reset_wait) + self.__usbDeviceMutex = threading.Lock() + self.__keyReceiver = G19Receiver(self) + self.__threadDisplay = None + + self.__frame_content = [0x10, 0x0F, 0x00, 0x58, 0x02, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x3F, 0x01, 0xEF, 0x00, 0x0F] + for i in range(16, 256): + self.__frame_content.append(i) + for i in range(256): + self.__frame_content.append(i) + + @staticmethod + def convert_image_to_frame(filename): + '''Loads image from given file. + + Format will be auto-detected. If neccessary, the image will be resized + to 320x240. + + @return Frame data to be used with send_frame(). + + ''' + img = Img.open(filename) + access = img.load() + if img.size != (320, 240): + img = img.resize((320, 240), Img.CUBIC) + access = img.load() + data = [] + for x in range(320): + for y in range(240): + ax = access[x, y] + if len(ax) == 3: + r, g, b = ax + else: + r, g, b, a = ax + val = G19.rgb_to_uint16(r, g, b) + data.append(val >> 8) + data.append(val & 0xff) + return data + + @staticmethod + def rgb_to_uint16(r, g, b): + '''Converts a RGB value to 16bit highcolor (5-6-5). + + @return 16bit highcolor value in little-endian. + + ''' + rBits = r * 2**5 / 255 + gBits = g * 2**6 / 255 + bBits = b * 2**5 / 255 + + rBits = rBits if rBits <= 0b00011111 else 0b00011111 + gBits = gBits if gBits <= 0b00111111 else 0b00111111 + bBits = bBits if bBits <= 0b00011111 else 0b00011111 + + valueH = (rBits << 3) | (gBits >> 3) + valueL = (gBits << 5) | bBits + return valueL << 8 | valueH + + def add_input_processor(self, input_processor): + self.__keyReceiver.add_input_processor(input_processor) + + def add_applet(self, applet): + '''Starts an applet.''' + self.add_input_processor(applet.get_input_processor()) + + def fill_display_with_color(self, r, g, b): + '''Fills display with given color.''' + # 16bit highcolor format: 5 red, 6 gree, 5 blue + # saved in little-endian, because USB is little-endian + value = self.rgb_to_uint16(r, g, b) + valueH = value & 0xff + valueL = value >> 8 + frame = [valueL, valueH] * (320 * 240) + self.send_frame(frame) + + def load_image(self, filename): + '''Loads image from given file. + + Format will be auto-detected. If neccessary, the image will be resized + to 320x240. + + ''' + self.send_frame(self.convert_image_to_frame(filename)) + + def read_g_and_m_keys(self, maxLen=20): + '''Reads interrupt data from G, M and light switch keys. + + @return maxLen Maximum number of bytes to read. + @return Read data or empty list. + + ''' + self.__usbDeviceMutex.acquire() + val = [] + try: + val = list(self.__usbDevice.handleIf1.interruptRead( + 0x83, maxLen, 10)) + except usb.USBError as e: + if e.message != "Connection timed out": + logger.debug("Error reading g and m keys", exc_info = e) + pass + finally: + self.__usbDeviceMutex.release() + return val + + def read_display_menu_keys(self): + '''Reads interrupt data from display keys. + + @return Read data or empty list. + + ''' + self.__usbDeviceMutex.acquire() + val = [] + try: + val = list(self.__usbDevice.handleIf0.interruptRead(0x81, 2, 10)) + except usb.USBError as e: + if e.message != "Connection timed out": + logger.debug("Error reading display menu keys", exc_info = e) + pass + finally: + self.__usbDeviceMutex.release() + return val + + def read_multimedia_keys(self): + '''Reads interrupt data from multimedia keys. + + @return Read data or empty list. + + ''' + if not self.enable_mm_keys: + return False + + self.__usbDeviceMutex.acquire() + val = [] + try: + val = list(self.__usbDevice.handleIfMM.interruptRead(0x82, 2, 10)) + except usb.USBError as e: + if e.message != "Connection timed out": + logger.debug("Error reading multimedia keys", exc_info = e) + pass + finally: + self.__usbDeviceMutex.release() + return val + + def reset(self): + '''Initiates a bus reset to USB device.''' + self.__usbDeviceMutex.acquire() + try: + self.__usbDevice.reset() + finally: + self.__usbDeviceMutex.release() + + def save_default_bg_color(self, r, g, b): + '''This stores given color permanently to keyboard. + + After a reset this will be color used by default. + + ''' + rtype = usb.TYPE_CLASS | usb.RECIP_INTERFACE + colorData = [7, r, g, b] + self.__usbDeviceMutex.acquire() + try: + self.__usbDevice.handleIf1.controlMsg( + rtype, 0x09, colorData, 0x308, 0x01, self.__write_timeout) + finally: + self.__usbDeviceMutex.release() + + def send_frame(self, data): + '''Sends a frame to display. + + @param data 320x240x2 bytes, containing the frame in little-endian + 16bit highcolor (5-6-5) format. + Image must be row-wise, starting at upper left corner and ending at + lower right. This means (data[0], data[1]) is the first pixel and + (data[239 * 2], data[239 * 2 + 1]) the lower left one. + + ''' + if len(data) != (320 * 240 * 2): + raise ValueError("illegal frame size: " + str(len(data)) + + " should be 320x240x2=" + str(320 * 240 * 2)) + frame = list(self.__frame_content) + frame += data + + self.__usbDeviceMutex.acquire() + try: + self.__usbDevice.handleIf0.bulkWrite(2, frame, self.__write_timeout) + finally: + self.__usbDeviceMutex.release() + + def set_bg_color(self, r, g, b): + '''Sets backlight to given color.''' + rtype = usb.TYPE_CLASS | usb.RECIP_INTERFACE + colorData = [7, r, g, b] + self.__usbDeviceMutex.acquire() + try: + self.__usbDevice.handleIf1.controlMsg( + rtype, 0x09, colorData, 0x307, 0x01, 10000) + finally: + self.__usbDeviceMutex.release() + + def set_enabled_m_keys(self, keys): + '''Sets currently lit keys as an OR-combination of LIGHT_KEY_M1..3,R. + + example: + from logitech.g19_keys import Data + lg19 = G19() + lg19.set_enabled_m_keys(Data.LIGHT_KEY_M1 | Data.LIGHT_KEY_MR) + + ''' + rtype = usb.TYPE_CLASS | usb.RECIP_INTERFACE + self.__usbDeviceMutex.acquire() + try: + self.__usbDevice.handleIf1.controlMsg( + rtype, 0x09, [5, keys], 0x305, 0x01, self.__write_timeout) + finally: + self.__usbDeviceMutex.release() + + def set_display_brightness(self, val): + '''Sets display brightness. + + @param val in [0,100] (off..maximum). + + ''' + data = [val, 0xe2, 0x12, 0x00, 0x8c, 0x11, 0x00, 0x10, 0x00] + rtype = usb.TYPE_VENDOR | usb.RECIP_INTERFACE + self.__usbDeviceMutex.acquire() + try: + self.__usbDevice.handleIf1.controlMsg(rtype, 0x0a, data, 0x0, 0x0, self.__write_timeout) + finally: + self.__usbDeviceMutex.release() + + def start_event_handling(self): + '''Start event processing (aka keyboard driver). + + This method is NOT thread-safe. + + ''' + self.stop_event_handling() + self.__threadDisplay = threading.Thread( + target=self.__keyReceiver.run) + self.__keyReceiver.start() + self.__threadDisplay.name = "EventThread" + self.__threadDisplay.setDaemon(True) + self.__threadDisplay.start() + + def stop_event_handling(self): + '''Stops event processing (aka keyboard driver). + + This method is NOT thread-safe. + + ''' + self.__keyReceiver.stop() + if self.__threadDisplay: + self.__threadDisplay.join() + self.__threadDisplay = None + + def close(self): + logger.info("Closing G19") + self.stop_event_handling() + self.__usbDevice.close() + + +class G19UsbController(object): + '''Controller for accessing the G19 USB device. + + The G19 consists of two composite USB devices: + * 046d:c228 + The keyboard consisting of two interfaces: + MI00: keyboard + EP 0x81(in) - INT the keyboard itself + MI01: (ifacMM) + EP 0x82(in) - multimedia keys, incl. scroll and Winkey-switch + + * 046d:c229 + LCD display with two interfaces: + MI00 (0x05): (iface0) via control data in: display keys + EP 0x81(in) - INT + EP 0x02(out) - BULK display itself + MI01 (0x06): (iface1) backlight + EP 0x83(in) - INT G-keys, M1..3/MR key, light key + + ''' + + def __init__(self, resetOnStart=False, enable_mm_keys=False, resetWait = 0): + self.enable_mm_keys = enable_mm_keys + logger.info("Looking for LCD device") + self.__lcd_device = self._find_device(0x046d, 0xc229) + if not self.__lcd_device: + raise usb.USBError("G19 LCD not found on USB bus") + + # Reset + self.handleIf0 = self.__lcd_device.open() + if resetOnStart: + logger.info("Resetting LCD device") + self.handleIf0.reset() + time.sleep(float(resetWait) / 1000.0) + logger.info("Re-opening LCD device") + self.handleIf0 = self.__lcd_device.open() + logger.info("Re-opened LCD device") + + self.handleIf1 = self.__lcd_device.open() + + config = self.__lcd_device.configurations[0] + display_interface = config.interfaces[0][0] + + # This is to cope with a difference in pyusb 1.0 compatibility layer + if len(config.interfaces) > 1: + macro_and_backlight_interface = config.interfaces[1][0] + else: + macro_and_backlight_interface = config.interfaces[0][1] + + try: + logger.debug("Detaching kernel driver for LCD device") + # Use .interfaceNumber for pyusb 1.0 compatibility layer + self.handleIf0.detachKernelDriver(display_interface.interfaceNumber) + logger.debug("Detached kernel driver for LCD device") + except usb.USBError as e: + logger.debug("Detaching kernel driver for LCD device failed.", exc_info = e) + + try: + logger.debug("Detaching kernel driver for macro / backlight device") + # Use .interfaceNumber for pyusb 1.0 compatibility layer + self.handleIf1.detachKernelDriver(macro_and_backlight_interface.interfaceNumber) + logger.debug("Detached kernel driver for macro / backlight device") + except usb.USBError as e: + logger.debug("Detaching kernel driver for macro / backlight device failed.", exc_info = e) + + logger.debug("Setting configuration") + + #self.handleIf0.setConfiguration(1) + #self.handleIf1.setConfiguration(1) + + logger.debug("Claiming LCD interface") + self.handleIf0.claimInterface(display_interface.interfaceNumber) + logger.info("Claimed LCD interface") + logger.debug("Claiming macro interface") + self.handleIf1.claimInterface(macro_and_backlight_interface.interfaceNumber) + logger.info("Claimed macro interface") + + if self.enable_mm_keys: + logger.debug("Looking for multimedia keys device") + self.__kbd_device = self._find_device(0x046d, 0xc228) + if not self.__kbd_device: + raise usb.USBError("G19 keyboard not found on USB bus") + self.handleIfMM = self.__kbd_device.open() + + if resetOnStart: + logger.debug("Resetting multimedia keys device") + self.handleIfMM.reset() + logger.debug("Re-opening multimedia keys device") + self.handleIfMM = self.__kbd_device.open() + logger.debug("Re-opened multimedia keys device") + + + config = self.__kbd_device.configurations[0] + ifacMM = config.interfaces[1][0] + + try: + self.handleIfMM.setConfiguration(1) + except usb.USBError as e: + logger.debug("Error when trying to set configuration", exc_info = e) + pass + try: + logger.debug("Detaching kernel driver for multimedia keys device") + self.handleIfMM.detachKernelDriver(ifacMM) + logger.debug("Detached kernel driver for multimedia keys device") + except usb.USBError as e: + logger.debug("Detaching kernel driver for multimedia keys device failed.", exc_info = e) + + logger.debug("Claiming multimedia interface") + self.handleIfMM.claimInterface(1) + logger.info("Claimed multimedia keys interface") + + + def close(self): + if self.enable_mm_keys: + self.handleIfMM.releaseInterface() + self.handleIf1.releaseInterface() + self.handleIf0.releaseInterface() + + @staticmethod + def _find_device(idVendor, idProduct): + for bus in usb.busses(): + for dev in bus.devices: + if dev.idVendor == idVendor and \ + dev.idProduct == idProduct: + return dev + return None + + def reset(self): + '''Resets the device on the USB.''' + self.handleIf0.reset() + self.handleIf1.reset() + +def main(): + lg19 = G19() + lg19.start_event_handling() + time.sleep(20) + lg19.stop_event_handling() + +if __name__ == '__main__': + main() diff --git a/src/pylibg19/g19/globals.py b/src/pylibg19/g19/globals.py new file mode 100644 index 0000000..6b7e7e3 --- /dev/null +++ b/src/pylibg19/g19/globals.py @@ -0,0 +1,18 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2011 Brett Smith +# +# 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 . + +name = "pylibg19" +version = "0.0.1" diff --git a/src/pylibg19/g19/keys.py b/src/pylibg19/g19/keys.py new file mode 100644 index 0000000..c90e688 --- /dev/null +++ b/src/pylibg19/g19/keys.py @@ -0,0 +1,205 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2010 Brett Smith +# +# 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 . + +class Key(object): + '''Static container containing all keys.''' + + # G/M keys + # light switch + LIGHT, \ + M1, \ + M2, \ + M3, \ + MR, \ + G01, \ + G02, \ + G03, \ + G04, \ + G05, \ + G06, \ + G07, \ + G08, \ + G09, \ + G10, \ + G11, \ + G12 = range(17) + + # special keys at display + BACK, \ + DOWN, \ + LEFT, \ + MENU, \ + OK, \ + RIGHT, \ + SETTINGS, \ + UP = range(G12 + 1, G12 + 9) + + # multimedia keys + WINKEY_SWITCH, \ + NEXT, \ + PREV, \ + STOP, \ + PLAY, \ + MUTE, \ + SCROLL_UP, \ + SCROLL_DOWN = range(UP + 1, UP + 9) + + displayKeys = set([ + BACK, + DOWN, + LEFT, + MENU, + OK, + RIGHT, + SETTINGS, + UP + ]) + + mmKeys = set([ + WINKEY_SWITCH, + NEXT, + PREV, + STOP, + PLAY, + MUTE, + SCROLL_UP, + SCROLL_DOWN]) + + gmKeys = set([ + G01, + G02, + G03, + G04, + G05, + G06, + G07, + G08, + G09, + G10, + G11, + G12, + LIGHT, + M1, + M2, + M3, + MR]) + + +class Data(object): + '''Static container with all data values for all keys.''' + + ## + ## display keys + ## + + # special keys at display + # The current state of pressed keys is an OR-combination of the following + # codes. + # Incoming data always has 0x80 appended, e.g. pressing and releasing the menu + # key results in two INTERRUPT transmissions: [0x04, 0x80] and [0x00, 0x80] + # Pressing (and holding) UP and OK at the same time results in [0x88, 0x80]. + displayKeys = {} + displayKeys[0x01] = Key.SETTINGS + displayKeys[0x02] = Key.BACK + displayKeys[0x04] = Key.MENU + displayKeys[0x08] = Key.OK + displayKeys[0x10] = Key.RIGHT + displayKeys[0x20] = Key.LEFT + displayKeys[0x40] = Key.DOWN + displayKeys[0x80] = Key.UP + + + ## + ## G- and M-Keys + ## + + # these are the bit fields for setting the currently illuminated keys + # (see set_enabled_m_keys()) + LIGHT_KEY_M1 = 0x80 + LIGHT_KEY_M2 = 0x40 + LIGHT_KEY_M3 = 0x20 + LIGHT_KEY_MR = 0x10 + + # specific codes sent by M- and G-keys + # received as [0x02, keyL, keyH, 0x40] + # example: G3: [0x02, 0x04, 0x00, 0x40] + # G1 + G2 + G11: [0x02, 0x03, 0x04, 0x40] + KEY_G01 = 0x000001 + KEY_G02 = 0x000002 + KEY_G03 = 0x000004 + KEY_G04 = 0x000008 + KEY_G05 = 0x000010 + KEY_G06 = 0x000020 + KEY_G07 = 0x000040 + KEY_G08 = 0x000080 + KEY_G09 = 0x000100 + KEY_G10 = 0x000200 + KEY_G11 = 0x000400 + KEY_G12 = 0x000800 + KEY_M1 = 0x001000 + KEY_M2 = 0x002000 + KEY_M3 = 0x004000 + KEY_MR = 0x008000 + + # light switch + # this on is similar to G-keys: + # down: [0x02, 0x00, 0x00, 0x48] + # up: [0x02, 0x00, 0x00, 0x40] + KEY_LIGHT = 0x080000 + + gmKeys = {} + gmKeys[KEY_G01] = Key.G01 + gmKeys[KEY_G02] = Key.G02 + gmKeys[KEY_G03] = Key.G03 + gmKeys[KEY_G04] = Key.G04 + gmKeys[KEY_G05] = Key.G05 + gmKeys[KEY_G06] = Key.G06 + gmKeys[KEY_G07] = Key.G07 + gmKeys[KEY_G08] = Key.G08 + gmKeys[KEY_G09] = Key.G09 + gmKeys[KEY_G10] = Key.G10 + gmKeys[KEY_G11] = Key.G11 + gmKeys[KEY_G12] = Key.G12 + gmKeys[KEY_G12] = Key.G12 + gmKeys[KEY_M1] = Key.M1 + gmKeys[KEY_M2] = Key.M2 + gmKeys[KEY_M3] = Key.M3 + gmKeys[KEY_MR] = Key.MR + gmKeys[KEY_LIGHT] = Key.LIGHT + + + ## + ## MM-keys + ## + + # multimedia keys + # received as [0x01, key] + # example: NEXT+SCROLL_UP: [0x01, 0x21] + # after scroll stopped: [0x01, 0x01] + # after release: [0x01, 0x00] + mmKeys = {} + mmKeys[0x01] = Key.NEXT + mmKeys[0x02] = Key.PREV + mmKeys[0x04] = Key.STOP + mmKeys[0x08] = Key.PLAY + mmKeys[0x10] = Key.MUTE + mmKeys[0x20] = Key.SCROLL_UP + mmKeys[0x40] = Key.SCROLL_DOWN + + # winkey switch to winkey off: [0x03, 0x01] + # winkey switch to winkey on: [0x03, 0x00] + KEY_WIN_SWITCH = 0x0103 + diff --git a/src/pylibg19/g19/receivers.py b/src/pylibg19/g19/receivers.py new file mode 100644 index 0000000..b4154ef --- /dev/null +++ b/src/pylibg19/g19/receivers.py @@ -0,0 +1,311 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2010 Brett Smith +# +# 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 . + +from keys import (Data, Key) +from runnable import Runnable + +import threading +import time +import logging +logger = logging.getLogger(__name__) + +class InputProcessor(object): + '''Object to process key presses.''' + + def process_input(self, inputEvent): + '''Processes given event. + + Should return as fast as possible. Any time-consuming processing + should be done in another thread. + + @param inputEvent Event to process. + @return True if event was consumed, or False if ignored. + + ''' + return False + + +class InputEvent(object): + '''Event created by a key press or release.''' + + def __init__(self, oldState, newState, keysDown, keysUp): + '''Creates an InputEvent. + + @param oldState State before event happened. + @param newState State after event happened. + @param keysDown Keys newly pressed. + @param keysUp Kys released by this event. + + ''' + self.oldState = oldState + self.newState = newState + self.keysDown = keysDown + self.keysUp = keysUp + + +class State(object): + '''Current state of keyboard.''' + + def __init__(self): + self.__keysDown = set() + + def _data_to_keys_g_and_m(self, data): + '''Converts a G/M keys data package to a set of keys defined as + pressed by it. + + ''' + if len(data) != 4 or data[0] != 2: + raise ValueError("not a multimedia key packet: " + str(data)) + empty = 0x400000 + curVal = data[3] << 16 | data[2] << 8 | data[1] + keys = [] + while curVal != empty: + foundAKey = False + for val in Data.gmKeys.keys(): + if val & curVal == val: + curVal ^= val + keys.append(Data.gmKeys[val]) + foundAKey = True + if not foundAKey: + raise ValueError("incorrect g/m key packet: " + + str(data)) + + return set(keys) + self.__keysDown = set() + + def _data_to_keys_d(self, data): + '''Converts a D data package to a set of keys defined as + pressed by it. + ''' + if len(data) != 2 or data[1] != 0x80: + raise ValueError("not a D key packet: " + str(data)) + curVal = data[0] + keys = [] + + '''Zero is release + ''' + if curVal != 0: + foundAKey = False + for val in Data.displayKeys.keys(): + if val & curVal == val: + curVal ^= val + keys.append(Data.displayKeys[val]) + foundAKey = True + if not foundAKey: + raise ValueError("incorrect D key packet: " + + str(data)) + return set(keys) + + def _data_to_keys_mm(self, data): + '''Converts a multimedia keys data package to a set of keys defined as + pressed by it. + + ''' + if len(data) != 2 or data[0] not in [1, 3]: + raise ValueError("not a multimedia key packet: " + str(data)) + if data[0] == 1: + curVal = data[1] + keys = [] + while curVal: + foundAKey = False + for val in Data.mmKeys.keys(): + if val & curVal == val: + curVal ^= val + keys.append(Data.mmKeys[val]) + foundAKey = True + if not foundAKey: + raise ValueError("incorrect multimedia key packet: " + + str(data)) + elif data == [3, 1]: + keys = [Key.WINKEY_SWITCH] + elif data == [3, 0]: + keys = [] + else: + raise ValueError("incorrect multimedia key packet: " + str(data)) + + return set(keys) + + def _update_keys_down(self, possibleKeys, keys): + '''Updates internal keysDown set. + + Updates the current state of all keys in 'possibleKeys' with state + given in 'keys'. + + Example: + Currently set as pressed in self.__keysDown: [A, B] + possibleKeys: [B, C, D] + keys: [C] + + This would set self.__keysDown to [A, C] and return ([C], [B]) + + @param possibleKeys Keys whose state could be given as 'pressed' at the + same time by 'keys'. + @param keys Current state of all keys in possibleKeys. + @return A pair of sets listing newly pressed and newly released keys. + + ''' + keysDown = set() + keysUp = set() + for key in possibleKeys: + if key in keys: + if key not in self.__keysDown: + self.__keysDown.add(key) + keysDown.add(key) + else: + if key in self.__keysDown: + self.__keysDown.remove(key) + keysUp.add(key) + return (keysDown, keysUp) + + def clone(self): + '''Returns an exact copy of this state.''' + state = State() + state.__keysDown = set(self.__keysDown) + return state + + def packet_received_g_and_m(self, data): + '''Mutates the state by given data packet from G- and M- keys. + + @param data Data packet received. + @return InputEvent for data packet, or None if data packet was ignored. + + ''' + oldState = self.clone() + evt = None + logger.debug("G key of %d", len(data)) + if len(data) == 4: + keys = self._data_to_keys_g_and_m(data) + keysDown, keysUp = self._update_keys_down(Key.gmKeys, keys) + newState = self.clone() + evt = InputEvent(oldState, newState, keysDown, keysUp) + return evt + + def packet_received_d(self, data): + '''Mutates the state by given data packet from D- keys. + + @param data Data packet received. + @return InputEvent for data packet, or None if data packet was ignored. + + ''' + oldState = self.clone() + evt = None + logger.debug("D key of %d", len(data)) + if len(data) == 2: + keys = self._data_to_keys_d(data) + keysDown, keysUp = self._update_keys_down(Key.displayKeys, keys) + newState = self.clone() + evt = InputEvent(oldState, newState, keysDown, keysUp) + return evt + + def packet_received_mm(self, data): + '''Mutates the state by given data packet from multimedia keys. + + @param data Data packet received. + @return InputEvent for data packet. + + ''' + oldState = self.clone() + if len(data) != 2: + raise ValueError("incorrect multimedia key packet: " + str(data)) + logger.debug("MM or Win key of %d", len(data)) + keys = self._data_to_keys_mm(data) + winKeySet = set([Key.WINKEY_SWITCH]) + if data[0] == 1: + # update state of all mm keys + logger.debug("MM key %d", len(data)) + possibleKeys = Key.mmKeys.difference(winKeySet) + keysDown, keysUp = self._update_keys_down(possibleKeys, keys) + else: + # update winkey state + logger.debug("Win key") + keysDown, keysUp = self._update_keys_down(winKeySet, keys) + newState = self.clone() + return InputEvent(oldState, newState, keysDown, keysUp) + + +class G19Receiver(Runnable): + '''This receiver consumes all data sent by special keys.''' + + def __init__(self, g19): + Runnable.__init__(self) + self.__g19 = g19 + self.__ips = [] + self.__mutex = threading.Lock() + self.__state = State() + + def add_input_processor(self, processor): + '''Adds an input processor.''' + self.__mutex.acquire() + self.__ips.append(processor) + self.__mutex.release() + pass + + def execute(self): + gotData = False + processors = self.list_all_input_processors() + + if self.__g19.enable_mm_keys: + data = self.__g19.read_multimedia_keys() + if data: + logger.debug('MM keys data %s', len(data)) + evt = self.__state.packet_received_mm(data) + if evt: + for proc in processors: + if proc.process_input(evt): + break + else: + logger.info('MM keys ignored') + gotData = True + + data = self.__g19.read_g_and_m_keys() + if data: + logger.debug('G/M keys data %s', len(data)) + evt = self.__state.packet_received_g_and_m(data) + if evt: + for proc in processors: + if proc.process_input(evt): + break + else: + logger.info('G/M keys ignored') + gotData = True + + data = self.__g19.read_display_menu_keys() + if data: + logger.debug('Menu keys Data %s', len(data)) + evt = self.__state.packet_received_d(data) + if evt: + for proc in processors: + if proc.process_input(evt): + break + else: + logger.info('Menu keys ignored') + gotData = True + + if not gotData: + time.sleep(0.05) + + def list_all_input_processors(self): + '''Returns a list of all input processors currently registered to this + receiver. + + @return All registered processors. This list is a copy of the internal + one. + + ''' + self.__mutex.acquire() + allProcessors = list(self.__ips) + self.__mutex.release() + return allProcessors diff --git a/src/pylibg19/g19/runnable.py b/src/pylibg19/g19/runnable.py new file mode 100644 index 0000000..a8f3932 --- /dev/null +++ b/src/pylibg19/g19/runnable.py @@ -0,0 +1,85 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2010 Brett Smith +# +# 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 . + +import threading + +class Runnable(object): + '''Helper object to create thread content objects doing periodic tasks, or + tasks supporting premature termination. + + Override execute() in inherited class. This will be called until the + thread is stopped. A Runnable can be started multiple times opposed to + threading.Thread. + + To write a non-periodic task that should support premature termination, + simply override run() and call is_about_to_stop() at possible termination + points. + + ''' + + def __init__(self): + self.__keepRunning = True + self.__mutex = threading.Lock() + + def execute(self): + '''This method must be implemented and will be executed in an infinite + loop as long as stop() was not called. + + An implementation is free to check is_about_to_stop() at any time to + allow a clean termination of current processing before reaching the end + of execute(). + + ''' + pass + + def is_about_to_stop(self): + '''Returns whether this thread will terminate after completing the + current execution cycle. + + @return True if thread will terminate after current execution cycle. + + ''' + self.__mutex.acquire() + val = self.__keepRunning + self.__mutex.release() + return not val + + def run(self): + '''Implements the infinite loop. Do not override, but override + execute() instead. + + ''' + while not self.is_about_to_stop(): + self.execute() + + def start(self): + '''Starts the thread. If stop() was called, but start() was not, run() + will do nothing. + + ''' + self.__mutex.acquire() + self.__keepRunning = True + self.__mutex.release() + + def stop(self): + '''Flags this thread to be terminated after next completed execution + cycle. Calling this method will NOT stop the thread instantaniously, + but will complete the current operation and terminate in a clean way. + + ''' + self.__mutex.acquire() + self.__keepRunning = False + self.__mutex.release() diff --git a/src/scripts/Makefile.am b/src/scripts/Makefile.am new file mode 100644 index 0000000..aa6024b --- /dev/null +++ b/src/scripts/Makefile.am @@ -0,0 +1,16 @@ + +if ENABLE_SYSTEMTRAY + MAYBE_SYSTEMTRAY = g15-systemtray +endif + +if ENABLE_INDICATOR + MAYBE_INDICATOR = g15-indicator +endif + +if ENABLE_DRIVER_KERNEL + MAYBE_KERNEL = g15-system-service +endif + +bin_SCRIPTS = g15-launch libg15test g15-diag g15-config g15-desktop-service g15-support-dump $(MAYBE_SYSTEMTRAY) $(MAYBE_INDICATOR) $(MAYBE_KERNEL) + +EXTRA_DIST = g15-launch libg15test g15-diag g15-config g15-desktop-service g15-systemtray g15-indicator g15-system-service g15-support-dump diff --git a/src/scripts/evtest b/src/scripts/evtest new file mode 100755 index 0000000..9b1f419 --- /dev/null +++ b/src/scripts/evtest @@ -0,0 +1,77 @@ +#!/usr/bin/env python2 + +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2011 Brett Smith +# +# 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 . + +import optparse +import select +import fcntl +import pyinputevent.scancodes as S +from pyinputevent.uinput import UInputDevice +from pyinputevent.pyinputevent import InputEvent, SimpleDevice +from pyinputevent.keytrans import * + +EVIOCGRAB = 0x40044590 + +class ForwardDevice(SimpleDevice): + def __init__(self, *args, **kwargs): + SimpleDevice.__init__(self, *args, **kwargs) + self.ctrl = False + self.alt = False + self.shift = False + + def monitor(self): + poll = select.poll() + poll.register(self, select.POLLIN | select.POLLPRI | select.POLLHUP | select.POLLNVAL | select.POLLERR) + fno = self.fileno() + fcntl.ioctl(fno, EVIOCGRAB, 1) + while True: + for x, e in poll.poll(): + self.read() + + @property + def modcode(self): + code = 0 + if self.shift: + code += 1 + if self.ctrl: + code += 2 + if self.alt: + code += 4 + return code + + def receive(self, event): + print "Event: %s" % str(event) + if event.etype == S.EV_KEY: + key = str(event.ecode) + if event.evalue == 2: + print "Auto %s" % key + else: + if event.evalue == 1: + print "Down %s" % key + else: + print "Up %s" % key + elif event.etype == 0: + print "Etype 0" + else: + print "Unhandled event: %s" % str(event) + +if __name__ == "__main__": + parser = optparse.OptionParser() + (options, args) = parser.parse_args() + device = ForwardDevice(args[0]) + device.monitor() + \ No newline at end of file diff --git a/src/scripts/g15-config b/src/scripts/g15-config new file mode 100755 index 0000000..585995d --- /dev/null +++ b/src/scripts/g15-config @@ -0,0 +1,61 @@ +#!/usr/bin/env python2 + +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2010 Brett Smith +# +# 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 . + +import sys +import os + +# with pygobject-3.14.0 this seems to fix the import error +import gconf + +# Allow running from local path +path = os.path.join(os.path.abspath(os.path.dirname(sys.argv[0])), "..") +if os.path.exists(path): + sys.path.insert(0, path) + +#Logging +import gnome15.g15logging as g15logging +logger = g15logging.get_root_logger() + +# This is a work around - Now Gio is used in the lens plugin, it must be +# initialised before GTK. +try: + from gi.repository import Gio +except Exception as a: + logger.debug("Error when importing Gio", exc_info = a) + pass + +import pygtk +pygtk.require('2.0') +import gtk +import gconf +import gobject +gobject.threads_init() + +# DBUS - Use to check current desktop service status or stop it +import dbus +from dbus.mainloop.glib import DBusGMainLoop +from dbus.mainloop.glib import threads_init +dbus.mainloop.glib.threads_init() +DBusGMainLoop(set_as_default=True) + +import gnome15.g15config as g15config +import gnome15.g15globals as g15globals +import gnome15.g15drivermanager as g15drivermanager + +a = g15config.G15Config() +a.run() diff --git a/src/scripts/g15-desktop-service b/src/scripts/g15-desktop-service new file mode 100755 index 0000000..037ec77 --- /dev/null +++ b/src/scripts/g15-desktop-service @@ -0,0 +1,112 @@ +#!/usr/bin/env python2 + +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2011 Brett Smith +# +# 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 . + +import sys +import os +import glib +import time + +# Allow running from local path +path = os.path.abspath(os.path.join(os.path.abspath(os.path.dirname(sys.argv[0])), "..")) +if os.path.exists(path): + sys.path.insert(0, path) + +# Logging +import gnome15.g15logging as g15logging +logger = g15logging.get_root_logger() + +# +import gobject +import gnome15.util.g15pythonlang as g15pythonlang +gobject.threads_init() + +# DBUS - Use to check current desktop service status or stop it +import dbus +from dbus.mainloop.glib import DBusGMainLoop +from dbus.mainloop.glib import threads_init +threads_init() +DBusGMainLoop(set_as_default=True) + + +# Server host class + +def check_service_status(session_dbus): + try : + session_bus.get_object('org.gnome15.Gnome15', '/org/gnome15/Service').GetServerInformation() + return True + except Exception as e: + logger.debug("Did not found enabled service", exc_info = e) + return False + +def start_service(options): + + if g15pythonlang.module_exists("setproctitle"): + import setproctitle + setproctitle.setproctitle(os.path.basename(os.path.abspath(sys.argv[0]))) + else: + # Not a big issue + logger.debug("No setproctitle, process will be named 'python'") + + # Start the loop + try : + import gnome15.g15service as g15service + service = g15service.G15Service(None, no_trap=options.no_trap) + service.exit_on_no_devices = options.exit_on_no_devices + g15service.logger.setLevel(logger.level) + service.start_loop() + except dbus.exceptions.NameExistsException as e: + logger.debug("DBus service already exist", exc_info = e) + print "Gnome15 desktop service is already running" + sys.exit(1) + +if __name__ == "__main__": + import optparse + parser = optparse.OptionParser() + parser.add_option("-l", "--log", dest="log_level", metavar="INFO,DEBUG,WARNING,ERROR,CRITICAL", + default="warning" , help="Log level") + parser.add_option("-f", "--foreground", action="store_true", dest="foreground", + default=False, help="Run desktop service in foreground.") + parser.add_option("-n", "--notrap", action="store_true", dest="no_trap", + default=False, help="Do not try to trap signals.") + parser.add_option("-x", "--exit", action="store_true", dest="exit_on_no_devices", + default=False, help="Exit immediately if there are no devices.") + (options, args) = parser.parse_args() + + if options.log_level != None: + logger.setLevel(g15logging.get_level(options.log_level)) + + session_bus = None + + if len(args) == 1 and ( args[0] == "stop" or args[0] == "restart" ): + session_bus = dbus.SessionBus() + if not check_service_status(session_bus): + if args[0] == "stop": + print "Gnome15 desktop service is not running" + else: + session_bus.get_object('org.gnome15.Gnome15', '/org/gnome15/Service').Stop() + while check_service_status(session_bus): + pass + session_bus.close() + + if len(args) == 0 or ( len(args) == 1 and ( args[0] == "start" or args[0] == "restart" ) ): + session_bus = dbus.SessionBus() + if check_service_status(session_bus): + print "Gnome15 desktop service already running" + else: + if options.foreground or ( not options.foreground and os.fork() == 0 ): + start_service(options) diff --git a/src/scripts/g15-diag b/src/scripts/g15-diag new file mode 100755 index 0000000..dbe08c2 --- /dev/null +++ b/src/scripts/g15-diag @@ -0,0 +1,194 @@ +#!/usr/bin/env python2 + +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2011 Brett Smith +# +# 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 . + +""" +DBUS System Service that is intended to replace 'lgsetled', the command line tool initially +used by the kernel driver support to set the brightness of keyboard lights (the device +files of which require root access, as they are in /sys). +""" + +import sys +import os +import glib + +# Allow running from local path +path = os.path.abspath(os.path.join(os.path.abspath(os.path.dirname(__file__)), "..")) +if os.path.exists(path): + sys.path.insert(0, path) + +# Logging +import gnome15.g15logging as g15logging +logger = g15logging.get_root_logger() + +# Allow running from local path +path = os.path.abspath(os.path.join(os.path.abspath(os.path.dirname(sys.argv[0])), "..")) +if os.path.exists(path): + sys.path.insert(0, path) + +# +import gobject +gobject.threads_init() + +import gconf +import gnome15.g15drivermanager as g15drivermanager +import gnome15.g15devices as g15devices +import gnome15.g15uinput as g15uinput +import gnome15.g15driver as g15driver +import termios, sys, os + +TERMIOS = termios +conf_client = gconf.client_get_default() + +def getkey(): + fd = sys.stdin.fileno() + old = termios.tcgetattr(fd) + new = termios.tcgetattr(fd) + new[3] = new[3] & ~TERMIOS.ICANON & ~TERMIOS.ECHO + new[6][TERMIOS.VMIN] = 1 + new[6][TERMIOS.VTIME] = 0 + termios.tcsetattr(fd, TERMIOS.TCSANOW, new) + c = None + try: + c = os.read(fd, 1) + finally: + termios.tcsetattr(fd, TERMIOS.TCSAFLUSH, old) + return c + +def list_drivers(): + for driver in g15drivermanager.imported_drivers.values(): + print "Id: %s" % driver.id + print "\tName: %s" % driver.name + print "\tDescription: %s" % driver.description + +def list_devices(): + for device in g15devices.find_all_devices(): + print "UID: %s" % device.uid + print "\tModel: %s" % device.model_id + print "\tUSB ID: 0x%0.4x:0x%0.4x" % ( device.controls_usb_id[0], device.controls_usb_id[1] ) + print "\tLCD BPP: %d" % ( device.bpp ) + print "\tLCD Size: %s" % ( str(device.lcd_size) ) + driver, reconfigured = get_driver(device) + if driver is None: + print "\tConfigured Driver: None found" + else: + driver_mod = sys.modules[driver.__module__] + if reconfigured: + print "\tConfigured Driver: %s (%s) [Next best, configured driver not available]" % ( driver_mod.id, driver_mod.name ) + else: + print "\tConfigured Driver: %s (%s)" % ( driver_mod.id, driver_mod.name ) + print "\t\tName: %s" % driver.get_name() + print "\t\tModel: %s" % driver.get_model_name() + print "\t\tSupported Models: %s" % driver.get_model_names() + print "\t\tBPP: %s" % driver.get_bpp() + print "\t\tAntialias: %s" % driver.get_antialias() + +def get_driver(device): + reconfigured = False + try: + driver = g15drivermanager.get_driver(conf_client, device) + except: + driver = g15drivermanager.get_best_driver(conf_client, device) + reconfigured = True + return driver, reconfigured + +def _check_hint(hint, value, name, list): + if value & hint: + list.append(name) + +def controls(uid): + g15uinput.open_devices() + device = g15devices.get_device(uid) + driver, reconfigured = get_driver(device) + if driver is None: + raise Exception("No driver for device with UID of %s." % uid) + + for c in driver.get_controls(): + print "%s" % c.id + print "\tName: %s" % c.name + print "\tLower: %s" % str(c.lower) + print "\tHigher: %s" % str(c.upper) + print "\tValue: %s" % str(c.value) + print "\tDefault Value: %s" % str(c.default_value) + hint_names = [] + _check_hint(g15driver.HINT_DIMMABLE, c.hint, "Dimmable", hint_names) + _check_hint(g15driver.HINT_SHADEABLE, c.hint, "Shadeable", hint_names) + _check_hint(g15driver.HINT_FOREGROUND, c.hint, "Foreground", hint_names) + _check_hint(g15driver.HINT_BACKGROUND, c.hint, "Background", hint_names) + _check_hint(g15driver.HINT_HIGHLIGHT, c.hint, "Highlight", hint_names) + _check_hint(g15driver.HINT_SWITCH, c.hint, "Switch", hint_names) + _check_hint(g15driver.HINT_MKEYS, c.hint, "MKeys", hint_names) + _check_hint(g15driver.HINT_VIRTUAL, c.hint, "Virtual", hint_names) + _check_hint(g15driver.HINT_RED_BLUE_LED, c.hint, "Red/Blue", hint_names) + print "\tHints: %s" % ",".join(hint_names) + +def keytest(uid): + g15uinput.open_devices() + device = g15devices.get_device(uid) + if uid is None: + raise Exception("No device with UID of %s." % uid) + driver, reconfigured = get_driver(device) + if driver is None: + raise Exception("No driver for device with UID of %s." % uid) + driver.connect() + print "Connected, now monitoring macro keys" + driver.grab_keyboard(handle_key) + print "Press any standard key to stop monitoring macro keys" + getkey() + driver.disconnect() + +def handle_key(keys, state): + print "Keys: %s, State: %d" % (keys, state) + +if __name__ == "__main__": + import optparse + parser = optparse.OptionParser() + parser.add_option("-l", "--log", dest="log_level", metavar="INFO,DEBUG,WARNING,ERROR,CRITICAL", + default="warning" , help="Log level") + (options, args) = parser.parse_args() + + if len(args) == 0: + print "No command" + sys.exit(1) + + if args[0] == "devices": + list_devices() + elif args[0] == "drivers": + list_drivers() + elif args[0] == "keytest": + del args[0] + if len(args) == 0: + print "No device UID specified. Use 'g15-diag devices' to show devices" + sys.exit(1) + keytest(args[0]) + elif args[0] == "controls": + del args[0] + if len(args) == 0: + print "No device UID specified. Use 'g15-diag devices' to show devices" + sys.exit(1) + controls(args[0]) + elif args[0] == "control": + del args[0] + if len(args) == 0: + print "No device UID specified. Use 'g15-diag devices' to show devices" + sys.exit(1) + device = arg[0] + del args[0] + if len(args) == 0: + print "No control ID specified. Use 'g15-diag controls ' to show controls" + sys.exit(1) + control(device, args[0]) diff --git a/src/scripts/g15-indicator b/src/scripts/g15-indicator new file mode 100755 index 0000000..53f9eee --- /dev/null +++ b/src/scripts/g15-indicator @@ -0,0 +1,110 @@ +#!/usr/bin/env python2 + +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2010 Brett Smith +# +# 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 . + +''' +Provides a panel indicator that can be used to control and monitor the Gnome15 +desktop service (g15-desktop-service). It will display a list of currently active +screens on activation, and allow the configuration UI to be launched (g15-config) +''' + +import sys +import pygtk +pygtk.require('2.0') +import gtk +import os +import appindicator +import gconf +from threading import RLock + +# Allow running from local path +path = os.path.join(os.path.abspath(os.path.dirname(sys.argv[0])), "..") +if os.path.exists(path): + sys.path.insert(0, path) + +# Logging +import gnome15.g15logging as g15logging +logger = g15logging.get_root_logger() + +# This block MUST be before the imports of the gnome15 modules +import dbus +import gobject +gobject.threads_init() +import dbus +from dbus.mainloop.glib import DBusGMainLoop +from dbus.mainloop.glib import threads_init +threads_init() +DBusGMainLoop(set_as_default=True) + +import gnome15.g15globals as g15globals +import gnome15.g15service as g15service +import gnome15.g15screen as g15screen +import gnome15.util.g15convert as g15convert +import gnome15.g15desktop as g15desktop + +class G15Indicator(g15desktop.G15GtkMenuPanelComponent): + + def __init__(self): + g15desktop.G15GtkMenuPanelComponent.__init__(self) + + def create_component(self): + + item = gtk.MenuItem("Preferences") + item.connect("activate", self.show_configuration) + self.menu.append(item) + + item = gtk.MenuItem("About Gnome15") + item.connect("activate", self.about_info) + self.menu.append(item) + + self.menu.append(gtk.MenuItem()) + + self.indicator = appindicator.Indicator("gnome15", + self.get_icon_path("logitech-g-keyboard-panel"), + appindicator.CATEGORY_HARDWARE) + self.indicator.set_status (appindicator.STATUS_ACTIVE) + self.indicator.set_menu(self.menu) + + def clear_attention(self): + self.remove_attention_menu_item() + if self.conf_client.get_bool("/apps/gnome15/indicate_only_on_error"): + self.indicator.set_status (appindicator.STATUS_PASSIVE) + else: + self.indicator.set_status (appindicator.STATUS_ACTIVE) + + def attention(self, message = None): + self.indicator.set_status (appindicator.STATUS_ATTENTION) + + def icons_changed(self): + self.indicator.set_icon(self.get_icon_path([ "logitech-g-keyboard-panel", "logitech-g-keyboard-applet" ])) + self.indicator.set_attention_icon(self.get_icon_path([ "logitech-g-keyboard-error-panel", "logitech-g-keyboard-error-applet" ])) + +# run it in a gtk window +if __name__ == "__main__": + try : + import setproctitle + setproctitle.setproctitle(os.path.basename(os.path.abspath(sys.argv[0]))) + except Exception as e: + logger.debug("Could not import setproctitle. Using python as processname", exc_info = e) + pass + + if g15desktop.get_desktop() == "gnome-shell": + sys.stderr.write("Indicator is not supported in GNOME Shell, use the GNOME Shell extension instead") + sys.exit(1) + + G15Indicator().start_service() + gtk.main() \ No newline at end of file diff --git a/src/scripts/g15-launch b/src/scripts/g15-launch new file mode 100755 index 0000000..00bf358 --- /dev/null +++ b/src/scripts/g15-launch @@ -0,0 +1,67 @@ +#!/usr/bin/env python2 + +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2011 Brett Smith +# +# 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 . + +import sys +import os +import glib +import time + +# Allow running from local path +path = os.path.abspath(os.path.join(os.path.abspath(os.path.dirname(sys.argv[0])), "..")) +if os.path.exists(path): + sys.path.insert(0, path) + +# Logging +import gnome15.g15logging as g15logging +logger = g15logging.get_root_logger() + +import dbus +from dbus.mainloop.glib import DBusGMainLoop +DBusGMainLoop(set_as_default=True) + +if __name__ == "__main__": + import optparse + parser = optparse.OptionParser() + parser.add_option("-l", "--log", dest="log_level", metavar="INFO,DEBUG,WARNING,ERROR,CRITICAL", + default="warning" , help="Log level") + parser.add_option("-p", "--profile", action="store_true", dest="profile", + default="", help="Name of profile to activate. Defaults to automatic selection.") + parser.add_option("-s", "--screens", action="store_true", dest="screens", + default="", help="Which device(s) to use. Defaults to best device. \ +Names are in the format [model]_[index]. So if you have two G13 keyboards \ +and a G19, to select the first G13 you would use g13_0.") + (options, args) = parser.parse_args() + if len(args) == 0: + print "You must provide the command to launch through Gnome15 as an argument." + sys.exit(2) + + if options.log_level != None: + logger.setLevel(g15logging.get_level(options.log_level)) + + session_bus = dbus.SessionBus() + try : + service = session_bus.get_object('org.gnome15.Gnome15', \ + '/org/gnome15/Service') + except Exception as e: + logger.debug("D-Bus service not available.", exc_info = e) + print "Gnome15 desktop service is not running. Applications may not be \ +launched through it. You can start the service using g15-desktop-service." + sys.exit(1) + + service.Launch(options.profile, options.screens, args) + \ No newline at end of file diff --git a/src/scripts/g15-support-dump b/src/scripts/g15-support-dump new file mode 100755 index 0000000..9a88f54 --- /dev/null +++ b/src/scripts/g15-support-dump @@ -0,0 +1,130 @@ +#!/bin/bash + +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2011 Brett Smith +# +# 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 . + +# +# Simple script to gather as much information about the environmen +# Gnome15 is running in as possible. +# +# Sorry it's a bit untidy, it will improve :) + +# Check running as root +if [ $(id -u) != 0 ] +then echo "$0: you should run that as root using either 'sudo $0' or 'su -c $0'" >&2 + exit 1 +fi + +separator() { + echo "------------------------------------------------------------" +} + +# System details +echo -e "System Details\n" +uname -a +if [ -f /etc/lsb-release ] +then cat /etc/lsb-release +fi +echo +cat /proc/cpuinfo +echo +cat /proc/meminfo + +# Gnome15 packages +echo -e "Gnome15 Packages\n" +if which dpkg >/dev/null 2>&1 +then echo -e "Debian based packaging found\n" + dpkg -l 'gnome15*' 'pylibg19*' 'lg4l*' 'python-uinput*' 'python-inputevent*' 'libsuinput*' 'libg15*' 'g15*' 2>/dev/null +fi +if which rpm >/dev/null 2>&1 +then echo -e "RPM based packaging found\n" + rpm -qa 'gnome15*' 'pylibg19*' 'lg4l*' 'python-uinput*' 'python-inputevent*' 'libsuinput*' 'libg15*' 'g15*' 2>/dev/null +fi +separator + +# lsusb +echo -e "USB Device Summary (lsusb)\n" +lsusb +echo -e "\nUSB Device Details (lsusb -v)\n" +lsusb -v +separator + +# kernel modules +echo -n "Kernel modules :" +mods=$(lsmod|awk '{ print $1 }'|grep "hid_"|sort -u) +if [ -z "${mods}" ] +then echo "No kernel modules used" +else echo "${mods}" + echo -e "\nFrame buffers: " + ls -l /dev/fb* + echo -e "\nInput Devices: " + for i in /dev/input/by-id/* + do + linked_to=$(ls -l $i|awk '{ print $10 }') + linked_to_name=$(basename $linked_to) + linked_to_file=/dev/input/$linked_to_name + linked_to_details=$(ls -l $linked_to_file|awk '{ print $1, $3, $4 }') + echo $(basename $i)" -> ${linked_to_name} ( ${linked_to_details} )" + done +fi +separator +if [ -f /etc/default/lg4l-linux ] +then echo "/etc/default/lg4l-linux contents :-" + cat /etc/default/lg4l-linux + separator +fi + +# USB device permissions (for g15direct/g19direct) +echo -n "USB device permissions" +ls -lR /dev/bus/usb +separator + +if [ -d /sys/class/leds ] +then echo -n "LED files (/sys/class/leds)" + ls -l /sys/class/leds + separator +fi + + +if [ -d /sys/class/graphics ] +then echo "Frame buffer information" + for i in /sys/class/graphics/* + do + echo "$i ->" + pushd $i >/dev/null + if [ -f name ]; then + echo " Name : "$(cat name) + fi + if [ -f mode ]; then + echo " Mode : "$(cat mode) + fi + if [ -f modes ]; then + echo " Modes: "$(cat modes) + fi + if [ -f bits_per_pixel ]; then + echo " BPP : "$(cat bits_per_pixel) + fi + ls -l|awk '{ print "\t" $0 }' + popd >/dev/null + done + separator +fi + +if [ -d /sys/bus/usb/drivers ] +then echo "Drivers bound to USB devices" + find /sys/bus/usb/drivers + separator + fi diff --git a/src/scripts/g15-system-service b/src/scripts/g15-system-service new file mode 100755 index 0000000..802d699 --- /dev/null +++ b/src/scripts/g15-system-service @@ -0,0 +1,105 @@ +#!/usr/bin/env python2 + +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2011 Brett Smith +# +# 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 . + +""" +DBUS System Service that is intended to replace 'lgsetled', the command line tool initially +used by the kernel driver support to set the brightness of keyboard lights (the device +files of which require root access, as they are in /sys). +""" + + +import sys +import os +import glib + +# Allow running from local path +path = os.path.abspath(os.path.join(os.path.abspath(os.path.dirname(sys.argv[0])), "..")) +if os.path.exists(path): + sys.path.insert(0, path) + +# Logging +import gnome15.g15logging as g15logging +logger = g15logging.get_root_logger() + +# +import gobject +gobject.threads_init() + +# DBUS - Use to check current desktop service status or stop it +import dbus +from dbus.mainloop.glib import DBusGMainLoop +from dbus.mainloop.glib import threads_init +threads_init() +DBusGMainLoop(set_as_default=True) + +# Server host class + +def check_service_status(bus): + return bus.name_has_owner('org.gnome15.SystemService') + +def start_service(bus, no_trap=False,): + try : + import setproctitle + setproctitle.setproctitle(os.path.basename(os.path.abspath(sys.argv[0]))) + except ImportError as ie: + # Not a big issue + logger.debug("No setproctitle, process will be named 'python'", exc_info = ie) + + # Start the loop + try : + import gnome15.g15system as g15system + service = g15system.G15SystemServiceController(bus, no_trap=no_trap) + service.start_loop() + except dbus.exceptions.NameExistsException as e: + logger.debug("Gnome15 service already running", exc_info = e) + print "Gnome15 desktop service is already running" + sys.exit(1) + +if __name__ == "__main__": + import optparse + parser = optparse.OptionParser() + parser.add_option("-l", "--log", dest="log_level", metavar="INFO,DEBUG,WARNING,ERROR,CRITICAL", + default="warning" , help="Log level") + parser.add_option("-f", "--foreground", action="store_true", dest="foreground", + default=False, help="Run desktop service in foreground.") + parser.add_option("-s", "--session", action="store_true", dest="use_session_bus", + default=False, help="Use the session bus instead of system bus.") + parser.add_option("-n", "--notrap", action="store_true", dest="no_trap", + default=False, help="Do not try to trap signals.") + (options, args) = parser.parse_args() + + if options.log_level != None: + logger.setLevel(g15logging.get_level(options.log_level)) + + if len(args) == 1 and ( args[0] == "stop" or args[0] == "restart" ): + bus = dbus.SessionBus() if options.use_session_bus else dbus.SystemBus() + if not check_service_status(bus): + if args[0] == "stop": + print "Gnome15 system service is not running" + else: + service_object = bus.get_object('org.gnome15.SystemService', '/org/gnome15/SystemService') + system_service = dbus.Interface(service_object, 'org.gnome15.SystemService') + system_service.Stop() + + if len(args) == 0 or ( len(args) == 1 and ( args[0] == "start" or args[0] == "restart" ) ): + bus = dbus.SessionBus() if options.use_session_bus else dbus.SystemBus() + if check_service_status(bus): + print "Gnome15 desktop service already running" + else: + if options.foreground or ( not options.foreground and os.fork() == 0 ): + start_service(bus, options.no_trap) diff --git a/src/scripts/g15-systemtray b/src/scripts/g15-systemtray new file mode 100755 index 0000000..0de2696 --- /dev/null +++ b/src/scripts/g15-systemtray @@ -0,0 +1,129 @@ +#!/usr/bin/env python2 + +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2011 Brett Smith +# +# 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 . + +''' +Provides a panel indicator that can be used to control and monitor the Gnome15 +desktop service (g15-desktop-service). It will display a list of currently active +screens on activation, and allow the configuration UI to be launched (g15-config) +''' + + +import sys +import pygtk +pygtk.require('2.0') +import gtk +import os +import gconf +from threading import RLock + +# Allow running from local path +path = os.path.join(os.path.abspath(os.path.dirname(sys.argv[0])), "..") +if os.path.exists(path): + sys.path.insert(0, path) + +# Logging +import gnome15.g15logging as g15logging +logger = g15logging.get_root_logger() + +# This block MUST be before the imports of the gnome15 modules +import dbus +import gobject +gobject.threads_init() +import dbus +from dbus.mainloop.glib import DBusGMainLoop +from dbus.mainloop.glib import threads_init +threads_init() +DBusGMainLoop(set_as_default=True) + +import gnome15.g15globals as g15globals +import gnome15.g15service as g15service +import gnome15.g15screen as g15screen +import gnome15.util.g15icontools as g15icontools +import gnome15.g15desktop as g15desktop + +class G15SystemTray(g15desktop.G15GtkMenuPanelComponent): + + def __init__(self): + self.prefs_menu = gtk.Menu() + g15desktop.G15GtkMenuPanelComponent.__init__(self) + + def create_component(self): + self.status_icon = gtk.StatusIcon() + + self.status_icon.connect('popup-menu', self._on_popup_menu) + self.status_icon.connect('activate', self._on_activate) + self.status_icon.connect('scroll_event', self.scroll_event) + + def clear_attention(self): + self.remove_attention_menu_item() + self.status_icon.set_from_pixbuf(self.normal_icon) + self.status_icon.set_tooltip("") + self.status_icon.set_visible(not self.conf_client.get_bool("/apps/gnome15/indicate_only_on_error")) + + def attention(self, message=None): + self.status_icon.set_visible(True) + self.status_icon.set_from_pixbuf(self.attention_icon) + self.status_icon.set_tooltip(message if message != None else self.default_message) + + def _on_popup_menu(self, status, button, time): + self.prefs_menu.popup(None, None, None, button, time) + + def _on_activate(self, status): + if len(self.menu.get_children()) > 0: + self.menu.popup(None, None, None, 1, gtk.get_current_event_time()) + + def add_service_item(self, item): + self._append_item(item, self.prefs_menu) + + def add_start_desktop_service(self): + g15desktop.G15GtkMenuPanelComponent.add_start_desktop_service(self) + self.add_service_item(gtk.MenuItem()) + + def rebuild_desktop_component(self): + g15desktop.G15GtkMenuPanelComponent.rebuild_desktop_component(self) + if len(self.devices)> 1: + self.add_service_item(gtk.MenuItem()) + item = gtk.MenuItem("Properties") + item.connect("activate", self.show_configuration) + self.add_service_item(item) + item = gtk.MenuItem("About") + item.connect("activate", self.about_info) + self.add_service_item(item) + self.status_icon.menu = self.prefs_menu + + self.prefs_menu.show_all() + + def icons_changed(self): + self.normal_icon = gtk.gdk.pixbuf_new_from_file_at_size(g15icontools.get_icon_path([ "logitech-g-keyboard-applet", "logitech-g-keyboard-panel" ]), self.status_icon.get_size(), self.status_icon.get_size()) + self.attention_icon = gtk.gdk.pixbuf_new_from_file_at_size(g15icontools.get_icon_path([ "logitech-g-keyboard-error-panel", "logitech-g-keyboard-error-applet" ]), self.status_icon.get_size(), self.status_icon.get_size()) + +# run it in a gtk window +if __name__ == "__main__": + try : + import setproctitle + setproctitle.setproctitle(os.path.basename(os.path.abspath(sys.argv[0]))) + except Exception as e: + logger.debug("setproctitle not available. Process will be named python", + exc_info = e) + + if g15desktop.get_desktop() == "gnome-shell": + sys.stderr.write("System Tray is not recommended in GNOME Shell, use the GNOME Shell extension instead (if you have version 3.4 or above)") + + tray = G15SystemTray() + tray.start_service() + gtk.main() diff --git a/src/scripts/lg4l-image b/src/scripts/lg4l-image new file mode 100755 index 0000000..cec72c8 --- /dev/null +++ b/src/scripts/lg4l-image @@ -0,0 +1,204 @@ +#!/usr/bin/env python2 + +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2011 Brett Smith +# +# 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 . + +""" +Simple tool to draw an image on the framebuffer +""" + + +import sys +import os +import glib +import cairo +import array +from PIL import Image +from PIL import ImageMath +from cStringIO import StringIO + +# Allow running from local path +path = os.path.abspath(os.path.join(os.path.abspath(os.path.dirname(sys.argv[0])), "..")) +if os.path.exists(path): + sys.path.insert(0, path) + +import gnome15.util.g15convert as g15convert +import gnome15.util.g15cairo as g15cairo +import gnome15.drivers.fb as fb + +if __name__ == "__main__": + import optparse + parser = optparse.OptionParser() + parser.add_option("-s", "--scale", dest="scale", metavar="stretch,zoom,tile,center,scale", + default="zoom" , help="Scale type") + parser.add_option("-d", "--device", dest="device", + default="/dev/fb0" , help="Framebuffer device") + (options, args) = parser.parse_args() + bg_style = options.scale + + # Check arguments + if len(args) != 1: + sys.stderr.write("You must provide a single image filenanme") + sys.exit(1) + + # Locate and configure the framebuffer + fb_dev = fb.fb_device(options.device) + var_info = fb_dev.get_var_info() + fixed_info = fb_dev.get_fixed_info() + screen_size = ( var_info.xres, var_info.yres ) + width, height = screen_size + + # Create an empty string buffer for use with monochrome LCD + empty_buf = "" + for i in range(0, fixed_info.smem_len): + empty_buf += chr(0) + + # Load the image + bg_img = args[0] + if g15cairo.is_url(bg_img) or os.path.exists(bg_img): + img_surface = g15cairo.load_surface_from_file(bg_img) + if img_surface is not None: + sx = float(width) / img_surface.get_width() + sy = float(height) / img_surface.get_height() + surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, width, height) + context = cairo.Context(surface) + context.save() + if bg_style == "zoom": + scale = max(sx, sy) + context.scale(scale, scale) + context.set_source_surface(img_surface) + context.paint() + elif bg_style == "stretch": + context.scale(sx, sy) + context.set_source_surface(img_surface) + context.paint() + elif bg_style == "scale": + x = ( width - img_surface.get_width() * sy ) / 2 + context.translate(x, 0) + context.scale(sy, sy) + context.set_source_surface(img_surface) + context.paint() + elif bg_style == "center": + x = ( width - img_surface.get_width() ) / 2 + y = ( height - img_surface.get_height() ) / 2 + context.translate(x, y) + context.set_source_surface(img_surface) + context.paint() + elif bg_style == "tile": + context.set_source_surface(img_surface) + context.paint() + y = 0 + x = img_surface.get_width() + while y < height + img_surface.get_height(): + if x >= height + img_surface.get_width(): + x = 0 + y += img_surface.get_height() + context.restore() + context.save() + context.translate(x, y) + context.set_source_surface(img_surface) + context.paint() + x += img_surface.get_width() + + context.restore() + else: + sys.stderr.write("Failed to load image file %s." % bg_img) + sys.exit(1) + else: + sys.stderr.write("Image path %s is not a URL or an existing file." % bg_img) + sys.exit(1) + + # Convert the image to the required format for this device + if var_info.bits_per_pixel == 16: + try: + back_surface = cairo.ImageSurface (4, width, height) + except Exception as e: + logger.debug("Could not create ImageSurface. Trying alternative method", exc_info = e) + # Earlier version of Cairo + back_surface = cairo.ImageSurface (cairo.FORMAT_ARGB32, width, height) + back_context = cairo.Context (back_surface) + back_context.set_source_surface(surface, 0, 0) + back_context.set_operator (cairo.OPERATOR_SOURCE); + back_context.paint() + + if back_surface.get_format() == cairo.FORMAT_ARGB32: + """ + If the creation of the type 4 image failed (i.e. earlier version of Cairo) + then we have to convert it ourselves. This is slow. + + TODO Replace with C routine + """ + file_str = StringIO() + data = back_surface.get_data() + for i in range(0, len(data), 4): + r = ord(data[i + 2]) + g = ord(data[i + 1]) + b = ord(data[i + 0]) + file_str.write(g15convert.rgb_to_uint16(r, g, b)) + buf = file_str.getvalue() + else: + buf = str(back_surface.get_data()) + else: + arrbuf = array.array('B', empty_buf) + + argb_surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, width, height) + argb_context = cairo.Context(argb_surface) + argb_context.set_source_surface(surface) + argb_context.paint() + + ''' + Now convert the ARGB to a PIL image so it can be converted to a 1 bit monochrome image, with all + colours dithered. It would be nice if Cairo could do this :( Any suggestions? + ''' + pil_img = Image.frombuffer("RGBA", (width, height), argb_surface.get_data(), "raw", "RGBA", 0, 1) + pil_img = ImageMath.eval("convert(pil_img,'1')",pil_img=pil_img) + pil_img = ImageMath.eval("convert(pil_img,'P')",pil_img=pil_img) + pil_img = pil_img.point(lambda i: i >= 250,'1') + + # Invert the screen if required + if options.invert: + pil_img = pil_img.point(lambda i: 1^i) + + # Data is 160x43, 1 byte per pixel. Will have value of 0 or 1. + data = list(pil_img.getdata()) + v = 0 + b = 1 + + # TODO Replace with C routine + for row in range(0, height): + for col in range(0, width): + if data[( row * width ) + col]: + v += b + b = b << 1 + if b == 256: + # Full byte + b = 1 + i = row * fixed_info.line_length + col / 8 + + if row > 7 and col < 96: + ''' + ????? This was discovered more by trial and error rather than any + understanding of what is going on + ''' + i -= 12 + ( 7 * fixed_info.line_length ) + + arrbuf[i] = v + v = 0 + buf = arrbuf.tostring() + + # Write to buffer + fb_dev.buffer[0:len(buf)] = buf + diff --git a/src/scripts/libg15test b/src/scripts/libg15test new file mode 100755 index 0000000..ca12b53 --- /dev/null +++ b/src/scripts/libg15test @@ -0,0 +1,86 @@ +#!/usr/bin/env python2 + +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2012 Brett Smith +# +# 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 . + +""" +Utility to test the functioning of libg15 +""" + +import gnome15.drivers.pylibg15 as libg15 +import gnome15.g15devices as g15devices +import sys + +def clamp(val, min_val, max_val): + return max(min_val, min(val, max_val)) + +print 'libg15 test\n' +print 'Choose a device:' +mono_devices = [] +for idx, device in enumerate(g15devices.find_all_devices()): + if device.bpp == 1: + mono_devices.append(device) + print ' %d %-10s - %-40s (%04x %04x)' % (idx, device.uid, device.model_fullname, device.usb_id[0],device.usb_id[1] ) + +device_idx = int(raw_input('\nDevice:')) +if device_idx < 0 or device_idx >= len(mono_devices): + sys.stderr.write('Invalid device number\n') + sys.exit(1) +device = mono_devices[device_idx] + +print "WARNING: Reset sometimes doesn't work, try without it first" +reset_usb = raw_input('Reset USB Y/(N):').lower()[:1] == 'y' + +libg15.set_debug(libg15.G15_LOG_INFO) +libg15.init(reset_usb, device.usb_id[0], device.usb_id[1]) + +print "Intialised\n" +print "WARNING: Some operations may not be appropriate for your hardware. " +print "Do try to use options your device does not have. This script is a" +print "bit dumb and won't try to stop you." + +while True: + print + print "1 - Set the keyboard backlight brightness (G15,G11)" + print "2 - Set the LCD brightness (G15,Z10?)" + print "3 - Set the LCD contrast (G15,Z10?)" + print "4 - Set the keyboard backlight color (G13,G510)" + print "5 - Set the M-Key LEDs" + print "6 - Test extra keys" + print "0 - Exit" + option = int(raw_input('\nDevice:')) + if option == 0: + break + elif option == 1: + libg15.set_keyboard_brightness(clamp(int(raw_input('Backlight brightness (0-2):')), 0, 2)) + elif option == 2: + libg15.set_lcd_brightness(clamp(int(raw_input('LCD brightness (0-2):')), 0, 2)) + elif option == 3: + libg15.set_contrast(clamp(int(raw_input('LCD contrast (0-2):')), 0, 2)) + elif option == 4: + libg15.set_keyboard_color((clamp(int(raw_input('Red (0-255):')), 0, 255), \ + clamp(int(raw_input('Green (0-255):')), 0, 255), \ + clamp(int(raw_input('Blue (0-255):')), 0, 255))) + elif option == 5: + libg15.set_leds(clamp(int(raw_input('LED mask (0-15):')), 0, 15)) + elif option == 6: + print "******************************************************************" + print "* Now testing keys, abort with Ctrl+\\" + print "******************************************************************" + def callback(code, extended_code): + print "%04x/%04x - %08d/%08d"% (code, extended_code, code, extended_code) + libg15.grab_keyboard(callback).join() + \ No newline at end of file