move drlaunch in drlaunch
authorStefanos Harhalakis <v13@v13.gr>
Sun, 20 Nov 2011 13:05:25 +0000 (13:05 +0000)
committerStefanos Harhalakis <v13@v13.gr>
Sun, 20 Nov 2011 13:05:25 +0000 (13:05 +0000)
82 files changed:
LICENSE [deleted file]
MANIFEST.in [deleted file]
README [deleted file]
drlaunch/LICENSE [new file with mode: 0644]
drlaunch/MANIFEST.in [new file with mode: 0644]
drlaunch/README [new file with mode: 0644]
drlaunch/misc/drlaunch-48.base64 [new file with mode: 0644]
drlaunch/misc/drlaunch-48.png [new file with mode: 0644]
drlaunch/misc/drlaunch-64.png [new file with mode: 0644]
drlaunch/misc/drlaunch.desktop [new file with mode: 0644]
drlaunch/misc/mkico [new file with mode: 0755]
drlaunch/mkdist [new file with mode: 0755]
drlaunch/sdist [new file with mode: 0755]
drlaunch/setup.py [new file with mode: 0755]
drlaunch/src/.gitignore [new file with mode: 0644]
drlaunch/src/0.py [new file with mode: 0755]
drlaunch/src/__init__.py [new file with mode: 0755]
drlaunch/src/about.py [new file with mode: 0755]
drlaunch/src/about0.py [new file with mode: 0644]
drlaunch/src/apps.py [new file with mode: 0755]
drlaunch/src/config.py [new file with mode: 0755]
drlaunch/src/drlaunch_widget.py [new file with mode: 0755]
drlaunch/src/icon.py [new file with mode: 0755]
drlaunch/src/icongrid.py [new file with mode: 0755]
drlaunch/src/icons.py [new file with mode: 0755]
drlaunch/src/iconw.py [new file with mode: 0644]
drlaunch/src/launcher.py [new file with mode: 0755]
drlaunch/src/portrait.py [new file with mode: 0644]
drlaunch/src/portrait.py.orig [new file with mode: 0755]
drlaunch/src/sig.py [new file with mode: 0644]
drlaunch/src/widget.py [new file with mode: 0755]
drlaunch/src/win_config.py [new file with mode: 0755]
drlaunch/src/xdg/BaseDirectory.py [new file with mode: 0644]
drlaunch/src/xdg/Config.py [new file with mode: 0644]
drlaunch/src/xdg/DesktopEntry.py [new file with mode: 0644]
drlaunch/src/xdg/Exceptions.py [new file with mode: 0644]
drlaunch/src/xdg/IconTheme.py [new file with mode: 0644]
drlaunch/src/xdg/IniFile.py [new file with mode: 0644]
drlaunch/src/xdg/Locale.py [new file with mode: 0644]
drlaunch/src/xdg/Menu.py [new file with mode: 0644]
drlaunch/src/xdg/MenuEditor.py [new file with mode: 0644]
drlaunch/src/xdg/Mime.py [new file with mode: 0644]
drlaunch/src/xdg/RecentFiles.py [new file with mode: 0644]
drlaunch/src/xdg/__init__.py [new file with mode: 0644]
misc/drlaunch-48.base64 [deleted file]
misc/drlaunch-48.png [deleted file]
misc/drlaunch-64.png [deleted file]
misc/drlaunch.desktop [deleted file]
misc/mkico [deleted file]
mkdist [deleted file]
sdist [deleted file]
setup.py [deleted file]
src/.gitignore [deleted file]
src/0.py [deleted file]
src/__init__.py [deleted file]
src/about.py [deleted file]
src/about0.py [deleted file]
src/apps.py [deleted file]
src/config.py [deleted file]
src/drlaunch_widget.py [deleted file]
src/icon.py [deleted file]
src/icongrid.py [deleted file]
src/icons.py [deleted file]
src/iconw.py [deleted file]
src/launcher.py [deleted file]
src/portrait.py [deleted file]
src/portrait.py.orig [deleted file]
src/sig.py [deleted file]
src/widget.py [deleted file]
src/win_config.py [deleted file]
src/xdg/BaseDirectory.py [deleted file]
src/xdg/Config.py [deleted file]
src/xdg/DesktopEntry.py [deleted file]
src/xdg/Exceptions.py [deleted file]
src/xdg/IconTheme.py [deleted file]
src/xdg/IniFile.py [deleted file]
src/xdg/Locale.py [deleted file]
src/xdg/Menu.py [deleted file]
src/xdg/MenuEditor.py [deleted file]
src/xdg/Mime.py [deleted file]
src/xdg/RecentFiles.py [deleted file]
src/xdg/__init__.py [deleted file]

diff --git a/LICENSE b/LICENSE
deleted file mode 100644 (file)
index 892a13d..0000000
--- a/LICENSE
+++ /dev/null
@@ -1,677 +0,0 @@
-
-                   GNU GENERAL PUBLIC LICENSE
-                      Version 3, 29 June 2007
-
- Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
- Everyone is permitted to copy and distribute verbatim copies
- of this license document, but changing it is not allowed.
-
-                           Preamble
-
-  The GNU General Public License is a free, copyleft license for
-software and other kinds of works.
-
-  The licenses for most software and other practical works are designed
-to take away your freedom to share and change the works.  By contrast,
-the GNU General Public License is intended to guarantee your freedom to
-share and change all versions of a program--to make sure it remains free
-software for all its users.  We, the Free Software Foundation, use the
-GNU General Public License for most of our software; it applies also to
-any other work released this way by its authors.  You can apply it to
-your programs, too.
-
-  When we speak of free software, we are referring to freedom, not
-price.  Our General Public Licenses are designed to make sure that you
-have the freedom to distribute copies of free software (and charge for
-them if you wish), that you receive source code or can get it if you
-want it, that you can change the software or use pieces of it in new
-free programs, and that you know you can do these things.
-
-  To protect your rights, we need to prevent others from denying you
-these rights or asking you to surrender the rights.  Therefore, you have
-certain responsibilities if you distribute copies of the software, or if
-you modify it: responsibilities to respect the freedom of others.
-
-  For example, if you distribute copies of such a program, whether
-gratis or for a fee, you must pass on to the recipients the same
-freedoms that you received.  You must make sure that they, too, receive
-or can get the source code.  And you must show them these terms so they
-know their rights.
-
-  Developers that use the GNU GPL protect your rights with two steps:
-(1) assert copyright on the software, and (2) offer you this License
-giving you legal permission to copy, distribute and/or modify it.
-
-  For the developers' and authors' protection, the GPL clearly explains
-that there is no warranty for this free software.  For both users' and
-authors' sake, the GPL requires that modified versions be marked as
-changed, so that their problems will not be attributed erroneously to
-authors of previous versions.
-
-  Some devices are designed to deny users access to install or run
-modified versions of the software inside them, although the manufacturer
-can do so.  This is fundamentally incompatible with the aim of
-protecting users' freedom to change the software.  The systematic
-pattern of such abuse occurs in the area of products for individuals to
-use, which is precisely where it is most unacceptable.  Therefore, we
-have designed this version of the GPL to prohibit the practice for those
-products.  If such problems arise substantially in other domains, we
-stand ready to extend this provision to those domains in future versions
-of the GPL, as needed to protect the freedom of users.
-
-  Finally, every program is threatened constantly by software patents.
-States should not allow patents to restrict development and use of
-software on general-purpose computers, but in those that do, we wish to
-avoid the special danger that patents applied to a free program could
-make it effectively proprietary.  To prevent this, the GPL assures that
-patents cannot be used to render the program non-free.
-
-  The precise terms and conditions for copying, distribution and
-modification follow.
-
-                      TERMS AND CONDITIONS
-
-  0. Definitions.
-
-  "This License" refers to version 3 of the GNU General Public License.
-
-  "Copyright" also means copyright-like laws that apply to other kinds of
-works, such as semiconductor masks.
-  "The Program" refers to any copyrightable work licensed under this
-License.  Each licensee is addressed as "you".  "Licensees" and
-"recipients" may be individuals or organizations.
-
-  To "modify" a work means to copy from or adapt all or part of the work
-in a fashion requiring copyright permission, other than the making of an
-exact copy.  The resulting work is called a "modified version" of the
-earlier work or a work "based on" the earlier work.
-
-  A "covered work" means either the unmodified Program or a work based
-on the Program.
-
-  To "propagate" a work means to do anything with it that, without
-permission, would make you directly or secondarily liable for
-infringement under applicable copyright law, except executing it on a
-computer or modifying a private copy.  Propagation includes copying,
-distribution (with or without modification), making available to the
-public, and in some countries other activities as well.
-
-  To "convey" a work means any kind of propagation that enables other
-parties to make or receive copies.  Mere interaction with a user through
-a computer network, with no transfer of a copy, is not conveying.
-
-  An interactive user interface displays "Appropriate Legal Notices"
-to the extent that it includes a convenient and prominently visible
-feature that (1) displays an appropriate copyright notice, and (2)
-tells the user that there is no warranty for the work (except to the
-extent that warranties are provided), that licensees may convey the
-work under this License, and how to view a copy of this License.  If
-the interface presents a list of user commands or options, such as a
-menu, a prominent item in the list meets this criterion.
-
-  1. Source Code.
-
-  The "source code" for a work means the preferred form of the work
-for making modifications to it.  "Object code" means any non-source
-form of a work.
-
-  A "Standard Interface" means an interface that either is an official
-standard defined by a recognized standards body, or, in the case of
-interfaces specified for a particular programming language, one that
-is widely used among developers working in that language.
-
-  The "System Libraries" of an executable work include anything, other
-than the work as a whole, that (a) is included in the normal form of
-packaging a Major Component, but which is not part of that Major
-Component, and (b) serves only to enable use of the work with that
-Major Component, or to implement a Standard Interface for which an
-implementation is available to the public in source code form.  A
-"Major Component", in this context, means a major essential component
-(kernel, window system, and so on) of the specific operating system
-(if any) on which the executable work runs, or a compiler used to
-produce the work, or an object code interpreter used to run it.
-
-  The "Corresponding Source" for a work in object code form means all
-the source code needed to generate, install, and (for an executable
-work) run the object code and to modify the work, including scripts to
-control those activities.  However, it does not include the work's
-System Libraries, or general-purpose tools or generally available free
-programs which are used unmodified in performing those activities but
-which are not part of the work.  For example, Corresponding Source
-includes interface definition files associated with source files for
-the work, and the source code for shared libraries and dynamically
-linked subprograms that the work is specifically designed to require,
-such as by intimate data communication or control flow between those
-subprograms and other parts of the work.
-
-  The Corresponding Source need not include anything that users
-can regenerate automatically from other parts of the Corresponding
-Source.
-
-  The Corresponding Source for a work in source code form is that
-same work.
-
-  2. Basic Permissions.
-
-  All rights granted under this License are granted for the term of
-copyright on the Program, and are irrevocable provided the stated
-conditions are met.  This License explicitly affirms your unlimited
-permission to run the unmodified Program.  The output from running a
-covered work is covered by this License only if the output, given its
-content, constitutes a covered work.  This License acknowledges your
-rights of fair use or other equivalent, as provided by copyright law.
-
-  You may make, run and propagate covered works that you do not
-convey, without conditions so long as your license otherwise remains
-in force.  You may convey covered works to others for the sole purpose
-of having them make modifications exclusively for you, or provide you
-with facilities for running those works, provided that you comply with
-the terms of this License in conveying all material for which you do
-not control copyright.  Those thus making or running the covered works
-for you must do so exclusively on your behalf, under your direction
-and control, on terms that prohibit them from making any copies of
-your copyrighted material outside their relationship with you.
-
-  Conveying under any other circumstances is permitted solely under
-the conditions stated below.  Sublicensing is not allowed; section 10
-makes it unnecessary.
-
-  3. Protecting Users' Legal Rights From Anti-Circumvention Law.
-
-  No covered work shall be deemed part of an effective technological
-measure under any applicable law fulfilling obligations under article
-11 of the WIPO copyright treaty adopted on 20 December 1996, or
-similar laws prohibiting or restricting circumvention of such
-measures.
-
-  When you convey a covered work, you waive any legal power to forbid
-circumvention of technological measures to the extent such circumvention
-is effected by exercising rights under this License with respect to
-the covered work, and you disclaim any intention to limit operation or
-modification of the work as a means of enforcing, against the work's
-users, your or third parties' legal rights to forbid circumvention of
-technological measures.
-
-  4. Conveying Verbatim Copies.
-
-  You may convey verbatim copies of the Program's source code as you
-receive it, in any medium, provided that you conspicuously and
-appropriately publish on each copy an appropriate copyright notice;
-keep intact all notices stating that this License and any
-non-permissive terms added in accord with section 7 apply to the code;
-keep intact all notices of the absence of any warranty; and give all
-recipients a copy of this License along with the Program.
-
-  You may charge any price or no price for each copy that you convey,
-and you may offer support or warranty protection for a fee.
-
-  5. Conveying Modified Source Versions.
-
-  You may convey a work based on the Program, or the modifications to
-produce it from the Program, in the form of source code under the
-terms of section 4, provided that you also meet all of these conditions:
-
-    a) The work must carry prominent notices stating that you modified
-    it, and giving a relevant date.
-
-    b) The work must carry prominent notices stating that it is
-    released under this License and any conditions added under section
-    7.  This requirement modifies the requirement in section 4 to
-    "keep intact all notices".
-
-    c) You must license the entire work, as a whole, under this
-    License to anyone who comes into possession of a copy.  This
-    License will therefore apply, along with any applicable section 7
-    additional terms, to the whole of the work, and all its parts,
-    regardless of how they are packaged.  This License gives no
-    permission to license the work in any other way, but it does not
-    invalidate such permission if you have separately received it.
-
-    d) If the work has interactive user interfaces, each must display
-    Appropriate Legal Notices; however, if the Program has interactive
-    interfaces that do not display Appropriate Legal Notices, your
-    work need not make them do so.
-
-  A compilation of a covered work with other separate and independent
-works, which are not by their nature extensions of the covered work,
-and which are not combined with it such as to form a larger program,
-in or on a volume of a storage or distribution medium, is called an
-"aggregate" if the compilation and its resulting copyright are not
-used to limit the access or legal rights of the compilation's users
-beyond what the individual works permit.  Inclusion of a covered work
-in an aggregate does not cause this License to apply to the other
-parts of the aggregate.
-
-  6. Conveying Non-Source Forms.
-
-  You may convey a covered work in object code form under the terms
-of sections 4 and 5, provided that you also convey the
-machine-readable Corresponding Source under the terms of this License,
-in one of these ways:
-
-    a) Convey the object code in, or embodied in, a physical product
-    (including a physical distribution medium), accompanied by the
-    Corresponding Source fixed on a durable physical medium
-    customarily used for software interchange.
-
-    b) Convey the object code in, or embodied in, a physical product
-    (including a physical distribution medium), accompanied by a
-    written offer, valid for at least three years and valid for as
-    long as you offer spare parts or customer support for that product
-    model, to give anyone who possesses the object code either (1) a
-    copy of the Corresponding Source for all the software in the
-    product that is covered by this License, on a durable physical
-    medium customarily used for software interchange, for a price no
-    more than your reasonable cost of physically performing this
-    conveying of source, or (2) access to copy the
-    Corresponding Source from a network server at no charge.
-
-    c) Convey individual copies of the object code with a copy of the
-    written offer to provide the Corresponding Source.  This
-    alternative is allowed only occasionally and noncommercially, and
-    only if you received the object code with such an offer, in accord
-    with subsection 6b.
-
-    d) Convey the object code by offering access from a designated
-    place (gratis or for a charge), and offer equivalent access to the
-    Corresponding Source in the same way through the same place at no
-    further charge.  You need not require recipients to copy the
-    Corresponding Source along with the object code.  If the place to
-    copy the object code is a network server, the Corresponding Source
-    may be on a different server (operated by you or a third party)
-    that supports equivalent copying facilities, provided you maintain
-    clear directions next to the object code saying where to find the
-    Corresponding Source.  Regardless of what server hosts the
-    Corresponding Source, you remain obligated to ensure that it is
-    available for as long as needed to satisfy these requirements.
-
-    e) Convey the object code using peer-to-peer transmission, provided
-    you inform other peers where the object code and Corresponding
-    Source of the work are being offered to the general public at no
-    charge under subsection 6d.
-
-  A separable portion of the object code, whose source code is excluded
-from the Corresponding Source as a System Library, need not be
-included in conveying the object code work.
-
-  A "User Product" is either (1) a "consumer product", which means any
-tangible personal property which is normally used for personal, family,
-or household purposes, or (2) anything designed or sold for incorporation
-into a dwelling.  In determining whether a product is a consumer product,
-doubtful cases shall be resolved in favor of coverage.  For a particular
-product received by a particular user, "normally used" refers to a
-typical or common use of that class of product, regardless of the status
-of the particular user or of the way in which the particular user
-actually uses, or expects or is expected to use, the product.  A product
-is a consumer product regardless of whether the product has substantial
-commercial, industrial or non-consumer uses, unless such uses represent
-the only significant mode of use of the product.
-
-  "Installation Information" for a User Product means any methods,
-procedures, authorization keys, or other information required to install
-and execute modified versions of a covered work in that User Product from
-a modified version of its Corresponding Source.  The information must
-suffice to ensure that the continued functioning of the modified object
-code is in no case prevented or interfered with solely because
-modification has been made.
-
-  If you convey an object code work under this section in, or with, or
-specifically for use in, a User Product, and the conveying occurs as
-part of a transaction in which the right of possession and use of the
-User Product is transferred to the recipient in perpetuity or for a
-fixed term (regardless of how the transaction is characterized), the
-Corresponding Source conveyed under this section must be accompanied
-by the Installation Information.  But this requirement does not apply
-if neither you nor any third party retains the ability to install
-modified object code on the User Product (for example, the work has
-been installed in ROM).
-
-  The requirement to provide Installation Information does not include a
-requirement to continue to provide support service, warranty, or updates
-for a work that has been modified or installed by the recipient, or for
-the User Product in which it has been modified or installed.  Access to a
-network may be denied when the modification itself materially and
-adversely affects the operation of the network or violates the rules and
-protocols for communication across the network.
-
-  Corresponding Source conveyed, and Installation Information provided,
-in accord with this section must be in a format that is publicly
-documented (and with an implementation available to the public in
-source code form), and must require no special password or key for
-unpacking, reading or copying.
-
-  7. Additional Terms.
-
-  "Additional permissions" are terms that supplement the terms of this
-License by making exceptions from one or more of its conditions.
-Additional permissions that are applicable to the entire Program shall
-be treated as though they were included in this License, to the extent
-that they are valid under applicable law.  If additional permissions
-apply only to part of the Program, that part may be used separately
-under those permissions, but the entire Program remains governed by
-this License without regard to the additional permissions.
-
-  When you convey a copy of a covered work, you may at your option
-remove any additional permissions from that copy, or from any part of
-it.  (Additional permissions may be written to require their own
-removal in certain cases when you modify the work.)  You may place
-additional permissions on material, added by you to a covered work,
-for which you have or can give appropriate copyright permission.
-
-  Notwithstanding any other provision of this License, for material you
-add to a covered work, you may (if authorized by the copyright holders of
-that material) supplement the terms of this License with terms:
-
-    a) Disclaiming warranty or limiting liability differently from the
-    terms of sections 15 and 16 of this License; or
-
-    b) Requiring preservation of specified reasonable legal notices or
-    author attributions in that material or in the Appropriate Legal
-    Notices displayed by works containing it; or
-
-    c) Prohibiting misrepresentation of the origin of that material, or
-    requiring that modified versions of such material be marked in
-    reasonable ways as different from the original version; or
-
-    d) Limiting the use for publicity purposes of names of licensors or
-    authors of the material; or
-
-    e) Declining to grant rights under trademark law for use of some
-    trade names, trademarks, or service marks; or
-
-    f) Requiring indemnification of licensors and authors of that
-    material by anyone who conveys the material (or modified versions of
-    it) with contractual assumptions of liability to the recipient, for
-    any liability that these contractual assumptions directly impose on
-    those licensors and authors.
-
-  All other non-permissive additional terms are considered "further
-restrictions" within the meaning of section 10.  If the Program as you
-received it, or any part of it, contains a notice stating that it is
-governed by this License along with a term that is a further
-restriction, you may remove that term.  If a license document contains
-a further restriction but permits relicensing or conveying under this
-License, you may add to a covered work material governed by the terms
-of that license document, provided that the further restriction does
-not survive such relicensing or conveying.
-
-  If you add terms to a covered work in accord with this section, you
-must place, in the relevant source files, a statement of the
-additional terms that apply to those files, or a notice indicating
-where to find the applicable terms.
-
-  Additional terms, permissive or non-permissive, may be stated in the
-form of a separately written license, or stated as exceptions;
-the above requirements apply either way.
-
-  8. Termination.
-
-  You may not propagate or modify a covered work except as expressly
-provided under this License.  Any attempt otherwise to propagate or
-modify it is void, and will automatically terminate your rights under
-this License (including any patent licenses granted under the third
-paragraph of section 11).
-
-  However, if you cease all violation of this License, then your
-license from a particular copyright holder is reinstated (a)
-provisionally, unless and until the copyright holder explicitly and
-finally terminates your license, and (b) permanently, if the copyright
-holder fails to notify you of the violation by some reasonable means
-prior to 60 days after the cessation.
-
-  Moreover, your license from a particular copyright holder is
-reinstated permanently if the copyright holder notifies you of the
-violation by some reasonable means, this is the first time you have
-received notice of violation of this License (for any work) from that
-copyright holder, and you cure the violation prior to 30 days after
-your receipt of the notice.
-
-  Termination of your rights under this section does not terminate the
-licenses of parties who have received copies or rights from you under
-this License.  If your rights have been terminated and not permanently
-reinstated, you do not qualify to receive new licenses for the same
-material under section 10.
-
-  9. Acceptance Not Required for Having Copies.
-
-  You are not required to accept this License in order to receive or
-run a copy of the Program.  Ancillary propagation of a covered work
-occurring solely as a consequence of using peer-to-peer transmission
-to receive a copy likewise does not require acceptance.  However,
-nothing other than this License grants you permission to propagate or
-modify any covered work.  These actions infringe copyright if you do
-not accept this License.  Therefore, by modifying or propagating a
-covered work, you indicate your acceptance of this License to do so.
-
-  10. Automatic Licensing of Downstream Recipients.
-
-  Each time you convey a covered work, the recipient automatically
-receives a license from the original licensors, to run, modify and
-propagate that work, subject to this License.  You are not responsible
-for enforcing compliance by third parties with this License.
-
-  An "entity transaction" is a transaction transferring control of an
-organization, or substantially all assets of one, or subdividing an
-organization, or merging organizations.  If propagation of a covered
-work results from an entity transaction, each party to that
-transaction who receives a copy of the work also receives whatever
-licenses to the work the party's predecessor in interest had or could
-give under the previous paragraph, plus a right to possession of the
-Corresponding Source of the work from the predecessor in interest, if
-the predecessor has it or can get it with reasonable efforts.
-
-  You may not impose any further restrictions on the exercise of the
-rights granted or affirmed under this License.  For example, you may
-not impose a license fee, royalty, or other charge for exercise of
-rights granted under this License, and you may not initiate litigation
-(including a cross-claim or counterclaim in a lawsuit) alleging that
-any patent claim is infringed by making, using, selling, offering for
-sale, or importing the Program or any portion of it.
-
-  11. Patents.
-
-  A "contributor" is a copyright holder who authorizes use under this
-License of the Program or a work on which the Program is based.  The
-work thus licensed is called the contributor's "contributor version".
-
-  A contributor's "essential patent claims" are all patent claims
-owned or controlled by the contributor, whether already acquired or
-hereafter acquired, that would be infringed by some manner, permitted
-by this License, of making, using, or selling its contributor version,
-but do not include claims that would be infringed only as a
-consequence of further modification of the contributor version.  For
-purposes of this definition, "control" includes the right to grant
-patent sublicenses in a manner consistent with the requirements of
-this License.
-
-  Each contributor grants you a non-exclusive, worldwide, royalty-free
-patent license under the contributor's essential patent claims, to
-make, use, sell, offer for sale, import and otherwise run, modify and
-propagate the contents of its contributor version.
-
-  In the following three paragraphs, a "patent license" is any express
-agreement or commitment, however denominated, not to enforce a patent
-(such as an express permission to practice a patent or covenant not to
-sue for patent infringement).  To "grant" such a patent license to a
-party means to make such an agreement or commitment not to enforce a
-patent against the party.
-
-  If you convey a covered work, knowingly relying on a patent license,
-and the Corresponding Source of the work is not available for anyone
-to copy, free of charge and under the terms of this License, through a
-publicly available network server or other readily accessible means,
-then you must either (1) cause the Corresponding Source to be so
-available, or (2) arrange to deprive yourself of the benefit of the
-patent license for this particular work, or (3) arrange, in a manner
-consistent with the requirements of this License, to extend the patent
-license to downstream recipients.  "Knowingly relying" means you have
-actual knowledge that, but for the patent license, your conveying the
-covered work in a country, or your recipient's use of the covered work
-in a country, would infringe one or more identifiable patents in that
-country that you have reason to believe are valid.
-  
-  If, pursuant to or in connection with a single transaction or
-arrangement, you convey, or propagate by procuring conveyance of, a
-covered work, and grant a patent license to some of the parties
-receiving the covered work authorizing them to use, propagate, modify
-or convey a specific copy of the covered work, then the patent license
-you grant is automatically extended to all recipients of the covered
-work and works based on it.
-
-  A patent license is "discriminatory" if it does not include within
-the scope of its coverage, prohibits the exercise of, or is
-conditioned on the non-exercise of one or more of the rights that are
-specifically granted under this License.  You may not convey a covered
-work if you are a party to an arrangement with a third party that is
-in the business of distributing software, under which you make payment
-to the third party based on the extent of your activity of conveying
-the work, and under which the third party grants, to any of the
-parties who would receive the covered work from you, a discriminatory
-patent license (a) in connection with copies of the covered work
-conveyed by you (or copies made from those copies), or (b) primarily
-for and in connection with specific products or compilations that
-contain the covered work, unless you entered into that arrangement,
-or that patent license was granted, prior to 28 March 2007.
-
-  Nothing in this License shall be construed as excluding or limiting
-any implied license or other defenses to infringement that may
-otherwise be available to you under applicable patent law.
-
-  12. No Surrender of Others' Freedom.
-
-  If conditions are imposed on you (whether by court order, agreement or
-otherwise) that contradict the conditions of this License, they do not
-excuse you from the conditions of this License.  If you cannot convey a
-covered work so as to satisfy simultaneously your obligations under this
-License and any other pertinent obligations, then as a consequence you may
-not convey it at all.  For example, if you agree to terms that obligate you
-to collect a royalty for further conveying from those to whom you convey
-the Program, the only way you could satisfy both those terms and this
-License would be to refrain entirely from conveying the Program.
-
-  13. Use with the GNU Affero General Public License.
-
-  Notwithstanding any other provision of this License, you have
-permission to link or combine any covered work with a work licensed
-under version 3 of the GNU Affero General Public License into a single
-combined work, and to convey the resulting work.  The terms of this
-License will continue to apply to the part which is the covered work,
-but the special requirements of the GNU Affero General Public License,
-section 13, concerning interaction through a network will apply to the
-combination as such.
-
-  14. Revised Versions of this License.
-
-  The Free Software Foundation may publish revised and/or new versions of
-the GNU General Public License from time to time.  Such new versions will
-be similar in spirit to the present version, but may differ in detail to
-address new problems or concerns.
-
-  Each version is given a distinguishing version number.  If the
-Program specifies that a certain numbered version of the GNU General
-Public License "or any later version" applies to it, you have the
-option of following the terms and conditions either of that numbered
-version or of any later version published by the Free Software
-Foundation.  If the Program does not specify a version number of the
-GNU General Public License, you may choose any version ever published
-by the Free Software Foundation.
-
-  If the Program specifies that a proxy can decide which future
-versions of the GNU General Public License can be used, that proxy's
-public statement of acceptance of a version permanently authorizes you
-to choose that version for the Program.
-
-  Later license versions may give you additional or different
-permissions.  However, no additional obligations are imposed on any
-author or copyright holder as a result of your choosing to follow a
-later version.
-
-  15. Disclaimer of Warranty.
-
-  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
-APPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
-HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
-OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
-THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
-PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
-IS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
-ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
-
-  16. Limitation of Liability.
-
-  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
-WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
-THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
-GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
-USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
-DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
-PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
-EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
-SUCH DAMAGES.
-
-  17. Interpretation of Sections 15 and 16.
-
-  If the disclaimer of warranty and limitation of liability provided
-above cannot be given local legal effect according to their terms,
-reviewing courts shall apply local law that most closely approximates
-an absolute waiver of all civil liability in connection with the
-Program, unless a warranty or assumption of liability accompanies a
-copy of the Program in return for a fee.
-
-                    END OF TERMS AND CONDITIONS
-
-           How to Apply These Terms to Your New Programs
-
-  If you develop a new program, and you want it to be of the greatest
-possible use to the public, the best way to achieve this is to make it
-free software which everyone can redistribute and change under these terms.
-
-  To do so, attach the following notices to the program.  It is safest
-to attach them to the start of each source file to most effectively
-state the exclusion of warranty; and each file should have at least
-the "copyright" line and a pointer to where the full notice is found.
-
-    <one line to give the program's name and a brief idea of what it does.>
-    Copyright (C) <year>  <name of author>
-
-    This program is free software: you can redistribute it and/or modify
-    it under the terms of the GNU General Public License as published by
-    the Free Software Foundation, either version 3 of the License, or
-    (at your option) any later version.
-
-    This program is distributed in the hope that it will be useful,
-    but WITHOUT ANY WARRANTY; without even the implied warranty of
-    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-    GNU General Public License for more details.
-
-    You should have received a copy of the GNU General Public License
-    along with this program.  If not, see <http://www.gnu.org/licenses/>.
-
-Also add information on how to contact you by electronic and paper mail.
-
-  If the program does terminal interaction, make it output a short
-notice like this when it starts in an interactive mode:
-
-    <program>  Copyright (C) <year>  <name of author>
-    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
-    This is free software, and you are welcome to redistribute it
-    under certain conditions; type `show c' for details.
-
-The hypothetical commands `show w' and `show c' should show the appropriate
-parts of the General Public License.  Of course, your program's commands
-might be different; for a GUI interface, you would use an "about box".
-
-  You should also get your employer (if you work as a programmer) or school,
-if any, to sign a "copyright disclaimer" for the program, if necessary.
-For more information on this, and how to apply and follow the GNU GPL, see
-<http://www.gnu.org/licenses/>.
-
-  The GNU General Public License does not permit incorporating your program
-into proprietary programs.  If your program is a subroutine library, you
-may consider it more useful to permit linking proprietary applications with
-the library.  If this is what you want to do, use the GNU Lesser General
-Public License instead of this License.  But first, please read
-<http://www.gnu.org/philosophy/why-not-lgpl.html>.
-
-
diff --git a/MANIFEST.in b/MANIFEST.in
deleted file mode 100644 (file)
index a8bc4ad..0000000
+++ /dev/null
@@ -1,3 +0,0 @@
-include README LICENSE drlaunch_widget.py
-recursive-include src *.py
-recursive-include misc *
diff --git a/README b/README
deleted file mode 100644 (file)
index e69de29..0000000
diff --git a/drlaunch/LICENSE b/drlaunch/LICENSE
new file mode 100644 (file)
index 0000000..892a13d
--- /dev/null
@@ -0,0 +1,677 @@
+
+                   GNU GENERAL PUBLIC LICENSE
+                      Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+                           Preamble
+
+  The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+  The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works.  By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users.  We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors.  You can apply it to
+your programs, too.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+  To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights.  Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+  For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received.  You must make sure that they, too, receive
+or can get the source code.  And you must show them these terms so they
+know their rights.
+
+  Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+  For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software.  For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+  Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so.  This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software.  The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable.  Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products.  If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+  Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary.  To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+                      TERMS AND CONDITIONS
+
+  0. Definitions.
+
+  "This License" refers to version 3 of the GNU General Public License.
+
+  "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+  "The Program" refers to any copyrightable work licensed under this
+License.  Each licensee is addressed as "you".  "Licensees" and
+"recipients" may be individuals or organizations.
+
+  To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy.  The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+  A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+  To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy.  Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+  To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies.  Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+  An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License.  If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+  1. Source Code.
+
+  The "source code" for a work means the preferred form of the work
+for making modifications to it.  "Object code" means any non-source
+form of a work.
+
+  A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+  The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form.  A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+  The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities.  However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work.  For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+  The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+  The Corresponding Source for a work in source code form is that
+same work.
+
+  2. Basic Permissions.
+
+  All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met.  This License explicitly affirms your unlimited
+permission to run the unmodified Program.  The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work.  This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+  You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force.  You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright.  Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+  Conveying under any other circumstances is permitted solely under
+the conditions stated below.  Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+  3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+  No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+  When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+  4. Conveying Verbatim Copies.
+
+  You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+  You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+  5. Conveying Modified Source Versions.
+
+  You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+    a) The work must carry prominent notices stating that you modified
+    it, and giving a relevant date.
+
+    b) The work must carry prominent notices stating that it is
+    released under this License and any conditions added under section
+    7.  This requirement modifies the requirement in section 4 to
+    "keep intact all notices".
+
+    c) You must license the entire work, as a whole, under this
+    License to anyone who comes into possession of a copy.  This
+    License will therefore apply, along with any applicable section 7
+    additional terms, to the whole of the work, and all its parts,
+    regardless of how they are packaged.  This License gives no
+    permission to license the work in any other way, but it does not
+    invalidate such permission if you have separately received it.
+
+    d) If the work has interactive user interfaces, each must display
+    Appropriate Legal Notices; however, if the Program has interactive
+    interfaces that do not display Appropriate Legal Notices, your
+    work need not make them do so.
+
+  A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit.  Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+  6. Conveying Non-Source Forms.
+
+  You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+    a) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by the
+    Corresponding Source fixed on a durable physical medium
+    customarily used for software interchange.
+
+    b) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by a
+    written offer, valid for at least three years and valid for as
+    long as you offer spare parts or customer support for that product
+    model, to give anyone who possesses the object code either (1) a
+    copy of the Corresponding Source for all the software in the
+    product that is covered by this License, on a durable physical
+    medium customarily used for software interchange, for a price no
+    more than your reasonable cost of physically performing this
+    conveying of source, or (2) access to copy the
+    Corresponding Source from a network server at no charge.
+
+    c) Convey individual copies of the object code with a copy of the
+    written offer to provide the Corresponding Source.  This
+    alternative is allowed only occasionally and noncommercially, and
+    only if you received the object code with such an offer, in accord
+    with subsection 6b.
+
+    d) Convey the object code by offering access from a designated
+    place (gratis or for a charge), and offer equivalent access to the
+    Corresponding Source in the same way through the same place at no
+    further charge.  You need not require recipients to copy the
+    Corresponding Source along with the object code.  If the place to
+    copy the object code is a network server, the Corresponding Source
+    may be on a different server (operated by you or a third party)
+    that supports equivalent copying facilities, provided you maintain
+    clear directions next to the object code saying where to find the
+    Corresponding Source.  Regardless of what server hosts the
+    Corresponding Source, you remain obligated to ensure that it is
+    available for as long as needed to satisfy these requirements.
+
+    e) Convey the object code using peer-to-peer transmission, provided
+    you inform other peers where the object code and Corresponding
+    Source of the work are being offered to the general public at no
+    charge under subsection 6d.
+
+  A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+  A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling.  In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage.  For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product.  A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+  "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source.  The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+  If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information.  But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+  The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed.  Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+  Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+  7. Additional Terms.
+
+  "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law.  If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+  When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it.  (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.)  You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+  Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+    a) Disclaiming warranty or limiting liability differently from the
+    terms of sections 15 and 16 of this License; or
+
+    b) Requiring preservation of specified reasonable legal notices or
+    author attributions in that material or in the Appropriate Legal
+    Notices displayed by works containing it; or
+
+    c) Prohibiting misrepresentation of the origin of that material, or
+    requiring that modified versions of such material be marked in
+    reasonable ways as different from the original version; or
+
+    d) Limiting the use for publicity purposes of names of licensors or
+    authors of the material; or
+
+    e) Declining to grant rights under trademark law for use of some
+    trade names, trademarks, or service marks; or
+
+    f) Requiring indemnification of licensors and authors of that
+    material by anyone who conveys the material (or modified versions of
+    it) with contractual assumptions of liability to the recipient, for
+    any liability that these contractual assumptions directly impose on
+    those licensors and authors.
+
+  All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10.  If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term.  If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+  If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+  Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+  8. Termination.
+
+  You may not propagate or modify a covered work except as expressly
+provided under this License.  Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+  However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+  Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+  Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License.  If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+  9. Acceptance Not Required for Having Copies.
+
+  You are not required to accept this License in order to receive or
+run a copy of the Program.  Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance.  However,
+nothing other than this License grants you permission to propagate or
+modify any covered work.  These actions infringe copyright if you do
+not accept this License.  Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+  10. Automatic Licensing of Downstream Recipients.
+
+  Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License.  You are not responsible
+for enforcing compliance by third parties with this License.
+
+  An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations.  If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+  You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License.  For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+  11. Patents.
+
+  A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based.  The
+work thus licensed is called the contributor's "contributor version".
+
+  A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version.  For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+  Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+  In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement).  To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+  If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients.  "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+  
+  If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+  A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License.  You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+  Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+  12. No Surrender of Others' Freedom.
+
+  If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all.  For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+  13. Use with the GNU Affero General Public License.
+
+  Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work.  The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+  14. Revised Versions of this License.
+
+  The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time.  Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+  Each version is given a distinguishing version number.  If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation.  If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+  If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+  Later license versions may give you additional or different
+permissions.  However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+  15. Disclaimer of Warranty.
+
+  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+  16. Limitation of Liability.
+
+  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+  17. Interpretation of Sections 15 and 16.
+
+  If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+                    END OF TERMS AND CONDITIONS
+
+           How to Apply These Terms to Your New Programs
+
+  If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+  To do so, attach the following notices to the program.  It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+Also add information on how to contact you by electronic and paper mail.
+
+  If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+    <program>  Copyright (C) <year>  <name of author>
+    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+    This is free software, and you are welcome to redistribute it
+    under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License.  Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+  You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+<http://www.gnu.org/licenses/>.
+
+  The GNU General Public License does not permit incorporating your program
+into proprietary programs.  If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library.  If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.  But first, please read
+<http://www.gnu.org/philosophy/why-not-lgpl.html>.
+
+
diff --git a/drlaunch/MANIFEST.in b/drlaunch/MANIFEST.in
new file mode 100644 (file)
index 0000000..a8bc4ad
--- /dev/null
@@ -0,0 +1,3 @@
+include README LICENSE drlaunch_widget.py
+recursive-include src *.py
+recursive-include misc *
diff --git a/drlaunch/README b/drlaunch/README
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/drlaunch/misc/drlaunch-48.base64 b/drlaunch/misc/drlaunch-48.base64
new file mode 100644 (file)
index 0000000..47fd8d7
--- /dev/null
@@ -0,0 +1,124 @@
+begin-base64 644 drlaunch-48.png
+iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAABHNCSVQICAgI
+fAhkiAAAAAlwSFlzAAADOgAAAzoBWYNI0gAAABl0RVh0U29mdHdhcmUAd3d3
+Lmlua3NjYXBlLm9yZ5vuPBoAABTMSURBVGiBfZp7jCTXdd5/dep2dfW7e3pm
+Z2aH+9ZyuVy+SVEiRdGULJMSxTCBEtiyHEmUIzkRJASxDdiKg4Q0HCGWYSCw
+HQUGpBgCIcs0JAc2YjhKINA0RSzlXZErkibXpLxe7mN2dnYePf2qrr5961T+
+uD2zXJHIHTR6pqv71r3nfuf7vnN6AuAJAMRgCkUMGQCqyg3HbqSrdZbPvoKx
+I/wQdGYRXT8HCKaxBBMLmnL9/iVe6wmufZAHbtuHXDnHMxe3uO/wYbaimF0h
+PPP0X7Nn9y5WxsqDh+aw6gCIQ7AqkAsiMJgoiVNEoG5Ac4PNQAS0vZ+4FLPp
+ILjjY5/PT794kv3VIaViCWNiUIdTxySMkaiASpFS2oNcARgFhnY1ZmNrk1IY
+QRhBrmgAzev2c7ZjubBuufezv85zf/o1vvTlJ3jx61/hwsYKtWKEjJVd7RYS
+CHaSEBmhFIKIIcCQ5UDuGKlDFaIALqURCyXh8hgGLiW64R4+9HOfI5DQ5Efv
+uhutzOJWzmLiGoz7OAIKcZnRqEurVmE0dFSaMwxTpVKBja2E9kyV4UYP4jqV
+SOmuLdNot1lYuo4zKwPOrFvu++mHeeHk07yrsEXuLJ0E4nwCi4fYt7TE+X6P
+exarKBAHhrhguJLCxjChXRbqIXQcOCI0F9YtmEmPpLOKu+0Rwpl264nFhQWG
+E4VQ0MEGqhPIHFpsImEROymg4hiPEmgvkK5foVE2hHZCYh1qu4wHW5AL47FC
+YIgmA85fXuHCUKlvvklMSlCcIZuM0IVDHG7XOHfpMlGjzWQyoVUqkmSQqYdQ
+IwrZmAT0HJTCkHEWUC/kDGyOhIZ64DC1GYxgEMAkPdyoCyI4pxgMZrgGOdA+
+QDqyyHiE664TT0bEUkbqTbSQUJuk9HsbCIrJHN3NLUoRNO58hP7JP0da89Tq
+ZTr9ASaIaGRDtpIc01+mWYuwCBcGXcpG6OXqT0OEOIDNrR7tw0fRsSMWQ90o
+NjBcmRQYbVkMOR7D1SakQ5xLAQMh04xRSDY5tLSLyAiCQd0M5GDKZZYvniNe
+XEICS7/ThUIE4yFEFfp//XXYew/ajKARQ2cTXMb773o/t992O71BD0E8aaBI
+Lmjg80xyIa7EnDx5kiQHQsEIGCNc6jtmIoNWDAaAScK+Vp0zlx1gEANkDooN
+MAZ6Xb715F+RDBOsddjUMRr758e/8gTrm5v0t7oYiTwjBX5J99/7Ps50HenK
+afpuEdwYAkOr1WJsxzz5zSeRTNgeWZ6hU6IggH1L+zh48CDJcELNCAaoCVQL
+EAfCKy//AFEDk8mE18+dh6hAHEVEhQoEQpp0sYHBitKo1CATojAmimOKpYhC
+KSLpp4y31ljcfysaGtKoSBpAHpV55pnvcuHUM0TlNm7cI81SUjdAjPDQQw/x
+6MOPMrTDnYfLHDL90UxJJyljHaN5ykIJJIQJymLsuJz0aR+5DUN11u94tEFr
+YR/9YcIX/gW8+1iJI3sCim6IHUUA9HpbxMUqmKtRi4wipRqDceLDVvCUyngI
+xJBbdNwBMRDGyDTCUSHiEz//Cf7yf/8lw2TIOw2fDRAJLA+VSkGoGKFroVws
+sjpKkSh3OwLVubIMoz4AexZiTp9J33FigOHQ39T2B+igw3iQQjZChhugDkwR
+jj0ACIwGIJEXwbjt7xYIe/bu4f7334+IvOM9tvNjY6QUpznQncDAOrrDlEY5
+QuzEUWnOeS4KDNpcRNUxPyMstiPOrVjGFpwqmisOhwQhF8+fRxBcoLiohusv
+017YR2NmDuMygjCCv/seBA4NDJI7TGiYLynkV6P7i5/6RW46dhNGfDqqKqrq
+gRQIhaDAfCWiarxaz8ewpxpzQ7vKexaqiGSKm1hMqQHqKGUJqhMkz1mcLbOx
+pVxed37iXFEcEoBMI3Tk8FHsYAOnysbyOUZjxRgDdgCzRyCPUHWICMZZuhtd
+v4Hczzc/P89jn3qM+dl5j/3pBgBE/Cai0G/OBLBlIc0MayPlhU2HME5Q56C6
+CynW6HbWkDDaOcY7jsYMR37CmfYM5bgMwO7FPQA89tgv8bGPfRLnLFRblHa9
+y38wEKR/EUQ9jADiGMKrcNk+hX179/HIP3mEYlS8FkO5fxo4iEJwCqsjy0wR
+yoUIsWOEQImrdRisI6YMcQvN9FosTm9aqVaIpjdpNJsAfOPJ/87rp3+IKUQw
+7DA6d8prhxh0PMKvcUqtqfXXtjcwPQWnjjvvvJM777pzSsHTMf09FkjVy9Ji
+HNGbQCWEcrWCcZU6/Y0V0tEmRgyxCMbUcBKiOmSUCE4t7oXPkskenj014m/e
+UDRzEEaceP44susIc8fup/Pas6SZA4RYDFKI0ElCVFnAZQ4rgjrr4SCGiIie
+65FOUjLN+NDPfIjNziZvvP4GTh02s4x1zGBi6VjDUsUwE8NLmynxJGVfyWEY
+bML8DD5UCtU24HaC8NI/Drjj6BGi3t8h6Yucednxve+D6nShJoL1H9PfOAuF
+Coz7/vN2iLoUWksw7kFcRp1FAtmJrObThHbKOBsjgfC+e99Hd6vL8sryDguV
+C9AsGGzuV3agDr0urKYgIG85NoFBB4pVAF57M+XI/hgmK1Ba9G9xDuIGjb23
++g0XKhC3ICpCNvaUaKY5JAI3PTCdX67CIvD4VvULV6cwATuxRCbijtvuoFar
+7QRxy/qprELifC7kwFwM0jxwjFBCTGAgV0+Xoy02NhwhhlbdIIHi3ADiRc9C
+oz7DC69iggiXTXDqcKEBVSQ3mEwJCiVQC8/+Cc6lSOBhI+U25OBy55NY/UZs
+ZknHKYN0AEC9UYfQ60WzYLbxQTWEKDBUQkOraDCDzR6Li2VMbnD1Gq7X5W9f
+HnN5dcSuVsT3TiTgDG68CnmNU1fmMdEYMiBzuCxFd+1BL5/H5HgKnQDOAjE0
+W96oBYLZfRjSFDLY6mzR6/ZI+gmjoU/2uBgzPztPuVqm2W5SLpV58+ybrCcj
+zASqRXA5DJwhzw27CgZTrtY9ZgM8sLAcP+U4ERhMOM2F3GFzg+Z9yJW41PLx
+yBwuc9DfQnTszzV3UDQQGq/IvQ1oe/Vl8yKNxb089e2nePr7z0BcJlIBm9Ka
+m2djfYVyo0FqyhyaqXJ2PcGNuizt3rMNdqzCrhg2Uw8rk3bWoNbyGwiNBxgC
+gUJYhPEIhB1xAeN9jil5TneAS71YBV5wyBQyC3uPwNYVxExfL87gTERUrnPm
+/GVqC/sZdzdhkmDWNkl7axBV2HPj7SwPoFYvMzNbRwDN/QNg3SpxCK/3QGbf
++zDD0RANFdQS1+aJjSCmRGr7pFhStcRGiEs1YiOkuSWd9EkJiItl5mZ3ezaq
+1rCZhYLQsQbiNsZaKJSJyhGyaw76V+hvrnHs5jvZu7hIunUBOx6SJAMQw1x7
+lpWXf4C6hDh0vLTew+UpIpBmUA6hHFh66YB7bj1MIA98Jm+/9l12X7dAqVj0
+nkR80kzGV11isVCcMokynox3ip1Wa55ksOVpsVBmknZJkpR/vHCRsVVoHIKD
+N3Bo/SSLC4uMAkPRelqZn5/n/MU3vbgFIeVikaBYQpyilSpGHUSGmbhEVDC4
+DPJACSXjyA238K+++KsEBJKTC1FsqNUqCBEUykhrHrv6+lRVBVOsg00AwQV4
+fKvzRUwgUCijuYOCoXvpAhqVUNsluu52dO1NKMRUdu9DSruJLr+AjhMQg8vs
+TrdDAoOU6pibH+DxTz3CN/++wydumud81/JTS2U6I3ABLDbLzNxyH9fXIACT
+i5kqZAgStVDb9UmaG8i9TkihgFavg86P/c0KJXQyRMptNOn4nCGCwMHMHsyw
+h0uHPrFFMSZCKaLNJtFggAsN2l1FoirkGepGSGDQ625k7tCtbMzs4ff/w68g
+ofKdly7x+IPv4kC1RBxCO4LhBH7vDTCId3+lco1ypeytbGsJ0tQnX65QruMG
+PWS2jQZbGNRHPa+iUYTGAjkIBrnuMJ3E4jaW/emFCkGEG1vaMyXEpdCsA4ru
+vZ9osIrLUphMdSFZRjt12udP8Ox/6/DBjzxK/fv/l83WgyQGrAOJqzz80/fy
+6QMQEJp8adc8e/bu8aYt5+oQMy3SEyjVPWzGiW8CZJa3jcDb3yQZ8PqPzzA0
+MfTWAMOxG2+gUa9eO3+AL3TU+oCoUm826XW99SBzNBtNorhGbBwh0Fcom5g7
+bj7KF/7dLxOUKrX85puO0u/1cc75ifAKqGEIeeYL/EoLojJsre5gdtsSAFCq
+QOK7EUWxjDXn3MUBrjpH213gwIF9dDY6UKyBHSFRETUxphTjuhtQrPlGVnuB
+C92EohiG3XWo1Ln5yGHmSxFGIMkgEstga8A9H34UiYsROTnpxIuZEcGIoLnD
+2RFu4tuMpjaLcSkmwFsH9SK2/X4zHkGgOIHxJKNajHCjLfTyyxgR+v0+Lld0
+0se05hFyTNrFbizjCNC0T2djjQtn36SGMlw7T61WRkplTp0+zXLiOL2WIDmQ
+KxIqp//hDGa7tsLINV59mzJBfYFz5exP4EV52wgM1Gdwgx4wQd0Ir3Rm56Q0
+U0i6/lTLLSqR0O/2qNVrDPsDxI3pr54DdXRXhlCdY+ngERIHi40yum0GA1iM
+HeJhoFcV+JoxbTplUzsIsHDo2uth8eq13MHqGWTUnV7fzpNtez7d9CT1qh/G
+9NdXmJtt0h2kSJD7OaajtbBEqz3D8sVlqpMBG6nvCbn8aqyNCYRCoUAcGFyu
+pM5i5m4lmrubKAcNBNyYdNSHDHQyQXYd8rZaHT1rUbtMHFzCRCXiAIgiAuxO
+5F2mvlgKY8ghdWACxY22iJduZv+eOf71Zz/IT933bmxqWV5d4alv/zmvnDpJ
+f+RozR7kpdOvMnfgCGfPd7j90AE0j9E83m6MRlBr+FogN5iF9xJd/xmsdWBH
+4FJIBmDHqLWMbQo2RW2KlRGkzxO7M1DIvK8adKFSRIKphwqmTiyfPgKHmzgE
+5dd/6ZN89MH7cdYxTL3yt1stPvfYL/D6XXfxld//KsN/OAlz+5ht1tnorKGZ
+o2bA5CBeaCJIh2/B/f9/FIOExfn47Rdy9YvPPRw10ylTOb9wZWoaS4Bw77vv
+5p8/8iAvn4cvPTXioSe2+JnfuMDvfXsNgIPXH+RXvvBZnBHiyHD+4gqlWpug
+4AsmMWA0VzTt+a4CPpclMGgAmk8AX+RIINhkk48/PMt9H7ifgVP++JuvcfyZ
+s4iYaRr4At1vYCpiChIaVH1PiekXFlpu8Pjjv8mLF+B/PKc8eEz4j/+sibNV
+/uDPLvDHf7VGkvT4+IMHeOD++3jub55Ddx/m6Pwso8wRq2M4cb7pkWWZvzEe
+qxIIGsBg9Qy//U97/J//vMTP3j+DcVs8+uEb/bcxOczOV6ZdNUHEAOLpNVfy
+PAedagqy0xRzKBLAIx98H/uuW+S/Pi382keUL/+F5WO/vcWvfX2NZ3+Usm8+
+5gcvbZBax4c+8ADSaNOotkjTlALK2thhnUOuQubt0Ln7ENx95wEAPvPRFoPB
+tT3MUslc/axOo16sscNm4tsrb2U3CQREOHTIs9m5DZhvQKsMX/lknffeUObA
+oqHdMBzZW2WjbylXy+ggIWqUOdAo0xn7IOTBNbxp+MnxwzNw4gXP/1/7Xx2i
+6Nr3jEaTq39sNwbG/bc0CWJvBnFeIwIz7URcDVZt2ss6PC/88IzjnhtLHL4u
+5kdvJFxaS6ltB8mNcJfOsppYmkUhEhgrSFSa8TQa+kilzuHUYhSqCzfwpb9o
+8tH/tMJ3vr+OmTnMH/zh0yB+gs0rKeoUVYfLFHIhlog4MARBCLlFRHDq24Jx
+YIglIp0o3/rTp3A4/uV7hN/4M8Pv/kKFVlX57gtdFMeR/RH9kdKsxJx+5TQO
+ZW0wwE4s86WYmolZLMUYHfWAuWtN1tsEbfqqGJ5/o8nzv/wsszNlli9sveUA
+S6DZW/yRL0s1d95qb59K6BV/5fIaJ06+yM/fdQvPvwZf+MaImxemPJjBl79x
+jt/6nIfZ8ePHfcma9LgyStnTrGIVbKbItnkDpscN4HCjDm6wAoBmY9R5/Gu6
+wSQos7Kaounq1c+6ocd84NduCgYpVqYLn8IgZ6r4ioTCb/7WfwHgdz4OH77F
+kKRwqeOQEH733x7i8L4qp069xKunXyUqN1A3oTweEpupyckF0dx6issVAm/O
+JIj4/COL3F5/BUW5bSmhOXgOJORXP3kX6eXjuFz53M8dQ9MVb6NFfF9p2ut0
+E4umE2+88KzlcsUF6u8Rhiy/+WP+zRe/yIkTJ3j4duHzHy7z7392jk8/NEez
+kvKd7/xPvvqHX8UEBi1EmGKNlUS5nDgmucPlDqO5eBrNLSYw3om++jVWn+sy
+eP0F7Okn6F5/lGD5MnH6LeyxT1O69EfE3Zho+CnaW08SxzGYCKdeMwgj8jyb
+Jq/fnNcB31KLQzOFm3D8B8c5/qNXWKyXeeihj6DA+uVLfO/Zv8VmIyQUbABx
+OoFiEQKIjSMIHOXQEbTa7fxdhw6yvrmJacxhht6I+cW4nXyIjdkp5FOXsq3a
+sbmqyN5mexVu1quc+tEp7xp3LdFsNUmtRUJDFABRBbIx6TiB0hy4ASabYMJp
+hxDBhUUYd5CoSoRAY47KzG6O7auhowFm9xEM270cMZD03zF5dzRix26/c5Jj
+4mnhPx2BQUqNab2s7FjrQgnsVFMCQTQFN2JnLXENsUOoNJHI0C7FSGM3aTKA
+QoHlfspMAE///Zp3o8VCkRjBTRzpdMEmEOIw2mGn1Flv+ly64yoJIHXb9tdh
+AkNMBAEEQQAqaNrFxnNEUcz0/0hIR0OoVmAwJC5WYDL23+Sg2AxIh0ixQhzV
+oVpHspRBALNlYfn8q+y+5U7W+13ec+thpNPdIElSSpXSNFJTQxfoVXHO8ZFz
+Foh+4vXt0zC+ixFCu91mdW3KUGqwNqUUF32i6/REB33vnzSDxhyUK/5S4HtE
+YvvUCsCVs4gRxstvcGEkEEZcWl6mXjAk1SUCjMkrpQpHDh3ypgu/CZk2ZLd5
+3eVXoWTeQr1XXxck8F/OdXt9zi+fnyq1V94D+w4wN9uGaUlKYCD3p4aJ/P9m
+qC9rFRATIZUFmGxRLRq2OpsQlYniMtKe5z3vfT83f+BR/h9cGEWXakoA8AAA
+AABJRU5ErkJggg==
+====
diff --git a/drlaunch/misc/drlaunch-48.png b/drlaunch/misc/drlaunch-48.png
new file mode 100644 (file)
index 0000000..9c30d7d
Binary files /dev/null and b/drlaunch/misc/drlaunch-48.png differ
diff --git a/drlaunch/misc/drlaunch-64.png b/drlaunch/misc/drlaunch-64.png
new file mode 100644 (file)
index 0000000..0091ace
Binary files /dev/null and b/drlaunch/misc/drlaunch-64.png differ
diff --git a/drlaunch/misc/drlaunch.desktop b/drlaunch/misc/drlaunch.desktop
new file mode 100644 (file)
index 0000000..71434fc
--- /dev/null
@@ -0,0 +1,6 @@
+[Desktop Entry]
+Name=DrLaunch
+Comment=Application launcer widget with portrait mode
+Type=python
+X-Path=drlaunch_widget.py
+X-Multiple=true
diff --git a/drlaunch/misc/mkico b/drlaunch/misc/mkico
new file mode 100755 (executable)
index 0000000..d86ff34
--- /dev/null
@@ -0,0 +1,2 @@
+#convert drlaunch-64x64.png -scale 48x48 -depth 8 drlaunch-48x48.png
+uuencode -m drlaunch-48.png drlaunch-48.png > drlaunch-48.base64
diff --git a/drlaunch/mkdist b/drlaunch/mkdist
new file mode 100755 (executable)
index 0000000..9b59c08
--- /dev/null
@@ -0,0 +1,22 @@
+#!/bin/sh
+
+VERSION=`cd drlaunch/src ; python2.5 -c "import config ;print config.version"`
+
+if [ -z "$VERSION" ] ; then
+       echo "Version not found"
+       exit 1
+fi
+
+D="drlaunch-$VERSION"
+
+rm -rf tmp
+mkdir tmp
+cp -pR drlaunch "tmp/$D"
+
+FN="drlaunch-$VERSION.tar.gz"
+
+echo "Creating $FN"
+tar -zcf $FN --exclude ".svn" --exclude ".*.swp" -C tmp $D
+
+rm -rf tmp
+
diff --git a/drlaunch/sdist b/drlaunch/sdist
new file mode 100755 (executable)
index 0000000..de1476b
--- /dev/null
@@ -0,0 +1,18 @@
+#!/bin/bash
+
+rm MANIFEST dist/*.tar.gz
+python2.5 setup.py sdist
+
+for i in dist/drlaunch-*.tar.gz ; do
+       t=$(basename $i)
+       ver=${t#drlaunch-}
+       ver=${ver%.tar.gz}
+       if ! [ -d "../$ver" ] ; then
+               echo "New version: $ver"
+               cp ../drlaunch/dist/$t ..
+#              mkdir "../$ver"
+#              cp ../drlaunch/dist/$t ../$ver/$t
+#              ln -s $t ../$ver/drlaunch_$ver.orig.tar.gz
+       fi
+done
+
diff --git a/drlaunch/setup.py b/drlaunch/setup.py
new file mode 100755 (executable)
index 0000000..4ec6af6
--- /dev/null
@@ -0,0 +1,45 @@
+#!/usr/bin/env python
+# coding=UTF-8
+#
+# Copyright (C) 2010 Stefanos Harhalakis
+#
+# This file is part of maegirls.
+#
+# maegirls 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.
+#
+# maegirls 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 maegirls.  If not, see <http://www.gnu.org/licenses/>.
+#
+# $Id: setup.py 2266 2010-02-21 19:33:27Z v13 $
+
+from distutils.core import setup
+
+setup(
+    name='drlaunch',
+    version="1.0",
+    description="DrLaunch",
+    author="Stefanos Harhalakis",
+    author_email="v13@v13.gr",
+    url="not-available-yet",
+    packages=['drlaunch', 'drlaunch.xdg'],
+    package_dir={'drlaunch': 'src'},
+#    scripts=["maegirls"],
+#    data_files=[
+#          ("share/maegirls/translations", i18n_qm_files)
+#          ],
+    long_description="DrLaunch - Application launcher widget with portrait mode"
+    )
+
+#    py_modules=['src/core.py', 'src/graph.py', 'src/wifi-su.py',
+#      'src/wifi.py', 'src/win.py'],
+
+# vim: set ts=8 sts=4 sw=4 noet formatoptions=r ai nocindent:
+
diff --git a/drlaunch/src/.gitignore b/drlaunch/src/.gitignore
new file mode 100644 (file)
index 0000000..34fcef1
--- /dev/null
@@ -0,0 +1,2 @@
+*.pyo
+*.pyc
diff --git a/drlaunch/src/0.py b/drlaunch/src/0.py
new file mode 100755 (executable)
index 0000000..2b790f5
--- /dev/null
@@ -0,0 +1,26 @@
+#!/usr/bin/env python
+# coding=UTF-8
+# 
+# Copyright (C) 2010 Stefanos Harhalakis
+#
+# This file is part of wifieye.
+#
+# wifieye 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.
+#
+# wifieye 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 wifieye.  If not, see <http://www.gnu.org/licenses/>.
+#
+# $Id: 0.py 2265 2010-02-21 19:16:26Z v13 $
+
+__version__ = "$Id: 0.py 2265 2010-02-21 19:16:26Z v13 $"
+
+# vim: set ts=8 sts=4 sw=4 noet formatoptions=r ai nocindent:
+
diff --git a/drlaunch/src/__init__.py b/drlaunch/src/__init__.py
new file mode 100755 (executable)
index 0000000..2b790f5
--- /dev/null
@@ -0,0 +1,26 @@
+#!/usr/bin/env python
+# coding=UTF-8
+# 
+# Copyright (C) 2010 Stefanos Harhalakis
+#
+# This file is part of wifieye.
+#
+# wifieye 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.
+#
+# wifieye 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 wifieye.  If not, see <http://www.gnu.org/licenses/>.
+#
+# $Id: 0.py 2265 2010-02-21 19:16:26Z v13 $
+
+__version__ = "$Id: 0.py 2265 2010-02-21 19:16:26Z v13 $"
+
+# vim: set ts=8 sts=4 sw=4 noet formatoptions=r ai nocindent:
+
diff --git a/drlaunch/src/about.py b/drlaunch/src/about.py
new file mode 100755 (executable)
index 0000000..6f26ccc
--- /dev/null
@@ -0,0 +1,91 @@
+#!/usr/bin/env python
+# coding=UTF-8
+# 
+# Copyright (C) 2010 Stefanos Harhalakis
+#
+# This file is part of DrLaunch.
+#
+# DrLaunch 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.
+#
+# DrLaunch 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 DrLaunch.  If not, see <http://www.gnu.org/licenses/>.
+#
+# $Id: 0.py 2265 2010-02-21 19:16:26Z v13 $
+
+__version__ = "$Id: 0.py 2265 2010-02-21 19:16:26Z v13 $"
+
+from gtk import AboutDialog
+import config
+from about0 import HeAboutDialog
+from portrait import FremantleRotation
+
+class DlgAbout(HeAboutDialog, FremantleRotation):
+    def __init__(self):
+       HeAboutDialog.__init__(self)
+       FremantleRotation.__init__(self, "DrlaunchPlugin",
+           mode=FremantleRotation.AUTOMATIC)
+
+    @classmethod
+    def present2(cls, parent):
+       _copyright="""\
+Copyright (C) 2010 Stefanos Harhalakis <v13@v13.gr>
+Copyright (C) 2005-2010 Thomas Perl and the gPodder Team (for about0.py
+and portrait.py)
+"""
+       _description="""
+DrLaunch is a desktop launcher widget that
+supports portrait mode by rotating program
+icons. Whenever the device is rotated, icons
+are also rotated.
+
+Thanks to Thomas Perl and the gPodder Team.
+"""
+
+       _license="""\
+Send bug reports and feature requests to maemo's bugzilla:
+https://bugs.maemo.org/enter_bug.cgi?product=DrLaunch (preferred)
+or directly to Stefanos Harhalakis <v13@v13.gr>
+
+DrLaunch 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.
+
+DrLaunch 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 DrLaunch.  If not, see <http://www.gnu.org/licenses/>.
+
+DrLaunch uses a modified version of about.py from gpodder which is also
+distributed under the terms of the GPLv3 license. It also uses a
+modified version of portrait.py from gpodder which is also distributed
+under the terms of the GPLv3 license.
+"""
+
+       args={
+       "app_name":         "DrLaunch",
+       "version":          config.version,
+       "copyright":        _copyright + "\n" + _license,
+       "description":      _description,
+       "bugtracker_url":   "https://bugs.maemo.org/enter_bug.cgi?product=DrLaunch",
+       "parent":           parent,
+       }
+
+       cls.present(**args)
+
+    def set_icon_name(self, icon_name):
+       pass
+
+# vim: set ts=8 sts=4 sw=4 noet formatoptions=r ai nocindent:
+
diff --git a/drlaunch/src/about0.py b/drlaunch/src/about0.py
new file mode 100644 (file)
index 0000000..eeb6ae3
--- /dev/null
@@ -0,0 +1,156 @@
+#!/usr/bin/env python
+# coding=UTF-8
+#
+# gPodder - A media aggregator and podcast client
+# Copyright (c) 2005-2010 Thomas Perl and the gPodder Team
+#
+# gPodder 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.
+#
+# gPodder is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+
+# Python implementation of HeAboutDialog from hildon-extras
+# Copyright (c) 2010-04-11 Thomas Perl <thp@thpinfo.com>
+
+import hildon
+import gtk
+import dbus
+
+_ = str
+
+class HeAboutDialog(gtk.Dialog):
+    RESPONSE_WEBSITE, \
+    RESPONSE_BUGTRACKER, \
+    RESPONSE_DONATE = range(3)
+
+    def __init__(self):
+        gtk.Dialog.__init__(self)
+
+        self.website_url = None
+        self.bugtracker_url = None
+        self.donate_url = None
+
+        self.set_title(_('About'))
+
+        self.image_icon = gtk.Image()
+        self.label_app_name = gtk.Label()
+        self.label_version = gtk.Label()
+        self.label_description = gtk.Label()
+        self.label_copyright = gtk.Label()
+        self.table_layout = gtk.Table(3, 3, False)
+
+        hildon.hildon_helper_set_logical_font(self.label_app_name, 'X-LargeSystemFont')
+        hildon.hildon_helper_set_logical_font(self.label_version, 'LargeSystemFont')
+        hildon.hildon_helper_set_logical_font(self.label_copyright, 'SmallSystemFont')
+        hildon.hildon_helper_set_logical_color(self.label_copyright, gtk.RC_FG, gtk.STATE_NORMAL, 'SecondaryTextColor')
+
+        self.label_app_name.set_alignment(0, 1)
+        self.label_version.set_alignment(0, 1)
+        self.label_description.set_alignment(0, 0)
+        self.label_copyright.set_alignment(0, 1)
+        self.label_version.set_padding(10, 0)
+        self.label_copyright.set_padding(0, 5)
+        self.image_icon.set_padding(5, 5)
+
+        #content_area = self.get_content_area() # Starting with PyGTK 2.14
+       self.set_size_request(800,800)
+       pa=hildon.PannableArea()
+       pa.set_property('mov-mode', 
+               hildon.MOVEMENT_MODE_HORIZ | hildon.MOVEMENT_MODE_VERT )
+       vbox=gtk.VBox()
+       self.vbox.add(pa)
+       pa.add_with_viewport(vbox)
+        content_area = vbox
+
+        self.table_layout.attach(self.image_icon, 0, 1, 0, 2, 0, gtk.EXPAND, 0, 0)
+        self.table_layout.attach(self.label_app_name, 1, 2, 0, 1, 0, gtk.EXPAND | gtk.FILL, 0, 0)
+        self.table_layout.attach(self.label_version, 2, 3, 0, 1, gtk.EXPAND | gtk.FILL, gtk.EXPAND | gtk.FILL, 0, 0)
+        self.table_layout.attach(self.label_description, 1, 3, 1, 2, gtk.EXPAND | gtk.FILL, gtk.EXPAND | gtk.FILL, 0, 0)
+        self.table_layout.attach(self.label_copyright, 0, 3, 2, 3, gtk.EXPAND | gtk.FILL, gtk.EXPAND | gtk.FILL, 0, 0)
+        content_area.add(self.table_layout)
+        self.connect('response', self._on_response)
+        self.show_all()
+
+    def _on_response(self, dialog, response_id):
+        if response_id == HeAboutDialog.RESPONSE_WEBSITE:
+            self.open_webbrowser(self.website_url)
+        elif response_id == HeAboutDialog.RESPONSE_BUGTRACKER:
+            self.open_webbrowser(self.bugtracker_url)
+        elif response_id == HeAboutDialog.RESPONSE_DONATE:
+            self.open_webbrowser(self.donate_url)
+
+    def set_app_name(self, app_name):
+        self.label_app_name.set_text(app_name)
+        self.set_title(_('About %s') % app_name)
+
+    def set_icon_name(self, icon_name):
+        self.image_icon.set_from_icon_name(icon_name, gtk.ICON_SIZE_DIALOG)
+
+    def set_version(self, version):
+        self.label_version.set_text(version)
+
+    def set_description(self, description):
+        self.label_description.set_text(description)
+
+    def set_copyright(self, copyright):
+        self.label_copyright.set_text(copyright)
+
+    def set_website(self, url):
+        if self.website_url is None:
+            self.add_button(_('Visit website'), HeAboutDialog.RESPONSE_WEBSITE)
+        self.website_url = url
+
+    def set_bugtracker(self, url):
+        if self.bugtracker_url is None:
+            self.add_button(_('Report bug'), HeAboutDialog.RESPONSE_BUGTRACKER)
+        self.bugtracker_url = url
+
+    def set_donate_url(self, url):
+        if self.donate_url is None:
+            self.add_button(_('Donate'), HeAboutDialog.RESPONSE_DONATE)
+        self.donate_url = url
+
+    def open_webbrowser(self, url):
+        bus = dbus.SessionBus()
+        proxy = bus.get_object('com.nokia.osso_browser', '/com/nokia/osso_browser/request', 'com.nokia.osso_browser')
+        proxy.load_url(url, dbus_interface='com.nokia.osso_browser')
+
+    @classmethod
+    def present(cls, parent=None, app_name=None, icon_name=None, \
+            version=None, description=None, copyright=None, \
+            website_url=None, bugtracker_url=None, donate_url=None):
+        ad = cls()
+
+        if parent is not None:
+            ad.set_transient_for(parent)
+            ad.set_destroy_with_parent(True)
+
+        if app_name is not None:
+            ad.set_app_name(app_name)
+
+        ad.set_icon_name(icon_name)
+        ad.set_version(version)
+        ad.set_description(description)
+        ad.set_copyright(copyright)
+
+        if website_url is not None:
+            ad.set_website(website_url)
+
+        if bugtracker_url is not None:
+            ad.set_bugtracker(bugtracker_url)
+
+        if donate_url is not None:
+            ad.set_donate_url(donate_url)
+
+        ad.run()
+        ad.destroy()
+
diff --git a/drlaunch/src/apps.py b/drlaunch/src/apps.py
new file mode 100755 (executable)
index 0000000..1a7336f
--- /dev/null
@@ -0,0 +1,146 @@
+#!/usr/bin/env python
+# coding=UTF-8
+# 
+# Copyright (C) 2010 Stefanos Harhalakis
+#
+# This file is part of wifieye.
+#
+# wifieye 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.
+#
+# wifieye 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 wifieye.  If not, see <http://www.gnu.org/licenses/>.
+#
+# $Id: 0.py 2265 2010-02-21 19:16:26Z v13 $
+
+__version__ = "$Id: 0.py 2265 2010-02-21 19:16:26Z v13 $"
+
+import os
+
+from gettext import translation
+#import locale
+
+#from xdg.IconTheme import getIconPath
+
+appdir="/usr/share/applications/hildon"
+
+apps={}
+
+def readOneFn(fn):
+    global appdir
+
+    ret={
+       'id':       fn[:-8],
+       'name':     None,
+       'exec':     None,
+       'icon':     None,
+       'iconpath': None,
+       'domain':   None,
+       'type':     None,
+       }
+
+    fn2=appdir + '/' + fn
+
+    try:
+       f=open(fn2, 'rt')
+    except:
+       return(None)
+
+    inde=False
+    for line in f:
+       line=line.strip()
+       if line=='[Desktop Entry]':
+           inde=True
+           continue
+
+       if inde==False:
+           continue
+
+       # Reached another block
+       if line.startswith('[') and inde:
+           break
+
+       elif line.startswith('Name='):
+           l=line[5:]
+           ret['name']=l
+       elif line.startswith('Exec='):
+           l=line[5:]
+           ret['exec']=l
+       elif line.startswith('Icon='):
+           l=line[5:]
+           ret['icon']=l
+           # ret['iconpath']=getIconPath(l)
+       elif line.startswith('X-Text-Domain='):
+           l=line[14:]
+           ret['domain']=l
+       elif line.startswith('Type='):
+           l=line[5:]
+           ret['type']=l
+
+    if ret['domain']!=None:
+       try:
+           c=translation(ret['domain'])
+       except IOError, e:
+           c=None
+
+       if c!=None:
+           ret['name0']=ret['name']
+           ret['name']=c.gettext(ret['name0'])
+
+    if ret['name']==None:
+       ret['name']=ret['id']
+
+    return(ret)
+
+def readOne(name):
+    fn=name + ".desktop"
+
+    ret=readOneFn(fn)
+
+    return(ret)
+
+def scan():
+    global appdir, apps
+
+    files=os.listdir(appdir)
+
+    ret={}
+
+    for f in files:
+       if not f.endswith('.desktop'):
+           continue
+       if f.startswith('catorise-'):
+           continue
+
+       dt=readOneFn(f)
+
+       if dt==None:
+           continue
+       if dt['type']=='Daemon' or dt['type']=='daemon':
+           continue
+
+       t=f[:-8]
+       ret[t]=dt
+
+    apps=ret
+
+    return(ret)
+
+def getLastScan():
+    global apps
+
+    return(apps)
+
+if __name__=="__main__":
+    #locale.setlocale(locale.LC_ALL, '')
+    print scan()
+
+# vim: set ts=8 sts=4 sw=4 noet formatoptions=r ai nocindent:
+
diff --git a/drlaunch/src/config.py b/drlaunch/src/config.py
new file mode 100755 (executable)
index 0000000..50ae471
--- /dev/null
@@ -0,0 +1,363 @@
+#!/usr/bin/env python
+# coding=UTF-8
+# 
+# Copyright (C) 2010 Stefanos Harhalakis
+#
+# This file is part of wifieye.
+#
+# wifieye 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.
+#
+# wifieye 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 wifieye.  If not, see <http://www.gnu.org/licenses/>.
+#
+# $Id: 0.py 2265 2010-02-21 19:16:26Z v13 $
+
+__version__ = "$Id: 0.py 2265 2010-02-21 19:16:26Z v13 $"
+
+import os
+import pickle
+
+version = "1.0"
+
+try:
+    from glib import get_user_config_dir
+except:
+    def get_user_config_dir():
+       home=os.environ['HOME']
+       if home=='':
+           home="/home/user"
+       cfg="%s/.config" % (home)
+
+       return(cfg)
+
+def ensure_dir():
+    dir0=get_user_config_dir()
+    dir=dir0+'/drlaunch'
+    if not os.path.exists(dir):
+       os.mkdir(dir)
+    if not os.path.isdir(dir):
+       raise Exception('Failed to ensure directory' + dir)
+
+    return(dir)
+
+def get_config_fn():
+    dir=ensure_dir()
+    ret=dir + '/config'
+
+    return(ret)
+
+class Config(object):
+    def __init__(self, id):
+       self.id=id
+
+       self.size = (2,2)
+       self.themebgsize = 96
+       self.iconsize0 = 64
+###    self.iconspace = 42     # For 4 icons (height)
+##     self.iconspace = 36     # For 8 icons (width)
+
+#      self.iconsize = 64
+#      self.iconpadding = 12
+#      self.iconmargin = 6
+
+       self.apps=None
+       self.indiv=True
+       self.longpress=False
+       self.animate=True
+       self.nobg=False
+       self.themebg=False
+
+       self.setDefaultSizes()
+
+       #self.maxsz=(8,4)
+
+    def setSize(self, sz):
+       maxsz=self.getMaxSize()
+       sz2=(min(sz[0], maxsz[0]), min(sz[1], maxsz[1]))
+       self.size=sz2
+
+    def getSize(self):
+       return(self.size)
+
+    def setDefaultSizes(self):
+       self.iconsize=64
+       self.iconpadding=12
+       self.iconmargin=6
+
+    # Return the maximum grid size
+    def getMaxSize(self):
+       isf=self.getIconSizeFull()
+       retx=int(800/isf)
+       rety=int((480-60)/isf)
+       maxsz=(retx, rety)
+       return(maxsz)
+
+    # Return the maxmimum icon size
+    def getIconSizeRange(self):
+       return((48,128))
+
+    def getIconPaddingRange(self):
+       return((0,32))
+
+    def getIconMarginRange(self):
+       return((0,16))
+
+    def getIconSize(self):
+       return(self.iconsize)
+
+    def setIconSize(self, sz):
+       self.iconsize=sz
+
+    def getIconSpace(self):
+       return((2*self.iconpadding) + (2*self.iconmargin))
+
+    def getIconSizeFull(self):
+       ret=self.iconsize + (2*self.iconpadding) + (2*self.iconmargin)
+       return(ret)
+
+    def getIconMargin(self):
+       return(self.iconmargin)
+
+    def setIconMargin(self, sz):
+       self.iconmargin=sz
+
+    def getIconPadding(self):
+       return(self.iconpadding)
+
+    def setIconPadding(self, sz):
+       self.iconpadding=sz
+
+    def setIndiv(self, indiv):
+       self.indiv=indiv
+
+    def getIndiv(self):
+       return(self.indiv)
+
+    def setLongpress(self, lp):
+       self.longpress=lp
+
+    def getLongpress(self):
+       return(self.longpress)
+
+    def setAnimate(self, ar):
+       self.animate=ar
+
+    def getAnimate(self):
+       return(self.animate)
+
+    def setNoBg(self, nobg):
+       self.nobg=nobg
+
+    def getNoBg(self):
+       return(self.nobg)
+
+    def setThemeBg(self, themebg):
+       self.themebg=themebg
+
+    def getThemeBg(self):
+       return(self.themebg)
+
+    def setApps(self, aps):
+       """ apps is a dictionary of (x,y)=>appname """
+       self.apps=aps
+
+    def getApps(self):
+       if self.apps==None:
+           tmp={
+               (0,0):  'rtcom-call-ui',
+               (0,1):  'rtcom-messaging-ui',
+               (1,0):  'browser',
+               (1,1):  'osso-addressbook',
+               }
+           self.setApps(tmp)
+
+       return(self.apps)
+
+    def filterDefault(self, value, default):
+       if value==default:
+           return(-1)
+       else:
+           return(value)
+
+    def provideDefault(self, value, default):
+       if value==-1:
+           return(default)
+       else:
+           return(value)
+
+    def save(self):
+       self.check_init()
+
+       dt=self.load_all()
+
+       if dt==None:
+           dt={
+               'version':  7,
+               'data': {},
+               }
+
+       dt['data'][self.id]={
+           'size':         self.getSize(),
+           'apps':         self.getApps(),
+           'indiv':        self.getIndiv(),
+           'longpress':    self.getLongpress(),
+           'animate':      self.getAnimate(),
+           'nobg':         self.getNoBg(),
+           'themebg':      self.getThemeBg(),
+           'iconsize':     self.filterDefault(self.getIconSize(), 64),
+           'iconpadding':  self.filterDefault(self.getIconPadding(), 12),
+           'iconmargin':   self.filterDefault(self.getIconMargin(), 6),
+           }
+
+       fn=get_config_fn()
+
+       st=pickle.dumps(dt)
+       f=file(fn, 'w')
+       f.write(st)
+       f.close()
+
+    def parse_v1(self, dt0):
+       """ Convert a v1 config to v2 """
+       ret={
+           'version':      2,
+           'data':         {},
+           }
+
+       ret['data'][self.id]={
+           'size':         dt0['size'],
+           'apps':         dt0['apps'],
+           }
+
+       return(ret)
+
+    def parse_v2(self, dt):
+       # Perhaps copy dt?
+
+       dt['version']=3
+
+       for i in dt['data']:
+           dt['data'][i]['indiv']=False
+           dt['data'][i]['size']=(dt['data'][i]['size'], dt['data'][i]['size'])
+           dt['data'][i]['longpress']=True
+
+       return(dt)
+
+    def parse_v3(self, dt):
+       dt['version']=4
+
+       for i in dt['data']:
+           dt['data'][i]['animate']=True
+
+       return(dt)
+
+    def parse_v4(self, dt):
+       dt['version']=5
+
+       for i in dt['data']:
+           dt['data'][i]['nobg']=False
+
+       return(dt)
+
+    def parse_v5(self, dt):
+       dt['version']=6
+
+       for i in dt['data']:
+           dt['data'][i]['themebg']=False
+
+       return(dt)
+
+    def parse_v6(self, dt):
+       dt['version']=7
+
+       for i in dt['data']:
+           dt['data'][i]['iconsize']=-1
+           dt['data'][i]['iconpadding']=-1
+           dt['data'][i]['iconmargin']=-1
+
+       return(dt)
+
+    def load_all(self):
+       fn=get_config_fn()
+
+       try:
+           f=file(fn, 'r')
+           st=f.read()
+           f.close()
+           ret=pickle.loads(st)
+
+           if ret==None:
+               print "failed to load config"
+               ret=None
+           else:
+               if ret['version']==1:
+                   ret=self.parse_v1(ret)
+
+               if ret['version']==2:
+                   ret=self.parse_v2(ret)
+
+               if ret['version']==3:
+                   ret=self.parse_v3(ret)
+
+               if ret['version']==4:
+                   ret=self.parse_v4(ret)
+
+               if ret['version']==5:
+                   ret=self.parse_v5(ret)
+
+               if ret['version']==6:
+                   ret=self.parse_v6(ret)
+       except Exception, e:
+           print "config error:", e
+           ret=None
+
+       return(ret)
+
+    def load(self):
+       self.check_init()
+
+       fn=get_config_fn()
+
+       dt0=self.load_all()
+
+       if dt0==None or not dt0['data'].has_key(self.id):
+           return
+
+       dt=dt0['data'][self.id]
+
+       self.setSize(dt['size'])
+       self.setApps(dt['apps'])
+       self.setIndiv(dt['indiv'])
+       self.setLongpress(dt['longpress'])
+       self.setAnimate(dt['animate'])
+       self.setNoBg(dt['nobg'])
+       self.setThemeBg(dt['themebg'])
+       self.setIconSize(self.provideDefault(dt['iconsize'], 64))
+       self.setIconPadding(self.provideDefault(dt['iconpadding'], 12))
+       self.setIconMargin(self.provideDefault(dt['iconmargin'], 6))
+
+    def check_init(self):
+       if self.id==None:
+           import sys
+
+           print "config.init() not done"
+           sys.exit(1)
+
+def dump(obj):
+    attrs=[attr for attr in dir(obj) if not callable(getattr(obj,attr))]
+
+    print "obj:", obj
+    for attr in attrs:
+       if attr=='__gdoc__' or attr=='__doc__':
+           continue
+       print "  ", attr, ":", getattr(obj, attr)
+
+# vim: set ts=8 sts=4 sw=4 noet formatoptions=r ai nocindent:
+
diff --git a/drlaunch/src/drlaunch_widget.py b/drlaunch/src/drlaunch_widget.py
new file mode 100755 (executable)
index 0000000..c0f1f2a
--- /dev/null
@@ -0,0 +1,58 @@
+#!/usr/bin/env python
+# coding=UTF-8
+# 
+# Copyright (C) 2010 Stefanos Harhalakis
+#
+# This file is part of wifieye.
+#
+# wifieye 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.
+#
+# wifieye 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 wifieye.  If not, see <http://www.gnu.org/licenses/>.
+#
+# $Id: 0.py 2265 2010-02-21 19:16:26Z v13 $
+
+__version__ = "$Id: 0.py 2265 2010-02-21 19:16:26Z v13 $"
+
+import os.path
+from drlaunch import widget
+
+class DrlaunchPlugin(widget.DrlaunchPlugin):
+    pass
+
+def redirect_err():
+    import sys
+    import time
+
+    print "Opening /tmp/drlaunch.log"
+    f=open('/tmp/drlaunch.log', 'at', buffering=1)
+    sys.stdout=f
+    sys.stderr=f
+    print "Log open:", time.ctime()
+
+hd_plugin_type = DrlaunchPlugin
+
+if os.path.exists('/tmp/drlaunch.log'):
+    redirect_err()
+
+if __name__=="__main__":
+    import gobject
+    import gtk
+
+    gobject.type_register(hd_plugin_type)
+    obj=gobject.new(hd_plugin_type, plugin_id="plugin_id")
+    obj.show_all()
+    gtk.main()
+
+
+
+# vim: set ts=8 sts=4 sw=4 noet formatoptions=r ai nocindent:
+
diff --git a/drlaunch/src/icon.py b/drlaunch/src/icon.py
new file mode 100755 (executable)
index 0000000..c4ea529
--- /dev/null
@@ -0,0 +1,450 @@
+#!/usr/bin/env python
+# coding=UTF-8
+#
+# Copyright (C) 2010 Stefanos Harhalakis
+#
+# This file is part of wifieye.
+#
+# wifieye 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.
+#
+# wifieye 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 wifieye.  If not, see <http://www.gnu.org/licenses/>.
+#
+# $Id: 0.py 2265 2010-02-21 19:16:26Z v13 $
+
+__version__ = "$Id: 0.py 2265 2010-02-21 19:16:26Z v13 $"
+
+import gtk
+import gobject
+import hildon
+from hildondesktop import *
+from gtk import gdk
+from math import pi
+import cairo
+import time
+
+from portrait import FremantleRotation
+import launcher
+from xdg.IconTheme import getIconPath
+from sig import Disconnector
+
+#import config
+import apps
+
+# Background surface for icons
+iconbg=None
+sthemebg1=None
+sthemebg2=None
+
+# Load an icon
+# Fall-back to default/blue if not found or name==None
+def getIcon(name, iconsize):
+    # Default icon
+    idef='tasklaunch_default_application'
+
+    # If name==None then use the default icon
+    if name==None or name=='':
+       iname=idef
+    else:
+       iname=name
+
+    ico=getIconPath(iname, iconsize)
+
+    # If not found then use the default icon
+    if ico==None:
+       ico=getIconPath(idef, iconsize)
+
+    try:
+       ret=gtk.gdk.pixbuf_new_from_file_at_size(ico, iconsize, iconsize)
+    except:
+       # On error use the default icon
+       ico=getIconPath(idef, iconsize)
+       ret=gtk.gdk.pixbuf_new_from_file_at_size(ico, iconsize, iconsize)
+       print "Icon with unhandled format:", iname
+
+    return(ret)
+
+class Icon(Disconnector, gobject.GObject):
+#class Icon(gtk.Widget, Disconnector):
+
+    __v_t=(gobject.SIGNAL_RUN_FIRST | gobject.SIGNAL_ACTION,
+           gobject.TYPE_NONE, ())
+
+    gsignals={
+       'click':            __v_t,
+       'double-click':     __v_t,
+       'tripple-click':    __v_t,
+       'long-press':       __v_t,
+       }
+
+    __gsignals__=gsignals
+
+    def __init__(self, isconfig, config):
+#      self.__gobject_init__()
+       gobject.GObject.__init__(self)
+#      gtk.Widget.__init__(self)
+       Disconnector.__init__(self)
+
+       self.isconfig=isconfig
+       self.config=config
+
+       self.appname=None
+       self.icon=None
+        self.sicon=None
+       self.lastpress=0
+       self.ispressed=False
+
+       self.x=0
+       self.y=0
+
+       self.presstime=0.25
+
+       self.window_=None
+
+       self.clickcount=0
+
+       self.angle=0
+
+       self.cached_icons={}
+
+       self.draw_queued=False
+
+       #print "icon-init"
+
+    def __del__(self):
+       #print "icon-del"
+       pass
+
+    def timePressed(self):
+       """ return how much time a button is pressed """
+       dt=time.time() - self.lastpress
+
+       return(dt)
+
+    def reload(self):
+       self.clearAnimationCache()
+       self.clearBgCache()
+       self.invalidate()
+
+    def setApp(self, dt):
+       if dt==None:
+           self.appname=None
+           self.icon=None
+           self.sicon=None
+       else:
+           self.appname=dt['id']
+           self.icon=dt['icon2']
+           self.sicon=None
+       self.clearAnimationCache()
+       self.clearBgCache()
+       self.invalidate()
+
+    def clearAnimationCache(self):
+       self.cached_icons={}
+
+    def clearBgCache(self):
+       global iconbg, sthemebg1, sthemebg2
+
+       iconbg=None
+       sthemebg1=None
+       sthemebg2=None
+
+    def getSize(self):
+       return(self.config.getIconSizeFull())
+       # return(self.config.iconsize+self.config.iconspace)
+
+    def setAngle(self, angle):
+       """ Set the angle. Return True if the angle changed or False if it
+       didn't. The caller should invalidate the icon """
+
+       # The step in degrees
+       step=9
+
+       angle2=int(angle/step)*step
+
+       if angle2==self.angle:
+           return(False)
+
+       self.angle=angle2
+
+       # The caller should be responsible for redrawing.
+       # If we call invalidate() here there is the risk of having
+       # icons rotate individually using different angles
+#      self.invalidate()
+
+       return(True)
+
+    def mkbg(self, t_pressed):
+       """ Create the background of the icon and cache it as a global
+       variable among all icons and widget instances """
+        global iconbg
+
+        if iconbg!=None and t_pressed<=0.001:
+            return(iconbg)
+
+       w=self.getSize()
+       s=cairo.ImageSurface(cairo.FORMAT_ARGB32, w, w)
+       cr0=cairo.Context(s)
+       cr=gtk.gdk.CairoContext(cr0)
+
+       cr.set_source_rgba(0.1, 0.1, 0.1, 1)
+       cr.set_line_width(5)
+
+       #if self.ispressed:
+       if t_pressed>0.001 and \
+            (t_pressed <= self.presstime or self.ispressed):
+           t=1.0 * min(t_pressed, self.presstime) / self.presstime
+           g=0.3+0.5*t
+           b=0.3+0.7*t
+           cr.set_source_rgba(0, g, b, 0.7)
+       else:
+           cr.set_source_rgba(0.3, 0.3, 0.3, 0.7)
+
+        x=0
+        y=0
+       x3=x + (self.config.iconmargin)
+       y3=y + (self.config.iconmargin)
+
+       r=10    # Radius
+       w=self.config.iconsize+(self.config.iconpadding*2)
+
+       cr.move_to(x3+r, y3)
+       cr.arc(x3+w-r,  y3+r,   r,          pi*1.5, pi*2)
+       cr.arc(x3+w-r,  y3+w-r, r,          0,      pi*0.5)
+       cr.arc(x3+r,    y3+w-r, r,          pi*0.5, pi)
+       cr.arc(x3+r,    y3+r,   r,          pi,     pi*1.5)
+
+       cr.stroke_preserve()
+       cr.fill()
+       cr.clip()
+       cr.paint()
+#      cr.restore()
+
+        if t_pressed<0.001:
+            iconbg=s
+
+        return(s)
+
+    def get_sthemebg(self, pressed):
+       """ Return the theme's background icon as a surface. Cache it. """
+       global sthemebg1, sthemebg2
+
+       if not pressed and sthemebg1!=None:
+           return(sthemebg1)
+       if pressed and sthemebg2!=None:
+           return(sthemebg2)
+
+       fn="/etc/hildon/theme/images/"
+       if pressed:
+           fn+="ApplicationShortcutAppletPressed.png"
+       else:
+           fn+="ApplicationShortcutApplet.png"
+
+       w=self.config.iconsize + (self.config.iconpadding*2)
+       buf=gtk.gdk.pixbuf_new_from_file_at_size(fn, w, w)
+       s=cairo.ImageSurface(cairo.FORMAT_ARGB32, w, w)
+       cr0=cairo.Context(s)
+       cr=gtk.gdk.CairoContext(cr0)
+
+       cr.set_source_pixbuf(buf, 0, 0)
+       cr.paint()
+
+       if not pressed:
+           sthemebg1=s
+       else:
+           sthemebg2=s
+
+       return(s)
+
+    def get_sicon(self):
+       """ Return the icon as a surface. Cache it. """
+       if self.sicon!=None:
+           return(self.sicon)
+
+        w=self.config.iconsize
+       s=cairo.ImageSurface(cairo.FORMAT_ARGB32, w, w)
+       cr0=cairo.Context(s)
+       cr=gtk.gdk.CairoContext(cr0)
+
+       cr.set_source_pixbuf(self.icon, 0, 0)
+       cr.paint()
+
+       self.sicon=s
+
+       return(s)
+
+    def get_paint_icon(self):
+       """ Return the icon to paint as a surface. The icon is rotated
+       as needed. The result is cached. """
+       angle=self.angle
+
+       if self.timePressed() <= self.presstime or self.ispressed:
+            t=self.timePressed()
+           pressed=True
+       else:
+            t=0
+           pressed=False
+
+       if not pressed and self.cached_icons.has_key(angle):
+           return(self.cached_icons[angle])
+
+        w=self.config.getIconSizeFull()
+       s=cairo.ImageSurface(cairo.FORMAT_ARGB32, w, w)
+       cr0=cairo.Context(s)
+       cr=gtk.gdk.CairoContext(cr0)
+
+       # Paint the background
+       if self.config.getNoBg():
+           pass
+       elif self.config.getThemeBg():  # Use theme bg
+           s2=self.get_sthemebg(pressed)
+
+           # have in mind the size difference of iconsize+iconspace with
+           # the fixed themebgsize
+           #xy0=int((w-self.config.themebgsize)/2)
+           #xy0=int((w-self.config.iconsize)/2)
+           xy0=self.config.iconmargin
+
+           cr.save()
+           cr.set_source_surface(s2, xy0, xy0)
+           cr.paint()
+           cr.restore()
+       else:
+           s2=self.mkbg(t)
+           cr.save()
+           cr.set_source_surface(s2, 0, 0)
+           cr.paint()
+           cr.restore()
+
+       # If there is no icon then don't do anything more
+       if self.icon!=None:
+           # Get the icon as a surface (get_sicon() will cache the surface)
+           sicon=self.get_sicon()
+
+           # Width is the iconsize plus the empty border around the icon
+           #w=self.config.iconsize + self.config.iconspace
+
+           # This is used to locate the center of the surface
+           dx=int(w/2)
+
+           # This is the delta from the center where icons are drawn
+           dx2=int(self.config.iconsize/2)
+
+#          cr.save()
+
+           # A transformation matrix with dx/dy set to point to the center
+           m=cairo.Matrix(1, 0, 0, 1, dx, dx)
+           cr.set_matrix(m)
+           # Transform degrees to rads
+           rot=-1 * pi * 2 * self.angle / 360
+           cr.rotate(rot)
+           # Draw the icon
+           cr.set_source_surface(sicon, -dx2, -dx2)    # Faster than pixbuf
+#      cr.set_source_pixbuf(icon2, -dx2, -dx2)
+           cr.paint()
+
+#          cr.restore()
+
+       if not pressed:
+           self.cached_icons[angle]=s
+
+       return(s)
+
+
+    def draw(self, cr, x, y):
+       self.draw_queued=False
+       self.x=x
+       self.y=y
+
+       if self.icon==None and not self.isconfig:
+           return
+
+       cr.save()
+       s=self.get_paint_icon()
+       cr.set_source_surface(s, x, y)
+       cr.paint()
+
+       cr.restore()
+
+       return(False)
+
+    def timerPressed(self):
+#      if not self.ispressed:
+#          return(False)
+
+       self.invalidate()
+
+       if self.timePressed()>self.presstime:
+           ret=False
+       else:
+           ret=True
+
+       return(ret)
+
+    def doPress(self):
+       # Double-time: time for pressed and time for not-pressed
+       if time.time() - self.lastpress > self.presstime*2:
+           self.clickcount=0
+
+       self.lastpress=time.time()
+       self.ispressed=True
+       gobject.timeout_add(20, self.timerPressed)
+
+    def doRelease(self):
+       dt=time.time() - self.lastpress
+       self.ispressed=False
+       self.invalidate()
+       if dt<=self.presstime:
+           self.clickcount+=1
+           if self.clickcount==1:
+               self.emit('click')
+#              print "emit click", self
+           elif self.clickcount==2:
+               self.emit('double-click')
+           if self.clickcount==3:
+               self.emit('tripple-click')
+               self.clickcount=0
+       elif dt>self.presstime and dt<2:
+           self.emit('long-press')
+#          print "Emit lp"
+
+    def doCancel(self):
+       self.ispressed=False
+
+    def setWindow(self, window):
+       self.window_=window
+
+    def invalidate(self, window=None):
+       if window==None:
+           window=self.window_
+       else:
+           self.window_=window
+
+       if window==None:
+           return
+
+       if self.draw_queued:
+#          print "queued"
+           return
+
+       self.draw_queued=True
+       w=self.getSize()
+       rect=gdk.Rectangle(self.x, self.y, w, w)
+       gdk.Window.invalidate_rect(window, rect, True)
+
+#gobject.type_register(Icon)
+#signals=['click', 'double-click', 'tripple-click', 'long-press']
+#for s in signals:
+#    gobject.signal_new(s, Icon, gobject.SIGNAL_RUN_FIRST | \
+#      gobject.SIGNAL_ACTION, gobject.TYPE_NONE, ())
+
+# vim: set ts=8 sts=4 sw=4 noet formatoptions=r ai nocindent:
+
diff --git a/drlaunch/src/icongrid.py b/drlaunch/src/icongrid.py
new file mode 100755 (executable)
index 0000000..96588d5
--- /dev/null
@@ -0,0 +1,488 @@
+#!/usr/bin/env python
+# coding=UTF-8
+# 
+# Copyright (C) 2010 Stefanos Harhalakis
+#
+# This file is part of wifieye.
+#
+# wifieye 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.
+#
+# wifieye 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 wifieye.  If not, see <http://www.gnu.org/licenses/>.
+#
+# $Id: 0.py 2265 2010-02-21 19:16:26Z v13 $
+
+__version__ = "$Id: 0.py 2265 2010-02-21 19:16:26Z v13 $"
+
+import gtk
+import gobject
+import hildon
+from hildondesktop import *
+from gtk import gdk
+from math import pi
+import cairo
+import time
+
+from portrait import FremantleRotation
+#from xdg.IconTheme import getIconPath
+
+from config import dump
+import apps
+import icon
+from icon import Icon
+from icons import Icons
+
+#def getIcon(name, iconsize):
+#    ico=getIconPath(name, iconsize)
+#    ret=gtk.gdk.pixbuf_new_from_file_at_size(ico, iconsize, iconsize)
+#
+#    return(ret)
+
+# IconGrid is the main class that implements tha drawing of the grid
+# However, since it will be used both by the desktop plugin and the
+# configuration window, it cannot derive either gtk.Widget or HomePluginItem.
+# It is created here in a way that will allow it to work on both cases
+# and it is inherited by appropriate classes (one for the plugin and one
+# for the config widget)
+
+#class IconGrid(gtk.Widget, FremantleRotation):
+class IconGrid:        #(gobject.GObject):
+    def __init__(self, isconfig=False):
+#      self.__gobject_init__()
+#      gtk.Widget.__init__(self)
+
+       self.init_done=False
+
+       self.size=(0,0)
+
+       self.isconfig=isconfig
+
+       self.icons=None
+       self.lasticon=None  # The last icon that got selected
+
+       self.draw_pending=False
+
+       self.mode=None
+
+       # If this is False then animations are forcefully disabled
+       self.do_animations=True
+
+       self.angle_timer_start=0
+
+       # Duration of the rotation effect
+       self.rotation_time=0.8
+
+#      print "ig-init"
+
+#    def __del__(self):
+#      print "ig-del"
+
+    def do_realize(self, config):
+#      print "ig-realize"
+       self.config=config
+
+       if self.icons!=None:
+           print
+           print
+           print
+           print "WTF??????????????????????"
+           print
+           print
+           print
+
+       self.icons=Icons(self.isconfig, self.config)
+       #print "self:", self
+       #self.icons.set_parent(self)
+       self.setMode('l')
+       self.setSize(config.getMaxSize())
+       self.reloadIcons()
+
+    def do_unrealize(self):
+#      print "ig-unrealize"
+       self.config=None
+       self.icons.finish()
+       self.icons=None
+       self.lasticon=None
+
+    def connect(self, what, *args):
+       if what in Icon.gsignals.keys():
+           ret=self.icons.connect(what, *args)
+       else:
+           ret=gobject.GObject.connect(self, what, *args)
+           #ret=super(IconGrid, self).connect(what, *args)
+
+       return(ret)
+
+    def setSize(self, size):
+       self.size=size
+       self.icons.setSize(size)
+
+    def getSize(self):
+       ret=self.icons.getSize()
+       return(ret)
+
+    def setMode(self, mode):
+       if self.mode==mode:
+#          print "same mode"
+           return
+
+       self.mode=mode
+       if not isinstance(self, gtk.Widget):
+           return
+
+       do_draw=False
+
+       try:
+           v=self.get_property('is-on-current-desktop')
+           if v:
+               do_draw=True
+           else:
+               self.draw_pending=True
+       except TypeError:
+           do_draw=True
+
+       if do_draw and self.config.getAnimate() and self.do_animations:
+           #self.queue_draw()
+           # Don't start another timer
+           # Instead adjust the time start to produce a nice effect ;-)
+           if self.angle_timer_start==0:
+               self.angle_timer_start=time.time()
+               gobject.timeout_add(20, self.timerAngle)
+           else:
+               dt=time.time()-self.angle_timer_start
+               da=90.0*dt/self.rotation_time
+
+               da2=90.0-da
+               dt2=da2*self.rotation_time/90.0
+               self.angle_timer_start=time.time()-dt2
+        else:
+            if self.mode=='l':
+                self.setAngle(0)
+            else:
+                self.setAngle(90)
+
+           if do_draw:
+               self.queue_draw()
+
+    def disableAnimation(self):
+       self.do_animations=False
+
+    def enableAnimation(self):
+       self.do_animations=True
+
+    def setAnimationEnable(self, value):
+       if value:
+           self.enableAnimation()
+       else:
+           self.disableAnimation()
+
+    def timerAngle(self):
+       if self.angle_timer_start==0:
+           self.angle_timer_start=time.time()-0.05
+
+       dt=time.time()-self.angle_timer_start
+
+       da=90.0*dt/self.rotation_time
+
+       if self.mode=='l':
+           angle=90-da
+       else:
+           angle=da
+
+       if angle>=90:
+           angle=90
+           ret=False
+       elif angle<0:
+           angle=0
+           ret=False
+       else:
+           ret=True
+
+       if self.setAngle(angle):
+           self.queue_draw()
+
+       if ret==False:
+           self.angle_timer_start=0
+
+       return(ret)
+
+    def iconAt(self, x, y):
+       """ Get icon at coordinates x,y. X and Y are in pixels """
+
+       w=self.config.getIconSizeFull()
+
+       if self.mode=='l' or self.config.getIndiv():
+           x2=int(x / w)
+           y2=int(y / w)
+       else:
+           x2=self.size[1] - int(y/w) - 1
+           y2=int(x/w)
+
+       ret=self.get(x2,y2)
+
+       return(ret)
+
+    def get(self, x, y):
+       ret=self.icons.get(x,y)
+
+       return(ret)
+
+    def _draw(self, cr, event):
+       self.draw_pending=False
+
+       w=self.config.getIconSizeFull()
+       for x,y in self.icons:
+           if self.mode=='l' or self.config.getIndiv():
+               #x2=x * (self.config.iconsize + self.config.iconspace)
+               #y2=y * (self.config.iconsize + self.config.iconspace)
+               x2=x * self.config.getIconSizeFull()
+               y2=y * self.config.getIconSizeFull()
+           else:
+               #x2=y * (self.config.iconsize + self.config.iconspace)
+               #y2=(self.size[1]-x-1) * \
+               #       (self.config.iconsize + self.config.iconspace)
+               x2=y * self.config.getIconSizeFull()
+               y2=(self.size[1]-x-1) * self.config.getIconSizeFull()
+
+           # Only repaint the needed icons
+           rect=gdk.Rectangle(x2, y2, w, w)
+           t=rect.intersect(event.area)
+           if t.width==0 and t.height==0:
+               continue
+
+           ico=self.icons.get(x,y)
+           ico.draw(cr, x2, y2)
+
+    def setAngle(self, angle):
+       """ Return True/False indicating that angle has changed """
+       ret=False
+       for x,y in self.icons:
+           ic=self.icons.get(x,y)
+           if ic.setAngle(angle):
+               ret=True
+
+       return(ret)
+
+    def clearAnimationCache(self):
+       """ Clear animation cache, freeing memory """
+       for x,y in self.icons:
+           ic=self.icons.get(x,y)
+           ic.clearAnimationCache()
+
+    def clearBgCache(self):
+       """ Clear backgrounds cache """
+       for x,y in self.icons:
+           ic=self.icons.get(x,y)
+           ic.clearBgCache()
+
+    def do_expose_event(self, event):
+       cr=self.window.cairo_create()
+
+       cr.rectangle(event.area.x, event.area.y,
+           event.area.width, event.area.height)
+
+       cr.clip()
+
+       if not self.init_done:
+           self.icons.setWindow(self.window)
+           self.init_done=True
+
+       self._draw(cr, event)
+
+    def setLastIcon(self, icon):
+       if icon==self.lasticon:
+           return
+
+       if self.lasticon!=None:
+           self.lasticon.doCancel()
+           self.lasticon.invalidate(self.window)
+       self.lasticon=icon
+
+    def do_button_press_event(self, event):
+       icon=self.iconAt(event.x, event.y)
+       if icon==None:
+           return
+#      rect=gdk.Rectangle(event.x,event.y,1,1)
+#      rect=gdk.Rectangle(0, 0, 100, 100)
+       icon.doPress()
+       icon.invalidate(self.window)
+       self.setLastIcon(icon)
+
+#      gdk.Window.invalidate_rect(self.window, rect, True)
+
+       return(True)
+
+    def do_button_release_event(self, event):
+       if self.lasticon!=None:
+           self.lasticon.invalidate(self.window)
+           self.lasticon.doRelease()
+
+       self.setLastIcon(None)
+
+       return(True)
+
+    def do_leave_notify_event(self, event):
+       self.setLastIcon(None)
+       return(True)
+
+    def do_pproperty_notify_event(self, event):
+       icon=self.iconAt(event.x, event.y)
+       if icon==None:
+           return
+       icon.doCancel()
+       icon.invalidate(self.window)
+       return(True)
+
+    def do_motion_notify_event(self, event):
+       icon=self.iconAt(event.x, event.y)
+       if self.lasticon==icon:
+           return(True)
+
+       self.setLastIcon(None)
+       icon.doCancel()
+       icon.invalidate(self.window)
+       return(True)
+
+    def do_button_press_event_old(self, event):
+       if event.type==gdk.BUTTON_PRESS:
+           if self.mode=='p':
+               self.setMode('l')
+           else:
+               self.setMode('p')
+           self.queue_draw()
+       return True
+
+    # For debugging
+    def do_event1(self, event):
+       print "event:", event, event.type
+
+    def reloadIcons(self):
+       self.icons.load()
+       self.lasticon=None
+
+#    def on_orientation_changed(self, orientation):
+#      print "orch:", orientation
+#      o=orientation[0]
+#      self.setMode(o)
+
+class IconGridWidget(IconGrid, gtk.Widget):
+    def __init__(self, isconfig, config, animation=True):
+       IconGrid.__init__(self, isconfig)
+       gtk.Widget.__init__(self)
+
+       # This must be called before do_realize
+       self.setAnimationEnable(animation)
+
+       self.config=config
+
+       IconGrid.do_realize(self, self.config)
+
+       self.setSize(self.size)
+
+#      print "igw-init"
+
+#    def __del__(self):
+#      print "igw-del"
+
+    def setSize(self, size):
+       IconGrid.setSize(self, size)
+
+       w=self.size[0] * self.config.getIconSizeFull()
+       h=self.size[1] * self.config.getIconSizeFull()
+
+       self.set_size_request(w, h)
+
+    def reconfig(self):
+       self.clearBgCache()
+       self.clearAnimationCache()
+       self.reloadIcons()
+       self.setSize(self.size)
+       self.icons.resizeMax()
+       self.queue_draw()
+
+    def do_realize(self):
+#      print "igw-realize"
+       screen=self.get_screen()
+       self.set_colormap(screen.get_rgba_colormap())
+       self.set_app_paintable(True)
+
+       self.set_flags(self.flags() | gtk.REALIZED)
+
+       self.window=gdk.Window(
+           self.get_parent_window(),
+           width=self.allocation.width,
+           height=self.allocation.height,
+           window_type=gdk.WINDOW_CHILD,
+           wclass=gdk.INPUT_OUTPUT,
+           event_mask=self.get_events() | gdk.EXPOSURE_MASK
+               | gdk.BUTTON_PRESS_MASK 
+               | gdk.BUTTON_RELEASE_MASK 
+               | gdk.BUTTON_MOTION_MASK
+               | gdk.POINTER_MOTION_MASK
+               | gdk.POINTER_MOTION_HINT_MASK 
+               | gdk.ENTER_NOTIFY_MASK
+               | gdk.LEAVE_NOTIFY_MASK )
+
+       self.window.set_user_data(self)
+
+       # No matter what the pygtk widget demo says, this is NOT CORRECT!!!
+       # Don't call style.attach(self.window) or else the program will crash
+       # after some time! It seems that there is a style already.
+       # If we want to use the style the we use get_style() instead.
+       # This one was very hard to solve. Thanks to gnome2-globalmenu guys.
+       # It was solved by looking the commit 2666:
+       # see:
+       # svn diff -r2665:2666 http://gnome2-globalmenu.googlecode.com/svn/trunk
+       # which solved cse 490:
+       # http://code.google.com/p/gnome2-globalmenu/issues/detail?id=490
+#      self.style.attach(self.window)
+       style=self.get_style()
+
+#      style.set_background(self.window, gtk.STATE_NORMAL)
+       self.window.move_resize(*self.allocation)
+
+#      self.pixmap, mask = gtk.gdk.pixmap_create_from_xpm_d(
+#            self.window, self.style.bg[gtk.STATE_NORMAL], STAR_PIXMAP)
+       
+#      self.gc = self.style.fg_gc[gtk.STATE_NORMAL]
+
+       #HomePluginItem.do_realize(self)
+
+#      screen=self.get_screen()
+#      self.set_colormap(screen.get_rgba_colormap())
+#      self.set_app_paintable(True)
+
+    def do_unrealize(self):
+#      print "igw-unrealize", self
+       IconGrid.do_unrealize(self)
+       self.window.destroy()
+       self.window.set_user_data(None)
+
+    def do_expose_event(self, event):
+       cr=self.window.cairo_create()
+
+       cr.rectangle(event.area.x, event.area.y,
+           event.area.width, event.area.height)
+       cr.clip()
+
+       style=self.get_style()
+       col=style.bg[gtk.STATE_NORMAL]
+       cr.set_source_color(col)
+       cr.paint()
+
+       IconGrid.do_expose_event(self, event)
+
+#gobject.type_register(IconGrid)
+gobject.type_register(IconGridWidget)
+
+
+# vim: set ts=8 sts=4 sw=4 noet formatoptions=r ai nocindent:
+
diff --git a/drlaunch/src/icons.py b/drlaunch/src/icons.py
new file mode 100755 (executable)
index 0000000..808d922
--- /dev/null
@@ -0,0 +1,254 @@
+#!/usr/bin/env python
+# coding=UTF-8
+# 
+# Copyright (C) 2010 Stefanos Harhalakis
+#
+# This file is part of wifieye.
+#
+# wifieye 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.
+#
+# wifieye 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 wifieye.  If not, see <http://www.gnu.org/licenses/>.
+#
+# $Id: 0.py 2265 2010-02-21 19:16:26Z v13 $
+
+__version__ = "$Id: 0.py 2265 2010-02-21 19:16:26Z v13 $"
+
+#import config
+import apps
+import icon
+from icon import getIcon, Icon
+from sig import Disconnector
+
+import gobject
+import gtk
+
+class IconIter:
+    def __init__(self, items):
+       self.iter=items.__iter__()
+
+    def __iter__(self):
+       ret=self.iter.__iter__()
+       return(ret)
+
+    def next(self):
+       ret=self.iter.next()
+       return(ret)
+
+class Icons(gobject.GObject, Disconnector):
+#class Icons(gtk.Widget, Disconnector):
+
+    #__gsignals__=Icon.gsignals
+    def __init__(self, isconfig, config):
+       self.__gobject_init__()
+       #gobject.GObject.__init__(self)
+       #gtk.Widget.__init__(self)
+       Disconnector.__init__(self)
+
+       self.icons={}
+       self.allicons={}
+       self.size=0
+       self.isconfig=isconfig
+       self.config=config
+
+       # signal handlers
+       self.handlers={}
+
+       self.maxsz=(0,0)
+
+       # setup allicons
+       self.resizeMax()
+
+#      print "icons-init"
+
+    def finish(self):
+       self.dis_finish()
+       return
+#      self.icons=None
+#      self.allicons=None
+#      print "icons-finish"
+
+#    def __del__(self):
+#      print "icons-del"
+
+    def resizeMax(self):
+       sz=self.maxsz
+       maxsz=self.config.getMaxSize()
+
+       # Create new entries in x
+       if maxsz[0]>sz[0]:
+           for x in xrange(maxsz[0]-sz[0]):
+               for y in xrange(sz[1]):
+                   k=(x+sz[0],y)
+                   ico=Icon(self.isconfig, self.config)
+                   self.allicons[k]=ico
+                   self.connect_one(ico)
+           sz=(maxsz[0], sz[1])
+       elif maxsz[0]<sz[0]:
+           for x in xrange(sz[0]-maxsz[0]):
+               for y in xrange(sz[1]):
+                   k=(maxsz[0]+x,y)
+                   t=self.allicons.pop(k)
+                   self.disconnect_one(t)
+           sz=(maxsz[0], sz[1])
+
+       # Create new entries in y
+       if maxsz[1]>sz[1]:
+           for y in xrange(maxsz[1]-sz[1]):
+               for x in xrange(sz[0]):
+                   k=(x,y+sz[1])
+                   ico=Icon(self.isconfig, self.config)
+                   self.allicons[k]=ico
+                   self.connect_one(ico)
+           sz=(sz[0], maxsz[1])
+       elif maxsz[1]<sz[1]:
+           for y in xrange(sz[1]-maxsz[1]):
+               for x in xrange(sz[0]):
+                   k=(x,y+maxsz[1])
+                   t=self.allicons.pop(k)
+                   self.disconnect_one(t)
+           sz=(sz[0], maxsz[1])
+
+       self.maxsz=sz
+
+    @classmethod
+    def register_signals(cls):
+       signals=Icon.gsignals
+       for s in signals:
+           ss=signals[s]
+           gobject.signal_new(s, cls, ss[0], ss[1], (Icon,))
+#          gobject.SIGNAL_RUN_FIRST,
+#              gobject.TYPE_NONE, (Icon,))
+
+    def __iter__(self):
+       return(IconIter(self.icons))
+
+    def connect_one(self, which):
+       self.handlers[which]={
+           'longpress':    self.c(which, 'long-press', self.signalLongpress),
+           'click':        self.c(which, 'click', self.signalClick),
+           'tripple':      self.c(which, 'tripple-click',
+                               self.signalTrippleClick)
+           }
+
+    def disconnect_one(self, which):
+       for i in self.handlers[which]:
+           which.disconnect(self.handlers[which][i])
+       self.handlers.pop(which)
+#      which.disconnect(self.h[which]_longpress)
+#      which.disconnect(self.h_click)
+#      which.disconnect(self.h_tripple)
+
+    def setSize(self, sz):
+#      print "sz:", sz, self.size
+
+       if sz==self.size:
+           return
+
+       old=self.icons
+       self.icons={}
+
+       for x in xrange(sz[0]):
+           for y in xrange(sz[1]):
+               k=(x,y)
+               ico=self.allicons[k]
+               self.icons[k]=ico
+#              if old.has_key(k):
+#                  old.pop(k)  # Re-used
+#              else:
+#                  self.connect_one(ico)
+
+       # Disconnect signals
+#      for i in old:
+#          self.disconnect_one(old[i])
+
+       self.size=sz
+
+    def getSize(self):
+       return(self.size)
+
+    def setWindow(self, win):
+       """ Set the window for all icons """
+
+       for i in self.icons:
+           ic=self.icons[i]
+           ic.setWindow(win)
+
+    def signalLongpress(self, icon):
+       #print "signalLongpress()", icon
+       self.emit('long-press', icon)
+
+    def signalClick(self, icon):
+       #print "signalClick()", icon
+       self.emit('click', icon)
+
+    def signalTrippleClick(self, icon):
+       #print "signalTrippleClick()", icon
+       self.emit('tripple-click', icon)
+
+    def get(self, x, y):
+       k=(x,y)
+       if self.icons.has_key(k):
+           ret=self.icons[k]
+       else:
+           ret=None
+
+       return(ret)
+
+    def load(self):
+#      x=0
+#      y=0
+#      fn=["maegirls", "wifieye", 'battery-eye', 'image-viewer',
+#          'tecnoballz', 'ncalc', 'rtcom-call-ui', 'rtcom-messaging-ui',
+#          'extcalllog', 'browser', 'modest', 'osso-addressbook']
+
+       wapps=self.config.getApps()
+       #sz=self.config.getSize()
+       sz=self.getSize()
+
+       for k in wapps:
+           x,y=k
+           if x>=sz or y>=sz:
+               continue
+
+           appname=wapps[k]
+           if appname!=None:
+               app=apps.readOne(appname)
+               if app!=None:
+                   app['icon2']=getIcon(app['icon'], self.config.getIconSize())
+                   self.get(x,y).setApp(app)
+           else:
+               ic=self.get(x,y)
+               ic.setApp(None)
+
+       # Reload icons to make sure backgrounds, etc are valid
+       for x in xrange(sz[0]):
+           for y in xrange(sz[1]):
+               ic=self.get(x,y)
+               ic.reload()
+
+
+#      for f in fn:
+#          dt=apps.readOne(f)
+#          dt['icon2']=getIcon(dt['icon'])
+#          print x, y, dt
+#          self.get(x,y).setApp(dt)
+#          x+=1
+#          if x>=config.getSize(getSize()):
+#              x=0
+#              y+=1
+##         self.icons.append(p)
+
+gobject.type_register(Icons)
+Icons.register_signals()
+
+# vim: set ts=8 sts=4 sw=4 noet formatoptions=r ai nocindent:
+
diff --git a/drlaunch/src/iconw.py b/drlaunch/src/iconw.py
new file mode 100644 (file)
index 0000000..f7f13e7
--- /dev/null
@@ -0,0 +1,125 @@
+#!/usr/bin/env python
+# coding=UTF-8
+# 
+# Copyright (C) 2010 Stefanos Harhalakis
+#
+# This file is part of wifieye.
+#
+# wifieye 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.
+#
+# wifieye 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 wifieye.  If not, see <http://www.gnu.org/licenses/>.
+#
+# $Id: 0.py 2265 2010-02-21 19:16:26Z v13 $
+
+__version__ = "$Id: 0.py 2265 2010-02-21 19:16:26Z v13 $"
+
+from icon import Icon, getIcon
+import apps
+
+import gtk
+import cairo
+from gtk import gdk
+import gobject
+
+class IconWidget(gtk.Widget):
+    def __init__(self, isconfig, config):
+       gtk.Widget.__init__(self)
+
+       self.config=config
+       self.icon=Icon(isconfig, config)
+
+    def getMaxIconSizeFull(self):
+       c=self.config
+       w=c.getIconSizeRange()[1] + 2*c.getIconPaddingRange()[1] + \
+           2*c.getIconMarginRange()[1]
+
+       return(w)
+
+    def resetSize(self):
+       w=self.getMaxIconSizeFull()
+       self.set_size_request(2*w, 2*w)
+
+    def do_realize(self):
+       screen=self.get_screen()
+       self.set_colormap(screen.get_rgba_colormap())
+       self.set_app_paintable(True)
+
+       self.set_flags(self.flags() | gtk.REALIZED)
+
+       self.window=gdk.Window(
+           self.get_parent_window(),
+           width=self.allocation.width,
+           height=self.allocation.height,
+           window_type=gdk.WINDOW_CHILD,
+           wclass=gdk.INPUT_OUTPUT,
+           event_mask=self.get_events() | gdk.EXPOSURE_MASK)
+
+       self.window.set_user_data(self)
+
+       self.window.move_resize(*self.allocation)
+
+#      style=self.get_style()
+#      style.set_background(self.window, gtk.STATE_NORMAL)
+#      self.gc=style.bg_gc[gtk.STATE_NORMAL]
+
+       self.icon.setWindow(self.window)
+       self.resetSize()
+
+    def do_unrealize(self):
+       self.window.destroy()
+       self.window.set_user_data(None)
+
+    def do_expose_event(self, event):
+       cr=self.window.cairo_create()
+
+       cr.rectangle(event.area.x, event.area.y,
+           event.area.width, event.area.height)
+
+       cr.clip()
+
+       cr.save()
+       # Paint the background
+       style=self.get_style()
+       col=style.bg[gtk.STATE_NORMAL]
+       cr.set_source_color(col)
+       cr.paint()
+
+       x2=self.config.getIconSizeFull()
+
+#      col=style.fg[gtk.STATE_ACTIVE]
+#      cr.set_source_color(col)
+#      #cr.set_source_rgba(0,1,1,0.5)
+#      cr.rectangle(1,1,x2+x2+2,x2+2)
+#      cr.stroke()
+#      cr.restore()
+
+       self.icon.draw(cr, 0, 0)
+       self.icon.draw(cr, x2, 0)
+       self.icon.draw(cr, 0, x2)
+       self.icon.draw(cr, x2, x2)
+
+    def setApp(self, app):
+       self.app=app
+       self.refresh()
+
+    def refresh(self):
+       if self.app!=None:
+           app=apps.readOne(self.app)
+           app['icon2']=getIcon(app['icon'], self.config.getIconSize())
+           self.icon.setApp(app)
+
+       self.queue_draw()
+
+gobject.type_register(IconWidget)
+
+# vim: set ts=8 sts=4 sw=4 noet formatoptions=r ai nocindent:
+
diff --git a/drlaunch/src/launcher.py b/drlaunch/src/launcher.py
new file mode 100755 (executable)
index 0000000..36d14bc
--- /dev/null
@@ -0,0 +1,51 @@
+#!/usr/bin/env python
+# coding=UTF-8
+# 
+# Copyright (C) 2010 Stefanos Harhalakis
+#
+# This file is part of wifieye.
+#
+# wifieye 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.
+#
+# wifieye 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 wifieye.  If not, see <http://www.gnu.org/licenses/>.
+#
+# $Id: 0.py 2265 2010-02-21 19:16:26Z v13 $
+
+__version__ = "$Id: 0.py 2265 2010-02-21 19:16:26Z v13 $"
+
+import dbus
+
+bus=None
+proxy=None
+
+def init():
+    global bus, proxy
+
+    if bus==None:
+       bus=dbus.SessionBus()
+
+    if proxy==None:
+       proxy=bus.get_object('com.nokia.HildonDesktop.AppMgr',
+           '/com/nokia/HildonDesktop/AppMgr')
+
+def launch(prog):
+    global bus, proxy
+    
+    proxy.LaunchApplication(prog, 
+       dbus_interface='com.nokia.HildonDesktop.AppMgr')
+
+if __name__=="__main__":
+    init()
+    launch('wifieye')
+
+# vim: set ts=8 sts=4 sw=4 noet formatoptions=r ai nocindent:
+
diff --git a/drlaunch/src/portrait.py b/drlaunch/src/portrait.py
new file mode 100644 (file)
index 0000000..7ea8b09
--- /dev/null
@@ -0,0 +1,242 @@
+# -*- coding: utf-8 -*-
+#
+# gPodder - A media aggregator and podcast client
+# Copyright (c) 2005-2010 Thomas Perl and the gPodder Team
+#
+# gPodder 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.
+#
+# gPodder is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+
+import dbus
+import dbus.glib
+from dbus.mainloop.glib import DBusGMainLoop
+
+import hildon
+import osso
+
+# Replace this with your own gettext() functionality
+_ = str
+
+
+class FremantleRotation(object):
+    """thp's screen rotation for Maemo 5
+
+    Simply instantiate an object of this class and let it auto-rotate
+    your StackableWindows depending on the device orientation.
+
+    If you need to relayout a window, connect to its "configure-event"
+    signal and measure the ratio of width/height and relayout for that.
+
+    You can set the mode for rotation to AUTOMATIC (default), NEVER or
+    ALWAYS with the set_mode() method.
+    """
+    AUTOMATIC, NEVER, ALWAYS = range(3)
+
+    # Human-readable captions for the above constants
+    MODE_CAPTIONS = (_('Automatic'), _('Landscape'), _('Portrait'))
+
+    # Privately-used constants
+    _PORTRAIT, _LANDSCAPE = ('portrait', 'landscape')
+    _ENABLE_ACCEL = 'req_accelerometer_enable'
+    _DISABLE_ACCEL = 'req_accelerometer_disable'
+
+    # Defined in mce/dbus-names.h
+    _MCE_SERVICE = 'com.nokia.mce'
+    _MCE_REQUEST_PATH = '/com/nokia/mce/request'
+    _MCE_REQUEST_IF = 'com.nokia.mce.request'
+
+    # sysfs device name for the keyboard slider switch
+    KBD_SLIDER = '/sys/devices/platform/gpio-switch/slide/state'
+    _KBD_OPEN = 'open'
+    _KBD_CLOSED = 'closed'
+
+    def __init__(self, app_name, main_window=None, version='1.0', mode=0,
+       dontrotate=False):
+        """Create a new rotation manager
+
+        app_name    ... The name of your application (for osso.Context)
+        main_window ... The root window (optional, hildon.StackableWindow)
+        version     ... The version of your application (optional, string)
+        mode        ... Initial mode for this manager (default: AUTOMATIC)
+       dontrotate  ... Don't rotate the window. (def: False)
+        """
+       self.dontrotate = dontrotate # V13
+        self._orientation = None
+        self._main_window = main_window
+        self._stack = hildon.WindowStack.get_default()
+        self._mode = -1
+        self._last_dbus_orientation = None
+        self._keyboard_state = self._get_keyboard_state()
+        app_id = '-'.join((app_name, self.__class__.__name__))
+        self._osso_context = osso.Context(app_id, version, False)
+        program = hildon.Program.get_instance()
+        program.connect('notify::is-topmost', self._on_topmost_changed)
+
+       # Hack for dbus. See:
+       # https://garage.maemo.org/pipermail/pymaemo-developers/2010-April/001445.html
+       # https://garage.maemo.org/pipermail/pymaemo-developers/2010-April/001454.html
+       # https://garage.maemo.org/pipermail/pymaemo-developers/2010-April/thread.html
+       # https://bugs.maemo.org/show_bug.cgi?id=8611
+       #
+       # If we use dbus.Bus.get_system() or dbus.SystemBus() then the
+       # program fails whenever bluezwitch is installed. This could
+       # also happen whenever another widget is using System Bus (sure?).
+       # 
+        #V13 system_bus = dbus.Bus.get_system()
+       dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
+       busaddress='unix:path=/var/run/dbus/system_bus_socket'
+       system_bus=dbus.bus.BusConnection(busaddress)
+       self.system_bus=system_bus
+
+        system_bus.add_signal_receiver(self._on_orientation_signal, \
+                signal_name='sig_device_orientation_ind', \
+                dbus_interface='com.nokia.mce.signal', \
+                path='/com/nokia/mce/signal')
+        system_bus.add_signal_receiver(self._on_keyboard_signal, \
+                signal_name='Condition', \
+                dbus_interface='org.freedesktop.Hal.Device', \
+                path='/org/freedesktop/Hal/devices/platform_slide')
+        self.set_mode(mode)
+
+    def get_mode(self):
+        """Get the currently-set rotation mode
+
+        This will return one of three values: AUTOMATIC, ALWAYS or NEVER.
+        """
+        return self._mode
+
+    def set_mode(self, new_mode):
+        """Set the rotation mode
+
+        You can set the rotation mode to AUTOMATIC (use hardware rotation
+        info), ALWAYS (force portrait) and NEVER (force landscape).
+        """
+        if new_mode not in (self.AUTOMATIC, self.ALWAYS, self.NEVER):
+            raise ValueError('Unknown rotation mode')
+
+        if self._mode != new_mode:
+            if self._mode == self.AUTOMATIC:
+                # Remember the current "automatic" orientation for later
+                self._last_dbus_orientation = self._orientation
+                # Tell MCE that we don't need the accelerometer anymore
+                self._send_mce_request(self._DISABLE_ACCEL)
+
+            if new_mode == self.NEVER:
+                self._orientation_changed(self._LANDSCAPE)
+            elif new_mode == self.ALWAYS and \
+                    self._keyboard_state != self._KBD_OPEN:
+                self._orientation_changed(self._PORTRAIT)
+            elif new_mode == self.AUTOMATIC:
+                # Restore the last-known "automatic" orientation
+                self._orientation_changed(self._last_dbus_orientation)
+                # Tell MCE that we need the accelerometer again
+                self._send_mce_request(self._ENABLE_ACCEL)
+
+            self._mode = new_mode
+
+    def reset_mode(self):
+       if self._mode==self.AUTOMATIC:
+           self._send_mce_request(self._ENABLE_ACCEL)
+       else:
+           self._send_mce_request(self._DISABLE_ACCEL)
+
+    def _send_mce_request(self, request):
+        rpc = osso.Rpc(self._osso_context)
+        rpc.rpc_run(self._MCE_SERVICE, \
+                    self._MCE_REQUEST_PATH, \
+                    self._MCE_REQUEST_IF, \
+                    request, \
+                    use_system_bus=True)
+
+    def _on_topmost_changed(self, program, property_spec):
+        # XXX: This seems to never get called on Fremantle(?)
+        if self._mode == self.AUTOMATIC:
+            if program.get_is_topmost():
+                self._send_mce_request(self._ENABLE_ACCEL)
+            else:
+                self._send_mce_request(self._DISABLE_ACCEL)
+
+    def _get_main_window(self):
+        if self._main_window:
+            # If we have gotten the main window as parameter, return it and
+            # don't try "harder" to find another window using the stack
+            return self._main_window
+        else:
+            # The main window is at the "bottom" of the window stack, and as
+            # the list we get with get_windows() is sorted "topmost first", we
+            # simply take the last item of the list to get our main window
+            windows = self._stack.get_windows()
+            if windows:
+                return windows[-1]
+            else:
+                return None
+
+    def _orientation_changed(self, orientation):
+        if self._orientation == orientation:
+            # Ignore repeated requests
+            return
+
+        flags = 0
+
+        if orientation != self._LANDSCAPE:
+            flags |= hildon.PORTRAIT_MODE_SUPPORT
+
+        if orientation == self._PORTRAIT:
+            flags |= hildon.PORTRAIT_MODE_REQUEST
+
+        window = self._get_main_window()
+        if window is not None and self.dontrotate==False:
+            hildon.hildon_gtk_window_set_portrait_flags(window, flags)
+
+        self._orientation = orientation
+
+       self.on_orientation_changed(orientation)
+
+    def on_orientation_changed(self, orientation):
+       pass
+
+    def _get_keyboard_state(self):
+        # For sbox, if the device does not exist assume that it's closed
+        try:
+            return open(self.KBD_SLIDER).read().strip()
+        except IOError:
+            return self._KBD_CLOSED
+
+    def _keyboard_state_changed(self):
+        state = self._get_keyboard_state()
+
+        if state == self._KBD_OPEN:
+            self._orientation_changed(self._LANDSCAPE)
+        elif state == self._KBD_CLOSED:
+            if self._mode == self.AUTOMATIC:
+                self._orientation_changed(self._last_dbus_orientation)
+            elif self._mode == self.ALWAYS:
+                self._orientation_changed(self._PORTRAIT)
+
+        self._keyboard_state = state
+
+    def _on_keyboard_signal(self, condition, button_name):
+        if condition == 'ButtonPressed' and button_name == 'cover':
+            self._keyboard_state_changed()
+
+    def _on_orientation_signal(self, orientation, stand, face, x, y, z):
+        if orientation in (self._PORTRAIT, self._LANDSCAPE):
+            if self._mode == self.AUTOMATIC and \
+                    self._keyboard_state != self._KBD_OPEN:
+                # Automatically set the rotation based on hardware orientation
+                self._orientation_changed(orientation)
+
+            # Save the current orientation for "automatic" mode later on
+            self._last_dbus_orientation = orientation
+
+# vim: set ts=8 sts=4 sw=4 noet formatoptions=r ai nocindent:
diff --git a/drlaunch/src/portrait.py.orig b/drlaunch/src/portrait.py.orig
new file mode 100755 (executable)
index 0000000..8cefa3e
--- /dev/null
@@ -0,0 +1,211 @@
+# -*- coding: utf-8 -*-
+#
+# gPodder - A media aggregator and podcast client
+# Copyright (c) 2005-2010 Thomas Perl and the gPodder Team
+#
+# gPodder 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.
+#
+# gPodder is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+
+import dbus
+import dbus.glib
+
+import hildon
+import osso
+
+# Replace this with your own gettext() functionality
+import gpodder
+_ = gpodder.gettext
+
+
+class FremantleRotation(object):
+    """thp's screen rotation for Maemo 5
+
+    Simply instantiate an object of this class and let it auto-rotate
+    your StackableWindows depending on the device orientation.
+
+    If you need to relayout a window, connect to its "configure-event"
+    signal and measure the ratio of width/height and relayout for that.
+
+    You can set the mode for rotation to AUTOMATIC (default), NEVER or
+    ALWAYS with the set_mode() method.
+    """
+    AUTOMATIC, NEVER, ALWAYS = range(3)
+
+    # Human-readable captions for the above constants
+    MODE_CAPTIONS = (_('Automatic'), _('Landscape'), _('Portrait'))
+
+    # Privately-used constants
+    _PORTRAIT, _LANDSCAPE = ('portrait', 'landscape')
+    _ENABLE_ACCEL = 'req_accelerometer_enable'
+    _DISABLE_ACCEL = 'req_accelerometer_disable'
+
+    # Defined in mce/dbus-names.h
+    _MCE_SERVICE = 'com.nokia.mce'
+    _MCE_REQUEST_PATH = '/com/nokia/mce/request'
+    _MCE_REQUEST_IF = 'com.nokia.mce.request'
+
+    # sysfs device name for the keyboard slider switch
+    KBD_SLIDER = '/sys/devices/platform/gpio-switch/slide/state'
+    _KBD_OPEN = 'open'
+    _KBD_CLOSED = 'closed'
+
+    def __init__(self, app_name, main_window=None, version='1.0', mode=0):
+        """Create a new rotation manager
+
+        app_name    ... The name of your application (for osso.Context)
+        main_window ... The root window (optional, hildon.StackableWindow)
+        version     ... The version of your application (optional, string)
+        mode        ... Initial mode for this manager (default: AUTOMATIC)
+        """
+        self._orientation = None
+        self._main_window = main_window
+        self._stack = hildon.WindowStack.get_default()
+        self._mode = -1
+        self._last_dbus_orientation = None
+        self._keyboard_state = self._get_keyboard_state()
+        app_id = '-'.join((app_name, self.__class__.__name__))
+        self._osso_context = osso.Context(app_id, version, False)
+        program = hildon.Program.get_instance()
+        program.connect('notify::is-topmost', self._on_topmost_changed)
+        system_bus = dbus.Bus.get_system()
+        system_bus.add_signal_receiver(self._on_orientation_signal, \
+                signal_name='sig_device_orientation_ind', \
+                dbus_interface='com.nokia.mce.signal', \
+                path='/com/nokia/mce/signal')
+        system_bus.add_signal_receiver(self._on_keyboard_signal, \
+                signal_name='Condition', \
+                dbus_interface='org.freedesktop.Hal.Device', \
+                path='/org/freedesktop/Hal/devices/platform_slide')
+        self.set_mode(mode)
+
+    def get_mode(self):
+        """Get the currently-set rotation mode
+
+        This will return one of three values: AUTOMATIC, ALWAYS or NEVER.
+        """
+        return self._mode
+
+    def set_mode(self, new_mode):
+        """Set the rotation mode
+
+        You can set the rotation mode to AUTOMATIC (use hardware rotation
+        info), ALWAYS (force portrait) and NEVER (force landscape).
+        """
+        if new_mode not in (self.AUTOMATIC, self.ALWAYS, self.NEVER):
+            raise ValueError('Unknown rotation mode')
+
+        if self._mode != new_mode:
+            if self._mode == self.AUTOMATIC:
+                # Remember the current "automatic" orientation for later
+                self._last_dbus_orientation = self._orientation
+                # Tell MCE that we don't need the accelerometer anymore
+                self._send_mce_request(self._DISABLE_ACCEL)
+
+            if new_mode == self.NEVER:
+                self._orientation_changed(self._LANDSCAPE)
+            elif new_mode == self.ALWAYS and \
+                    self._keyboard_state != self._KBD_OPEN:
+                self._orientation_changed(self._PORTRAIT)
+            elif new_mode == self.AUTOMATIC:
+                # Restore the last-known "automatic" orientation
+                self._orientation_changed(self._last_dbus_orientation)
+                # Tell MCE that we need the accelerometer again
+                self._send_mce_request(self._ENABLE_ACCEL)
+
+            self._mode = new_mode
+
+    def _send_mce_request(self, request):
+        rpc = osso.Rpc(self._osso_context)
+        rpc.rpc_run(self._MCE_SERVICE, \
+                    self._MCE_REQUEST_PATH, \
+                    self._MCE_REQUEST_IF, \
+                    request, \
+                    use_system_bus=True)
+
+    def _on_topmost_changed(self, program, property_spec):
+        # XXX: This seems to never get called on Fremantle(?)
+        if self._mode == self.AUTOMATIC:
+            if program.get_is_topmost():
+                self._send_mce_request(self._ENABLE_ACCEL)
+            else:
+                self._send_mce_request(self._DISABLE_ACCEL)
+
+    def _get_main_window(self):
+        if self._main_window:
+            # If we have gotten the main window as parameter, return it and
+            # don't try "harder" to find another window using the stack
+            return self._main_window
+        else:
+            # The main window is at the "bottom" of the window stack, and as
+            # the list we get with get_windows() is sorted "topmost first", we
+            # simply take the last item of the list to get our main window
+            windows = self._stack.get_windows()
+            if windows:
+                return windows[-1]
+            else:
+                return None
+
+    def _orientation_changed(self, orientation):
+        if self._orientation == orientation:
+            # Ignore repeated requests
+            return
+
+        flags = 0
+
+        if orientation != self._LANDSCAPE:
+            flags |= hildon.PORTRAIT_MODE_SUPPORT
+
+        if orientation == self._PORTRAIT:
+            flags |= hildon.PORTRAIT_MODE_REQUEST
+
+        window = self._get_main_window()
+        if window is not None:
+            hildon.hildon_gtk_window_set_portrait_flags(window, flags)
+
+        self._orientation = orientation
+
+    def _get_keyboard_state(self):
+        # For sbox, if the device does not exist assume that it's closed
+        try:
+            return open(self.KBD_SLIDER).read().strip()
+        except IOError:
+            return self._KBD_CLOSED
+
+    def _keyboard_state_changed(self):
+        state = self._get_keyboard_state()
+
+        if state == self._KBD_OPEN:
+            self._orientation_changed(self._LANDSCAPE)
+        elif state == self._KBD_CLOSED:
+            if self._mode == self.AUTOMATIC:
+                self._orientation_changed(self._last_dbus_orientation)
+            elif self._mode == self.ALWAYS:
+                self._orientation_changed(self._PORTRAIT)
+
+        self._keyboard_state = state
+
+    def _on_keyboard_signal(self, condition, button_name):
+        if condition == 'ButtonPressed' and button_name == 'cover':
+            self._keyboard_state_changed()
+
+    def _on_orientation_signal(self, orientation, stand, face, x, y, z):
+        if orientation in (self._PORTRAIT, self._LANDSCAPE):
+            if self._mode == self.AUTOMATIC and \
+                    self._keyboard_state != self._KBD_OPEN:
+                # Automatically set the rotation based on hardware orientation
+                self._orientation_changed(orientation)
+
+            # Save the current orientation for "automatic" mode later on
+            self._last_dbus_orientation = orientation
+
diff --git a/drlaunch/src/sig.py b/drlaunch/src/sig.py
new file mode 100644 (file)
index 0000000..f7cf115
--- /dev/null
@@ -0,0 +1,59 @@
+#!/usr/bin/env python
+# coding=UTF-8
+# 
+# Copyright (C) 2010 Stefanos Harhalakis
+#
+# This file is part of wifieye.
+#
+# wifieye 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.
+#
+# wifieye 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 wifieye.  If not, see <http://www.gnu.org/licenses/>.
+#
+# $Id: 0.py 2265 2010-02-21 19:16:26Z v13 $
+
+__version__ = "$Id: 0.py 2265 2010-02-21 19:16:26Z v13 $"
+
+import gtk
+
+class Disconnector(object):
+    def __init__(self):
+       self.chandles=[]
+       if isinstance(self, gtk.Widget):
+           self.c(self, 'delete-event', self.slotDiscDeleteEvent)
+
+    def c(self, obj, signal, slot, *args):
+       h=obj.connect(signal, slot, *args)
+       self.chandles.append((obj, h))
+
+       return(h)
+
+    def slotDiscDeleteEvent(self, sender, event):
+#      print "sig-delete-event"
+       self.dis_finish()
+       return(False)
+
+    def dis_finish(self, obj=None):
+       """ Disconnect all signals. If obj is specified the only disconnect
+       signals from that object """
+       for i in self.chandles:
+           if obj!=None and i[0]!=obj:
+               continue
+           #print i[0], i[1]
+           i[0].disconnect(i[1])
+       
+       if obj==None:
+           self.chandles=[]
+       else:
+           self.chandles=[a for a in self.chandles if a[0]!=obj]
+
+# vim: set ts=8 sts=4 sw=4 noet formatoptions=r ai nocindent:
+
diff --git a/drlaunch/src/widget.py b/drlaunch/src/widget.py
new file mode 100755 (executable)
index 0000000..35141bb
--- /dev/null
@@ -0,0 +1,303 @@
+#!/usr/bin/env python
+# coding=UTF-8
+# 
+# Copyright (C) 2010 Stefanos Harhalakis
+#
+# This file is part of wifieye.
+#
+# wifieye 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.
+#
+# wifieye 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 wifieye.  If not, see <http://www.gnu.org/licenses/>.
+#
+# $Id: 0.py 2265 2010-02-21 19:16:26Z v13 $
+
+__version__ = "$Id: 0.py 2265 2010-02-21 19:16:26Z v13 $"
+
+# HACK
+# Add the current module's directory to sys.path to bypass
+# problems when running as widget.
+# Restore the path at the end of the imports
+import sys
+import os
+
+orig_path=sys.path[:]
+tmp_path=os.path.dirname( os.path.realpath( __file__ ) )
+sys.path.append(tmp_path)
+
+# End of hack
+
+import gtk
+import gobject
+import hildon
+from hildondesktop import *
+from gtk import gdk
+from math import pi
+import cairo
+import gconf
+import time
+
+from subprocess import Popen,PIPE
+
+from portrait import FremantleRotation
+import launcher
+from xdg.IconTheme import getIconPath
+from win_config import WinConfig
+
+import config
+import apps
+from icon import Icon
+from icongrid import IconGrid
+from sig import Disconnector
+
+# Restore path
+sys.path=orig_path
+
+# IconGrid must be before HomePluginItem for its connect()
+# and do_button_*() to override those of HomePluginItem
+class DrlaunchPlugin(IconGrid, HomePluginItem, FremantleRotation, Disconnector):
+    def __init__(self):
+       IconGrid.__init__(self)
+       HomePluginItem.__init__(self)
+       FremantleRotation.__init__(self, 'DrlaunchPlugin',
+           mode=FremantleRotation.AUTOMATIC, dontrotate=True)
+       Disconnector.__init__(self)
+
+       self.winConfig=None
+
+       self.gconf=gconf.client_get_default()
+
+       self.set_settings(True)
+
+       self.id=None
+       self.config=None
+
+       self.reset_mode()
+
+    def get_id0(self):
+       """If this is called from the constructor then the program
+       core dumps """
+       aid=self.get_applet_id()
+
+       # Get desktop activity if D.A.M. is present
+       
+       act="/usr/bin/activity"
+
+       if os.path.exists(act):
+           r=Popen([act, "current"], stdout=PIPE).communicate()
+           activity=r[0].strip()
+       else:
+           activity=""
+
+       ret="%s-%s" % (aid, activity)
+
+       return(ret)
+
+    def get_id(self):
+       if self.id==None:
+           self.id=self.get_id0()
+
+       return(self.id)
+
+    def get_config(self):
+       if self.config==None:
+           id=self.get_id()
+           self.config=config.Config(id)
+
+       return(self.config)
+
+    def get_desktop_orientation(self):
+       """
+       Return desktop orientation
+
+       NOTE: This is the desktop orientation as it was introduced in CSSU.
+       Not the device orientation.
+
+       @return "portrait" or "landscape"
+       """
+
+       sw=gdk.screen_width()
+       sh=gdk.screen_height()
+
+       if sw>=sh:
+           ret='landscape'
+       else:
+           ret='portrait'
+
+       return(ret)
+
+    def is_rotating_desktop(self):
+       """
+       Check whether the desktop will change to portrait mode, as
+       added in CSSU.
+
+       @return True/False
+       """
+
+       c=self.gconf
+
+       # This returns False if the key doesn't exist
+       ret=c.get_bool('/apps/osso/hildon-desktop/ui_can_rotate')
+
+       return(ret)
+
+    def do_realize(self):
+       launcher.init()
+       config=self.get_config()
+       config.load()
+
+       IconGrid.do_realize(self, config)
+
+       self.setSize(config.getSize())
+       self.reloadIcons()
+
+       screen=self.get_screen()
+       self.set_colormap(screen.get_rgba_colormap())
+       self.set_app_paintable(True)
+
+       self.c(self, 'show-settings', self.slot_show_settings)
+       self.c(self, 'long-press', self.signalLongpress)
+       self.c(self, 'click', self.signalClick)
+       self.c(self, 'notify', self.signalNotify)
+
+       HomePluginItem.do_realize(self)
+
+    def on_orientation_changed(self, orientation):
+       # Avoid bugs
+       if orientation==None or len(orientation)==0:
+           return
+
+       # Get the first character of the string (l/p)
+       o=orientation[0]
+
+       # Get desktop orientation
+       #do=self.get_desktop_orientation()
+
+       # Is desktop rotation (per CSSU) enabled?
+       rd=self.is_rotating_desktop()
+
+       #print "desktop: %s / %s, device: %s" % (do, rd, o)
+
+       # In case of a rotating desktop, force orientation to be
+       # 'landscape'
+       if rd:
+           o='l'
+
+       self.setMode(o)
+#      self.queue_draw()
+
+    def do_expose_event(self, event):
+       IconGrid.do_expose_event(self, event)
+       HomePluginItem.do_expose_event(self, event)
+       self.reset_mode()
+
+    def slot_show_settings(self, dt):
+       if self.winConfig!=None:
+           # Doesn't work
+           # self.winConfig.show_all()
+           return
+
+       s=WinConfig(self.get_config())
+       s.show_all()
+       #s.c(s, 'delete-event', self.slotConfigDestroy)
+       self.c(s, 'delete-event', self.slotConfigDestroy)
+       #s.connect('destroy', self.slotConfigDestroy)
+       self.winConfig=s
+
+    def slotConfigDestroy(self, sender, event):
+#      print "Sender:", sender
+       dt=sender.getData()
+
+       # Disconnect signals for that object in order to be deleted
+       self.dis_finish(self.winConfig)
+       #self.winConfig.finish()
+       #self.winConfig.destroy()
+
+       self.winConfig=None
+
+       cfg=self.get_config()
+
+       cfg.setSize(dt['size'])
+       cfg.setApps(dt['apps'])
+       cfg.setIndiv(dt['indiv'])
+       cfg.setLongpress(dt['longpress'])
+       cfg.setAnimate(dt['animate'])
+       cfg.setNoBg(dt['nobg'])
+       cfg.setThemeBg(dt['themebg'])
+       cfg.setIconSize(dt['iconsize'])
+       cfg.setIconPadding(dt['iconpadding'])
+       cfg.setIconMargin(dt['iconmargin'])
+       cfg.save()
+       
+       # Resize widget
+       self.icons.resizeMax()
+       self.setSize(dt['size'])
+       self.reloadIcons()
+
+       # Free memory that is used for animations if animations are disabled
+       if not dt['animate']:
+           self.clearAnimationCache()
+
+       # Free memory of backgrounds in case they changed
+       self.clearBgCache()
+
+       self.queue_draw()
+
+#      print "slot-config-destroy-end"
+
+       return(False)
+
+    def handle_click(self, sender, icon):
+       """ common handler for longpress and click """
+       if icon.appname!=None and icon.appname!='':
+           launcher.launch(icon.appname)
+
+    def signalLongpress(self, sender, icon):
+       self.handle_click(sender, icon)
+
+    def signalClick(self, sender, icon):
+       config=self.get_config()
+
+       if not config.getLongpress():
+           self.handle_click(sender, icon)
+
+    def signalNotify(self, sender, property):
+       if property.name=='is-on-current-desktop':
+           v=self.get_property(property.name)
+           if v and self.draw_pending:
+               self.queue_draw()
+
+    def resize2(self):
+       config=self.get_config()
+
+       w=(self.size[0] * config.iconsize) + \
+           (self.size[0] * config.getIconSpace())
+       h=(self.size[1] * config.iconsize) + \
+           (self.size[1] * config.getIconSpace())
+       self.set_size_request(w, h)
+       self.resize(w, h)
+
+    def setSize(self, size):
+       IconGrid.setSize(self, size)
+       self.resize2()
+
+hd_plugin_type = DrlaunchPlugin
+
+if __name__=="__main__":
+    gobject.type_register(hd_plugin_type)
+    obj=gobject.new(hd_plugin_type, plugin_id="plugin_id")
+    obj.show_all()
+    gtk.main()
+
+
+
+# vim: set ts=8 sts=4 sw=4 noet formatoptions=r ai nocindent:
+
diff --git a/drlaunch/src/win_config.py b/drlaunch/src/win_config.py
new file mode 100755 (executable)
index 0000000..4639e2b
--- /dev/null
@@ -0,0 +1,775 @@
+#!/usr/bin/env python
+# coding=UTF-8
+# 
+# Copyright (C) 2010 Stefanos Harhalakis
+#
+# This file is part of wifieye.
+#
+# wifieye 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.
+#
+# wifieye 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 wifieye.  If not, see <http://www.gnu.org/licenses/>.
+#
+# $Id: 0.py 2265 2010-02-21 19:16:26Z v13 $
+
+__version__ = "$Id: 0.py 2265 2010-02-21 19:16:26Z v13 $"
+
+import gtk
+import gobject
+import hildon
+import time
+import copy
+
+from hildon import StackableWindow
+#from portrait import FremantleRotation
+#from xdg.IconTheme import getIconPath
+
+import config
+import apps
+from icon import Icon, getIcon
+from iconw import IconWidget
+from icongrid import IconGridWidget
+from about import DlgAbout
+from portrait import FremantleRotation
+from sig import Disconnector
+
+class DialogIconSize(gtk.Dialog):
+    def __init__(self, config):
+       gtk.Dialog.__init__(self, "Adjust icon size")
+
+       # Operate on copy of config
+       self.config=copy.copy(config)
+       # And keep another copy to be able to undo
+       self.config0=copy.copy(config)
+
+       vbox=self.vbox
+
+       self.pa=hildon.PannableArea()
+       vbox.add(self.pa)
+       self.pa.set_property('mov-mode', hildon.MOVEMENT_MODE_VERT)
+
+       vbox=gtk.VBox()
+       self.pa.add_with_viewport(vbox)
+       self.pa.set_size_request(-1, 1000)
+
+       self.labelDim=gtk.Label("koko")
+       vbox.add(self.labelDim)
+
+       vbox.add(gtk.Label("Icon size:"))
+       scale=gtk.HScale()
+       scale.set_digits(0)
+       scale.set_value_pos(gtk.POS_RIGHT)
+       scale.set_range(*self.config.getIconSizeRange())
+       scale.set_increments(8, 16)
+       self.scaleIconSize=scale
+       vbox.add(scale)
+
+       hbox=gtk.HBox()
+       vbox.add(hbox)
+
+       vbox2=gtk.VBox()
+       vbox2.add(gtk.Label("Padding:"))
+       scale=gtk.HScale()
+       scale.set_digits(0)
+       scale.set_value_pos(gtk.POS_RIGHT)
+       scale.set_range(*self.config.getIconPaddingRange())
+       scale.set_increments(2,2)
+       self.scaleIconPadding=scale
+       vbox2.add(scale)
+       hbox.add(vbox2)
+
+       vbox2=gtk.VBox()
+       vbox2.add(gtk.Label("Margin:"))
+       scale=gtk.HScale()
+       scale.set_digits(0)
+       scale.set_value_pos(gtk.POS_RIGHT)
+       scale.set_range(*self.config.getIconMarginRange())
+       scale.set_increments(2,2)
+       self.scaleIconMargin=scale
+       vbox2.add(scale)
+       hbox.add(vbox2)
+
+       self.icon=IconWidget(False, self.config)
+       self.icon.setApp('osso-addressbook')
+       vbox.add(self.icon)
+
+       self.scaleIconSize.set_value(self.config.getIconSize())
+       self.scaleIconPadding.set_value(self.config.getIconPadding())
+       self.scaleIconMargin.set_value(self.config.getIconMargin())
+
+       # Set thos after settings initial values to avoid unwanted effects
+       self.scaleIconSize.connect('value-changed', self.slotSzChangeSize)
+       self.scaleIconPadding.connect('value-changed', self.slotSzChangePadding)
+       self.scaleIconMargin.connect('value-changed', self.slotSzChangeMargin)
+
+       hbox=gtk.HBox()
+       vbox.add(hbox)
+
+       but=hildon.Button(
+               gtk.HILDON_SIZE_AUTO_WIDTH | gtk.HILDON_SIZE_FINGER_HEIGHT,
+               hildon.BUTTON_ARRANGEMENT_VERTICAL)
+       but.set_title("OK")
+       but.connect("clicked", self.slotButtonOK)
+       #hbox.add(but)
+       self.action_area.pack_start(but)
+       self.buttonOK=but
+
+       but=hildon.Button(
+               gtk.HILDON_SIZE_AUTO_WIDTH | gtk.HILDON_SIZE_FINGER_HEIGHT,
+               hildon.BUTTON_ARRANGEMENT_VERTICAL)
+       but.set_title("Undo")
+       but.connect("clicked", self.slotButtonUndo)
+       #hbox.add(but)
+       self.action_area.pack_start(but)
+       self.buttonUndo=but
+
+       but=hildon.Button(
+               gtk.HILDON_SIZE_AUTO_WIDTH | gtk.HILDON_SIZE_FINGER_HEIGHT,
+               hildon.BUTTON_ARRANGEMENT_VERTICAL)
+       but.set_title("Restore defaults")
+       but.connect("clicked", self.slotButtonDefaults)
+       #hbox.add(but)
+       self.action_area.pack_start(but)
+       self.buttonDefaults=but
+
+       self.show_all()
+
+       self.recalc()
+
+    def setWH(self, w, h):
+       self.labelDim.set_text("Grid size: %d x %d icons" % (w, h))
+       #self.labelWidth.set_text("Width: %d icons" % w)
+       #self.labelHeight.set_text("Height: %d icons" % h)
+
+    def recalc(self):
+       maxsz=self.config.getMaxSize()
+       self.setWH(maxsz[0], maxsz[1])
+       self.icon.refresh()
+
+    def getIconSize(self):
+       return(self.scaleIconSize.get_value())
+
+    def setIconSize(self, iconsize):
+       self.scaleIconSize.set_value(iconsize)
+
+    def getIconPadding(self):
+       return(self.scaleIconPadding.get_value())
+
+    def setIconPadding(self, pad):
+       self.scaleIconPadding.set_value(pad)
+
+    def getIconMargin(self):
+       return(self.scaleIconMargin.get_value())
+
+    def setIconMargin(self, margin):
+       self.scaleIconMargin.set_value(margin)
+
+    def slotSzChangeSize(self, sender):
+       self.config.setIconSize(int(self.getIconSize()))
+       self.recalc()
+
+    def slotSzChangeMargin(self, sender):
+       self.config.setIconMargin(int(self.getIconMargin()))
+       self.recalc()
+
+    def slotSzChangePadding(self, sender):
+       self.config.setIconPadding(int(self.getIconPadding()))
+       self.recalc()
+
+    def slotButtonUndo(self, sender):
+       self.config=copy.copy(self.config0)
+       self.recalc()
+       self.resetSliders()
+
+    def slotButtonDefaults(self, sender):
+       self.config.setDefaultSizes()
+       self.recalc()
+       self.resetSliders()
+
+    def slotButtonOK(self, sender):
+       self.response(gtk.RESPONSE_OK)
+
+    def resetSliders(self):
+       self.scaleIconSize.set_value(self.config.getIconSize())
+       self.scaleIconPadding.set_value(self.config.getIconPadding())
+       self.scaleIconMargin.set_value(self.config.getIconMargin())
+
+    def getData(self):
+       ret={
+           'size':     self.config.getIconSize(),
+           'padding':  self.config.getIconPadding(),
+           'margin':   self.config.getIconMargin()
+           }
+
+       return(ret)
+
+
+class WinConfig(StackableWindow, Disconnector): #, FremantleRotation):
+    def __init__(self, config, *args):
+       StackableWindow.__init__(self)
+       Disconnector.__init__(self)
+#      FremantleRotation.__init__(self, "DrlaunchPlugin",
+#          mode=FremantleRotation.AUTOMATIC)
+
+       self.config=copy.copy(config)
+
+       self.setupUi()
+
+#      h=self.c(self, 'delete-event', self.slotDeleteEvent)
+
+# This is a nice test. If it is displayed then the window is actually
+# destroyed and there is no memory leak
+#    def __del__(self):
+#      print "wc-del"
+
+    def setupUi(self):
+       """
+       self.pa         Main Pannable Area
+       self.col1       A VBox for the first column
+       self.col2       A VBox for the second column
+       self.w_igw      The IGW in an alignment
+       """
+       self.set_title("DrLaunch v" + config.version)
+
+       self.pa=hildon.PannableArea()
+#      self.add(self.pa)
+       self.pa.set_property('mov-mode', hildon.MOVEMENT_MODE_HORIZ)
+
+       self.add(self.pa)
+
+#1     hbox=gtk.HBox()
+#1     self.pa.add_with_viewport(hbox)
+
+       # Add the first column of options
+       al=gtk.Alignment(yscale=1)
+#1     hbox.add(al)
+
+       vbox=gtk.VBox()
+       al.add(vbox)
+       self.col1=al
+
+       maxsz=self.config.getMaxSize()
+
+       self.maxmaxsz=(16,16)
+
+       # ----------------------------------------------
+       vbox.add(gtk.Label('Width:'))
+
+       hbox2=gtk.HBox()
+       vbox.add(hbox2)
+
+       self.butsSizeX=[]
+       for i in xrange(self.maxmaxsz[0]):
+           but=hildon.GtkToggleButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
+           but.set_label("%s" % (i+1,))
+           self.c(but, 'toggled', self.slotButtonSizeX, i)
+
+           self.butsSizeX.append(but)
+
+           hbox2.add(but)
+
+       # ----------------------------------------------
+       vbox.add(gtk.Label('Height:'))
+
+       hbox2=gtk.HBox()
+       vbox.add(hbox2)
+
+       self.butsSizeY=[]
+       for i in xrange(self.maxmaxsz[1]):
+           but=hildon.GtkToggleButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
+           but.set_label("%s" % (i+1,))
+           self.c(but, 'toggled', self.slotButtonSizeY, i)
+
+           self.butsSizeY.append(but)
+
+           hbox2.add(but)
+
+       vbox2=gtk.VBox()
+
+       al=gtk.Alignment(xalign=0, yalign=1, xscale=1)
+       al.add(vbox2)
+       self.col11=al
+
+       vbox.add(al)
+       vbox=vbox2
+
+       but=hildon.CheckButton(
+               gtk.HILDON_SIZE_AUTO_WIDTH | gtk.HILDON_SIZE_FINGER_HEIGHT)
+       but.set_label("Rotate icons individually")
+       self.c(but, 'toggled', self.slotButtonRotateIndividually)
+       self.buttonRotateIndividually=but
+       vbox.add(but)
+
+       but=hildon.CheckButton(
+               gtk.HILDON_SIZE_AUTO_WIDTH | gtk.HILDON_SIZE_FINGER_HEIGHT)
+       but.set_label("Require long press")
+#      but.connect('toggled', self.slotButtonLongpress)
+       self.buttonRequireLongpress=but
+       vbox.add(but)
+
+       but=hildon.CheckButton(
+               gtk.HILDON_SIZE_AUTO_WIDTH | gtk.HILDON_SIZE_FINGER_HEIGHT)
+       but.set_label("Animate rotation")
+       self.buttonAnimateRotation=but
+       vbox.add(but)
+
+       # -----------------------------------------------
+       # Second column of options
+       vbox=gtk.VBox()
+
+       al=gtk.Alignment(xalign=0, yalign=1, xscale=1)
+       al.add(vbox)
+       self.col2=al
+
+       but=hildon.CheckButton(
+               gtk.HILDON_SIZE_AUTO_WIDTH | gtk.HILDON_SIZE_FINGER_HEIGHT)
+       but.set_label("Theme background")
+       but.connect('toggled', self.slotButtonThemeBackground)
+       self.buttonThemeBackground=but
+       vbox.add(but)
+
+       but=hildon.CheckButton(
+               gtk.HILDON_SIZE_AUTO_WIDTH | gtk.HILDON_SIZE_FINGER_HEIGHT)
+       but.set_label("No background")
+       self.c(but, 'toggled', self.slotButtonNoBackground)
+       self.buttonNoBackground=but
+       vbox.add(but)
+
+       but=hildon.Button(
+               gtk.HILDON_SIZE_AUTO_WIDTH | gtk.HILDON_SIZE_FINGER_HEIGHT,
+               hildon.BUTTON_ARRANGEMENT_VERTICAL)
+       but.set_label("Adjust icon size")
+       self.c(but, 'clicked', self.slotButtonIconSize)
+       self.buttonIconSize=but
+       vbox.add(but)
+
+#1     hbox.add(al)
+       but=hildon.Button(
+               gtk.HILDON_SIZE_AUTO_WIDTH | gtk.HILDON_SIZE_FINGER_HEIGHT,
+               hildon.BUTTON_ARRANGEMENT_VERTICAL)
+       but.set_title("About")
+       self.c(but, "clicked", self.slotButtonAbout)
+       vbox.add(but)
+       self.buttonAbout=but
+
+       # -----------------------------------------------
+       # Add the icongrid
+       self.igw=IconGridWidget(True, self.config, False)
+#      self.igw.do_realize()
+#      self.igw.setSize(config.getSize())
+       al=gtk.Alignment(xalign=0, xscale=0)
+       al.add(self.igw)
+       al.set_padding(0, 0, 20, 0)
+       self.w_igw=al
+#1     hbox.add(al)
+
+       self.igw.connect('long-press', self.slotLongpress)
+       self.igw.connect('click', self.slotLongpress)
+
+       self.ignore_toggle=False
+
+       self.setSize(self.config.getSize())
+       self.setIndiv(self.config.getIndiv())
+       self.setLongpress(self.config.getLongpress())
+       self.setAnimate(self.config.getAnimate())
+       self.setNoBg(self.config.getNoBg())
+       self.setThemeBg(self.config.getThemeBg())
+#      self.setIconSize(self.config.getIconSize())
+#      self.setIconPadding(self.config.getIconPadding())
+#      self.setIconMargin(self.config.getIconMargin())
+
+       hbox=gtk.HBox()
+       hbox.add(self.col1)
+       hbox.add(self.col2)
+       hbox.add(self.w_igw)
+       self.pa.add_with_viewport(hbox)
+
+       self.adjustMaxSize()
+
+#    def setupUi(self, orientation):
+#
+#      self.setupUi0()
+#
+#      hbox=gtk.HBox()
+#
+#      if orientation=='l':
+#          hbox.add(self.col1)
+#          hbox.add(self.col2)
+#          hbox.add(self.w_igw)
+#      else:
+#          vbox=gtk.VBox()
+#          hbox.add(vbox)
+#          vbox.add(self.col1)
+#          vbox.add(self.col2)
+#          hbox.add(self.w_igw)
+#
+#      self.pa.add_with_viewport(hbox)
+#
+#      if self.get_child()!=None:
+#          self.remove(self.get_child())
+#      self.add(self.pa)
+#      self.pa.show_all()
+
+    def adjustMaxSize(self):
+       sz=self.config.getMaxSize()
+       maxsz=self.maxmaxsz
+
+       for x in xrange(maxsz[0]):
+           if x<sz[0]:
+               self.butsSizeX[x].show()
+           else:
+               self.butsSizeX[x].hide()
+
+       for y in xrange(maxsz[1]):
+           if y<sz[1]:
+               self.butsSizeY[y].show()
+           else:
+               self.butsSizeY[y].hide()
+
+       osz=self.getSize()
+       nsz=[osz[0], osz[1]]
+
+       if osz[0]>sz[0]:
+           nsz[0]=sz[0]
+       if osz[1]>sz[1]:
+           nsz[1]=sz[1]
+       self.setSize(nsz)
+
+       self.setIndiv(self.getIndiv())
+
+    def setLayoutPortrait(self):
+       print "lo: p"
+       hbox=gtk.HBox()
+       
+       vbox=gtk.VBox()
+       hbox.add(vbox)
+       self.col1.reparent(vbox)
+       self.col2.reparent(vbox)
+       self.w_igw.reparent(hbox)
+
+       r=self.pa.get_children()[0]
+       self.pa.remove(r)
+       r.destroy()
+       self.pa.add_with_viewport(hbox)
+
+       self.pa.show_all()
+
+    def setLayoutLandscape(self):
+       print "lo: l"
+       hbox=gtk.HBox()
+       
+       self.col1.reparent(hbox)
+       self.col2.reparent(hbox)
+       self.w_igw.reparent(hbox)
+
+       r=self.pa.get_children()[0]
+       self.pa.remove(r)
+       r.destroy()
+       self.pa.add_with_viewport(hbox)
+
+       self.pa.show_all()
+
+    def on_orientation_changed(self, orientation):
+       # This is disabled for now since I've not found any reliable
+       # way for supporting orientation changes (#$#%$#*&% GTK)
+       return
+
+       print "orch:", orientation
+       if orientation=='portrait':
+           self.setLayoutPortrait()
+       else:
+           self.setLayoutLandscape()
+
+#    def slotDeleteEvent(self, sender, event):
+#      print "wc-del-event"
+#      return(False)
+
+    def slotLongpress(self, sender, icon):
+       self.doConfig(icon)
+
+    def slotButtonSizeX(self, sender, id):
+       if self.getIndiv():
+           old=self.getSize()
+           sz=(id+1, old[1])
+       else:
+           sz=(id+1, id+1)
+
+       self.setSize(sz)
+       
+    def slotButtonSizeY(self, sender, id):
+       old=self.getSize()
+       sz=(old[0], id+1)
+       self.setSize(sz)
+       
+    def slotButtonRotateIndividually(self, sender):
+       but=self.buttonRotateIndividually
+       self.setIndiv(but.get_active())
+
+    def slotButtonNoBackground(self, sender):
+       nobg=self.getNoBg()
+       self.setNoBg(nobg)
+
+    def slotButtonThemeBackground(self, sender):
+       themebg=self.getThemeBg()
+       self.setThemeBg(themebg)
+
+    def slotButtonAbout(self, sender):
+       DlgAbout.present2(self)
+
+    def slotButtonIconSize(self, sender):
+       dlg=DialogIconSize(self.config)
+       ret=dlg.run()
+       dt=dlg.getData()
+       dlg.destroy()
+
+       if ret!=gtk.RESPONSE_OK:
+           return
+
+       self.config.setIconSize(dt['size'])
+       self.config.setIconMargin(dt['margin'])
+       self.config.setIconPadding(dt['padding'])
+       self.igw.reconfig()
+       self.adjustMaxSize()
+       self.queue_draw()
+
+    def show_all(self):
+       StackableWindow.show_all(self)
+#      return
+       self.adjustMaxSize()
+       self.queue_draw()
+
+#    def slotScaleIconSzChange(self, sender):
+#      return
+#      self.config.setIconSize(self.getIconSize())
+#      self.config.setIconMargin(self.getIconMargin())
+#      self.config.setIconPadding(self.getIconPadding())
+#      self.igw.reconfig()
+#      self.adjustMaxSize()
+#      self.queue_draw()
+
+#    def slotButtonLongpress(self, sender):
+#      but=self.buttonRequireLongpress
+#      self.set
+
+    def setSize(self, sz):
+       if self.ignore_toggle:
+           return
+
+       self.ignore_toggle=True
+
+       maxsz=self.config.getMaxSize()
+
+       id=sz[0]-1
+
+       for i in xrange(maxsz[0]):
+           but=self.butsSizeX[i]
+           but.set_active(i==id)
+
+       id=sz[1]-1
+
+       for i in xrange(maxsz[1]):
+           but=self.butsSizeY[i]
+           but.set_active(i==id)
+
+       self.ignore_toggle=False
+
+       self.igw.setSize(sz)
+
+       self.igw.queue_draw()
+       self.queue_draw()
+
+    def getSize(self):
+       return(self.igw.getSize())
+
+    def getIndiv(self):
+       ret=self.buttonRotateIndividually.get_active()
+
+       return(ret)
+
+    def setIndiv(self, indiv):
+       if indiv:
+           for i in self.butsSizeY:
+               i.set_sensitive(True)
+           for i in self.butsSizeX:
+               i.set_sensitive(True)
+
+       else:
+           sz=self.config.getMaxSize()
+
+           for i in self.butsSizeY:
+               i.set_sensitive(False)
+
+           cnt=0
+           for i in self.butsSizeX:
+               cnt+=1
+               # Height is always the narrower, so use that as a limit
+               if cnt>sz[1]:
+                   i.set_sensitive(False)
+               else:
+                   i.set_sensitive(True)
+
+           sz=self.getSize()
+           szx=sz[0]
+           if szx>sz[1]:
+               szx=sz[1]
+           self.setSize((szx, szx))
+
+       self.buttonRotateIndividually.set_active(indiv)
+
+    def setLongpress(self, lp):
+       self.buttonRequireLongpress.set_active(lp)
+
+    def setAnimate(self, ar):
+       self.buttonAnimateRotation.set_active(ar)
+
+    def getNoBg(self):
+       ret=self.buttonNoBackground.get_active()
+       return(ret)
+
+    def setNoBg(self, nobg):
+       self.buttonNoBackground.set_active(nobg)
+
+       self.buttonThemeBackground.set_sensitive(not nobg)
+       self.config.setNoBg(nobg)
+       self.igw.reconfig()
+
+    def getThemeBg(self):
+       ret=self.buttonThemeBackground.get_active()
+       return(ret)
+
+    def setThemeBg(self, themebg):
+       self.buttonThemeBackground.set_active(themebg)
+       self.config.setThemeBg(themebg)
+       self.igw.reconfig()
+
+    def doConfig(self, icon):
+       aps=apps.scan()
+
+       lst=[aps[x]['name'] for x in aps]
+       lst.sort()
+
+       dialog=gtk.Dialog('App select', None,
+           gtk.DIALOG_DESTROY_WITH_PARENT, buttons=())
+
+       selector=hildon.TouchSelectorEntry(text=True)
+       selector.set_column_selection_mode(
+           hildon.TOUCH_SELECTOR_SELECTION_MODE_SINGLE)
+
+       dialog.vbox.pack_start(selector, True, True, 0)
+       dialog.set_size_request(0,900)
+       dialog.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
+       dialog.add_button(gtk.STOCK_OK, gtk.RESPONSE_OK)
+
+       selector.append_text('None')
+
+       idx=0
+       cnt=1
+       for app in lst:
+           if app==None:
+               continue
+           selector.append_text(app)
+           if icon.appname!=None and aps[icon.appname]['name']==app:
+               idx=cnt
+           cnt+=1
+
+       selector.set_active(0, idx)
+
+       dialog.show_all()
+
+       app=None
+
+       r=dialog.run()
+
+       if r==gtk.RESPONSE_OK:
+           idx2=selector.get_active(0)
+           if idx2<1:
+               app=None
+           else:
+               cur=lst[idx2-1]
+               for i in aps:
+                   if aps[i]['name']==cur:
+                       app=aps[i]
+                       break
+           if app!=None:
+               app['icon2']=getIcon(app['icon'], self.config.getIconSize())
+           else:
+               app={
+                   'id':       None,
+                   'icon2':    None,
+                   }
+           icon.setApp(app)
+
+       dialog.destroy()
+
+#    def finish(self):
+#      print "wc-finish"
+#      self.igw=None
+#
+#      self.dis_finish()
+
+    def getData(self):
+       szx=0
+       szy=0
+       for but in self.butsSizeX:
+           szx+=1
+           if but.get_active()==True:
+               break
+
+       for but in self.butsSizeY:
+           szy+=1
+           if but.get_active()==True:
+               break
+
+       if self.getIndiv():
+           sz=(szx, szy)
+       else:
+           sz=(szx, szx)
+
+       wapps={}
+
+       for x in xrange(sz[0]):
+           for y in xrange(sz[1]):
+               ico=self.igw.get(x,y)
+               k=(x,y)
+               wapps[k]=ico.appname
+
+       indiv=self.buttonRotateIndividually.get_active()
+       lp=self.buttonRequireLongpress.get_active()
+       ar=self.buttonAnimateRotation.get_active()
+       nobg=self.buttonNoBackground.get_active()
+       themebg=self.buttonThemeBackground.get_active()
+
+       ret={
+           'size':         sz,
+           'apps':         wapps,
+           'indiv':        indiv,
+           'longpress':    lp,
+           'animate':      ar,
+           'nobg':         nobg,
+           'themebg':      themebg,
+           'iconsize':     self.config.getIconSize(),
+           'iconpadding':  self.config.getIconPadding(),
+           'iconmargin':   self.config.getIconMargin(),
+           }
+
+       return(ret)
+
+if __name__=="__main__":
+    win=WinConfig()
+    win.connect('delete-event', gtk.main_quit)
+
+    win.show_all()
+    gtk.main()
+
+
+
+# vim: set ts=8 sts=4 sw=4 noet formatoptions=r ai nocindent:
+
diff --git a/drlaunch/src/xdg/BaseDirectory.py b/drlaunch/src/xdg/BaseDirectory.py
new file mode 100644 (file)
index 0000000..6f532c9
--- /dev/null
@@ -0,0 +1,97 @@
+"""
+This module is based on a rox module (LGPL):
+
+http://cvs.sourceforge.net/viewcvs.py/rox/ROX-Lib2/python/rox/basedir.py?rev=1.9&view=log
+
+The freedesktop.org Base Directory specification provides a way for
+applications to locate shared data and configuration:
+
+    http://standards.freedesktop.org/basedir-spec/
+
+(based on version 0.6)
+
+This module can be used to load and save from and to these directories.
+
+Typical usage:
+
+    from rox import basedir
+    
+    for dir in basedir.load_config_paths('mydomain.org', 'MyProg', 'Options'):
+        print "Load settings from", dir
+
+    dir = basedir.save_config_path('mydomain.org', 'MyProg')
+    print >>file(os.path.join(dir, 'Options'), 'w'), "foo=2"
+
+Note: see the rox.Options module for a higher-level API for managing options.
+"""
+
+from __future__ import generators
+import os
+
+_home = os.environ.get('HOME', '/')
+xdg_data_home = os.environ.get('XDG_DATA_HOME',
+            os.path.join(_home, '.local', 'share'))
+
+xdg_data_dirs = [xdg_data_home] + \
+    os.environ.get('XDG_DATA_DIRS', '/usr/local/share:/usr/share').split(':')
+
+xdg_config_home = os.environ.get('XDG_CONFIG_HOME',
+            os.path.join(_home, '.config'))
+
+xdg_config_dirs = [xdg_config_home] + \
+    os.environ.get('XDG_CONFIG_DIRS', '/etc/xdg').split(':')
+
+xdg_cache_home = os.environ.get('XDG_CACHE_HOME',
+            os.path.join(_home, '.cache'))
+
+xdg_data_dirs = filter(lambda x: x, xdg_data_dirs)
+xdg_config_dirs = filter(lambda x: x, xdg_config_dirs)
+
+def save_config_path(*resource):
+    """Ensure $XDG_CONFIG_HOME/<resource>/ exists, and return its path.
+    'resource' should normally be the name of your application. Use this
+    when SAVING configuration settings. Use the xdg_config_dirs variable
+    for loading."""
+    resource = os.path.join(*resource)
+    assert not resource.startswith('/')
+    path = os.path.join(xdg_config_home, resource)
+    if not os.path.isdir(path):
+        os.makedirs(path, 0700)
+    return path
+
+def save_data_path(*resource):
+    """Ensure $XDG_DATA_HOME/<resource>/ exists, and return its path.
+    'resource' is the name of some shared resource. Use this when updating
+    a shared (between programs) database. Use the xdg_data_dirs variable
+    for loading."""
+    resource = os.path.join(*resource)
+    assert not resource.startswith('/')
+    path = os.path.join(xdg_data_home, resource)
+    if not os.path.isdir(path):
+        os.makedirs(path)
+    return path
+
+def load_config_paths(*resource):
+    """Returns an iterator which gives each directory named 'resource' in the
+    configuration search path. Information provided by earlier directories should
+    take precedence over later ones (ie, the user's config dir comes first)."""
+    resource = os.path.join(*resource)
+    for config_dir in xdg_config_dirs:
+        path = os.path.join(config_dir, resource)
+        if os.path.exists(path): yield path
+
+def load_first_config(*resource):
+    """Returns the first result from load_config_paths, or None if there is nothing
+    to load."""
+    for x in load_config_paths(*resource):
+        return x
+    return None
+
+def load_data_paths(*resource):
+    """Returns an iterator which gives each directory named 'resource' in the
+    shared data search path. Information provided by earlier directories should
+    take precedence over later ones."""
+    resource = os.path.join(*resource)
+    for data_dir in xdg_data_dirs:
+        path = os.path.join(data_dir, resource)
+        if os.path.exists(path): yield path
diff --git a/drlaunch/src/xdg/Config.py b/drlaunch/src/xdg/Config.py
new file mode 100644 (file)
index 0000000..e2fbe64
--- /dev/null
@@ -0,0 +1,39 @@
+"""
+Functions to configure Basic Settings
+"""
+
+language = "C"
+windowmanager = None
+icon_theme = "highcolor"
+icon_size = 48
+cache_time = 5
+root_mode = False
+
+def setWindowManager(wm):
+    global windowmanager
+    windowmanager = wm
+
+def setIconTheme(theme):
+    global icon_theme
+    icon_theme = theme
+    import xdg.IconTheme
+    xdg.IconTheme.themes = []
+
+def setIconSize(size):
+    global icon_size
+    icon_size = size
+
+def setCacheTime(time):
+    global cache_time
+    cache_time = time
+
+def setLocale(lang):
+    import locale
+    lang = locale.normalize(lang)
+    locale.setlocale(locale.LC_ALL, lang)
+    import xdg.Locale
+    xdg.Locale.update(lang)
+
+def setRootMode(boolean):
+    global root_mode
+    root_mode = boolean
diff --git a/drlaunch/src/xdg/DesktopEntry.py b/drlaunch/src/xdg/DesktopEntry.py
new file mode 100644 (file)
index 0000000..8626d7f
--- /dev/null
@@ -0,0 +1,397 @@
+"""
+Complete implementation of the XDG Desktop Entry Specification Version 0.9.4
+http://standards.freedesktop.org/desktop-entry-spec/
+
+Not supported:
+- Encoding: Legacy Mixed
+- Does not check exec parameters
+- Does not check URL's
+- Does not completly validate deprecated/kde items
+- Does not completly check categories
+"""
+
+from xdg.IniFile import *
+from xdg.BaseDirectory import *
+import os.path
+
+class DesktopEntry(IniFile):
+    "Class to parse and validate DesktopEntries"
+
+    defaultGroup = 'Desktop Entry'
+
+    def __init__(self, filename=None):
+        self.content = dict()
+        if filename and os.path.exists(filename):
+            self.parse(filename)
+        elif filename:
+            self.new(filename)
+
+    def __str__(self):
+        return self.getName()
+
+    def parse(self, file):
+        IniFile.parse(self, file, ["Desktop Entry", "KDE Desktop Entry"])
+
+    # start standard keys
+    def getType(self):
+        return self.get('Type')
+    """ @deprecated, use getVersionString instead """
+    def getVersion(self):
+        return self.get('Version', type="numeric")
+    def getVersionString(self):
+        return self.get('Version')
+    def getName(self):
+        return self.get('Name', locale=True)
+    def getGenericName(self):
+        return self.get('GenericName', locale=True)
+    def getNoDisplay(self):
+        return self.get('NoDisplay', type="boolean")
+    def getComment(self):
+        return self.get('Comment', locale=True)
+    def getIcon(self):
+        return self.get('Icon', locale=True)
+    def getHidden(self):
+        return self.get('Hidden', type="boolean")
+    def getOnlyShowIn(self):
+        return self.get('OnlyShowIn', list=True)
+    def getNotShowIn(self):
+        return self.get('NotShowIn', list=True)
+    def getTryExec(self):
+        return self.get('TryExec')
+    def getExec(self):
+        return self.get('Exec')
+    def getPath(self):
+        return self.get('Path')
+    def getTerminal(self):
+        return self.get('Terminal', type="boolean")
+    """ @deprecated, use getMimeTypes instead """
+    def getMimeType(self):
+        return self.get('MimeType', list=True, type="regex")
+    def getMimeTypes(self):
+        return self.get('MimeType', list=True)
+    def getCategories(self):
+        return self.get('Categories', list=True)
+    def getStartupNotify(self):
+        return self.get('StartupNotify', type="boolean")
+    def getStartupWMClass(self):
+        return self.get('StartupWMClass')
+    def getURL(self):
+        return self.get('URL')
+    # end standard keys
+
+    # start kde keys
+    def getServiceTypes(self):
+        return self.get('ServiceTypes', list=True)
+    def getDocPath(self):
+        return self.get('DocPath')
+    def getKeywords(self):
+        return self.get('Keywords', list=True, locale=True)
+    def getInitialPreference(self):
+        return self.get('InitialPreference')
+    def getDev(self):
+        return self.get('Dev')
+    def getFSType(self):
+        return self.get('FSType')
+    def getMountPoint(self):
+        return self.get('MountPoint')
+    def getReadonly(self):
+        return self.get('ReadOnly', type="boolean")
+    def getUnmountIcon(self):
+        return self.get('UnmountIcon', locale=True)
+    # end kde keys
+
+    # start deprecated keys
+    def getMiniIcon(self):
+        return self.get('MiniIcon', locale=True)
+    def getTerminalOptions(self):
+        return self.get('TerminalOptions')
+    def getDefaultApp(self):
+        return self.get('DefaultApp')
+    def getProtocols(self):
+        return self.get('Protocols', list=True)
+    def getExtensions(self):
+        return self.get('Extensions', list=True)
+    def getBinaryPattern(self):
+        return self.get('BinaryPattern')
+    def getMapNotify(self):
+        return self.get('MapNotify')
+    def getEncoding(self):
+        return self.get('Encoding')
+    def getSwallowTitle(self):
+        return self.get('SwallowTitle', locale=True)
+    def getSwallowExec(self):
+        return self.get('SwallowExec')
+    def getSortOrder(self): 
+        return self.get('SortOrder', list=True)
+    def getFilePattern(self):
+        return self.get('FilePattern', type="regex")
+    def getActions(self):
+        return self.get('Actions', list=True)
+    # end deprecated keys
+
+    # desktop entry edit stuff
+    def new(self, filename):
+        if os.path.splitext(filename)[1] == ".desktop":
+            type = "Application"
+        elif os.path.splitext(filename)[1] == ".directory":
+            type = "Directory"
+        else:
+            raise ParsingError("Unknown extension", filename)
+
+        self.content = dict()
+        self.addGroup(self.defaultGroup)
+        self.set("Type", type)
+        self.filename = filename
+    # end desktop entry edit stuff
+
+    # validation stuff
+    def checkExtras(self):
+        # header
+        if self.defaultGroup == "KDE Desktop Entry":
+            self.warnings.append('[KDE Desktop Entry]-Header is deprecated')
+
+        # file extension
+        if self.fileExtension == ".kdelnk":
+            self.warnings.append("File extension .kdelnk is deprecated")
+        elif self.fileExtension != ".desktop" and self.fileExtension != ".directory":
+            self.warnings.append('Unknown File extension')
+
+        # Type
+        try:
+            self.type = self.content[self.defaultGroup]["Type"]
+        except KeyError:
+            self.errors.append("Key 'Type' is missing")
+
+        # Name
+        try:
+            self.name = self.content[self.defaultGroup]["Name"]
+        except KeyError:
+            self.errors.append("Key 'Name' is missing")
+
+    def checkGroup(self, group):
+        # check if group header is valid
+        if not (group == self.defaultGroup \
+        or re.match("^\Desktop Action [a-zA-Z]+\$", group) \
+        or (re.match("^\X-", group) and group.decode("utf-8", "ignore").encode("ascii", 'ignore') == group)):
+            self.errors.append("Invalid Group name: %s" % group)
+        else:
+            #OnlyShowIn and NotShowIn
+            if self.content[group].has_key("OnlyShowIn") and self.content[group].has_key("NotShowIn"):
+                self.errors.append("Group may either have OnlyShowIn or NotShowIn, but not both")
+
+    def checkKey(self, key, value, group):
+        # standard keys     
+        if key == "Type":
+            if value == "ServiceType" or value == "Service" or value == "FSDevice":
+                self.warnings.append("Type=%s is a KDE extension" % key)
+            elif value == "MimeType":
+                self.warnings.append("Type=MimeType is deprecated")
+            elif not (value == "Application" or value == "Link" or value == "Directory"):
+                self.errors.append("Value of key 'Type' must be Application, Link or Directory, but is '%s'" % value)
+
+            if self.fileExtension == ".directory" and not value == "Directory":
+                self.warnings.append("File extension is .directory, but Type is '%s'" % value)
+            elif self.fileExtension == ".desktop" and value == "Directory":
+                self.warnings.append("Files with Type=Directory should have the extension .directory")
+
+            if value == "Application":
+                if not self.content[group].has_key("Exec"):
+                    self.warnings.append("Type=Application needs 'Exec' key")
+            if value == "Link":
+                if not self.content[group].has_key("URL"):
+                    self.warnings.append("Type=Application needs 'Exec' key")
+
+        elif key == "Version":
+            self.checkValue(key, value)
+
+        elif re.match("^Name"+xdg.Locale.regex+"$", key):
+            pass # locale string
+
+        elif re.match("^GenericName"+xdg.Locale.regex+"$", key):
+            pass # locale string
+
+        elif key == "NoDisplay":
+            self.checkValue(key, value, type="boolean")
+
+        elif re.match("^Comment"+xdg.Locale.regex+"$", key):
+            pass # locale string
+
+        elif re.match("^Icon"+xdg.Locale.regex+"$", key):
+            self.checkValue(key, value)
+
+        elif key == "Hidden":
+            self.checkValue(key, value, type="boolean")
+
+        elif key == "OnlyShowIn":
+            self.checkValue(key, value, list=True)
+            self.checkOnlyShowIn(value)
+
+        elif key == "NotShowIn":
+            self.checkValue(key, value, list=True)
+            self.checkOnlyShowIn(value)
+
+        elif key == "TryExec":
+            self.checkValue(key, value)
+            self.checkType(key, "Application")
+
+        elif key == "Exec":
+            self.checkValue(key, value)
+            self.checkType(key, "Application")
+
+        elif key == "Path":
+            self.checkValue(key, value)
+            self.checkType(key, "Application")
+
+        elif key == "Terminal":
+            self.checkValue(key, value, type="boolean")
+            self.checkType(key, "Application")
+
+        elif key == "MimeType":
+            self.checkValue(key, value, list=True)
+            self.checkType(key, "Application")
+
+        elif key == "Categories":
+            self.checkValue(key, value)
+            self.checkType(key, "Application")
+            self.checkCategorie(value)
+
+        elif key == "StartupNotify":
+            self.checkValue(key, value, type="boolean")
+            self.checkType(key, "Application")
+
+        elif key == "StartupWMClass":
+            self.checkType(key, "Application")
+
+        elif key == "URL":
+            self.checkValue(key, value)
+            self.checkType(key, "URL")
+
+        # kde extensions
+        elif key == "ServiceTypes":
+            self.checkValue(key, value, list=True)
+            self.warnings.append("Key '%s' is a KDE extension" % key)
+
+        elif key == "DocPath":
+            self.checkValue(key, value)
+            self.warnings.append("Key '%s' is a KDE extension" % key)
+
+        elif re.match("^Keywords"+xdg.Locale.regex+"$", key):
+            self.checkValue(key, value, list=True)
+            self.warnings.append("Key '%s' is a KDE extension" % key)
+
+        elif key == "InitialPreference":
+            self.checkValue(key, value, type="numeric")
+            self.warnings.append("Key '%s' is a KDE extension" % key)
+
+        elif key == "Dev":
+            self.checkValue(key, value)
+            self.checkType(key, "FSDevice")
+            self.warnings.append("Key '%s' is a KDE extension" % key)
+
+        elif key == "FSType":
+            self.checkValue(key, value)
+            self.checkType(key, "FSDevice")
+            self.warnings.append("Key '%s' is a KDE extension" % key)
+
+        elif key == "MountPoint":
+            self.checkValue(key, value)
+            self.checkType(key, "FSDevice")
+            self.warnings.append("Key '%s' is a KDE extension" % key)
+
+        elif key == "ReadOnly":
+            self.checkValue(key, value, type="boolean")
+            self.checkType(key, "FSDevice")
+            self.warnings.append("Key '%s' is a KDE extension" % key)
+
+        elif re.match("^UnmountIcon"+xdg.Locale.regex+"$", key):
+            self.checkValue(key, value)
+            self.checkType(key, "FSDevice")
+            self.warnings.append("Key '%s' is a KDE extension" % key)
+
+        # deprecated keys
+        elif key == "Encoding":
+            self.checkValue(key, value)
+            self.warnings.append("Key '%s' is deprecated" % key)
+
+        elif re.match("^MiniIcon"+xdg.Locale.regex+"$", key):
+            self.checkValue(key, value)
+            self.warnings.append("Key '%s' is deprecated" % key)
+
+        elif key == "TerminalOptions":
+            self.checkValue(key, value)
+            self.warnings.append("Key '%s' is deprecated" % key)
+
+        elif key == "DefaultApp":
+            self.checkValue(key, value)
+            self.warnings.append("Key '%s' is deprecated" % key)
+
+        elif key == "Protocols":
+            self.checkValue(key, value, list=True)
+            self.warnings.append("Key '%s' is deprecated" % key)
+
+        elif key == "Extensions":
+            self.checkValue(key, value, list=True)
+            self.warnings.append("Key '%s' is deprecated" % key)
+
+        elif key == "BinaryPattern":
+            self.checkValue(key, value)
+            self.warnings.append("Key '%s' is deprecated" % key)
+
+        elif key == "MapNotify":
+            self.checkValue(key, value)
+            self.warnings.append("Key '%s' is deprecated" % key)
+
+        elif re.match("^SwallowTitle"+xdg.Locale.regex+"$", key):
+            self.warnings.append("Key '%s' is deprecated" % key)
+
+        elif key == "SwallowExec":
+            self.checkValue(key, value)
+            self.warnings.append("Key '%s' is deprecated" % key)
+
+        elif key == "FilePattern":
+            self.checkValue(key, value, type="regex", list=True)
+            self.warnings.append("Key '%s' is deprecated" % key)
+
+        elif key == "SortOrder":
+            self.checkValue(key, value, list=True)
+            self.warnings.append("Key '%s' is deprecated" % key)
+
+        elif key == "Actions":
+            self.checkValue(key, value, list=True)
+            self.warnings.append("Key '%s' is deprecated" % key)
+
+        # "X-" extensions
+        elif re.match("^X-[a-zA-Z0-9-]+", key):
+            pass
+
+        else:
+            self.errors.append("Invalid key: %s" % key)
+
+    def checkType(self, key, type):
+        if not self.getType() == type:
+            self.errors.append("Key '%s' only allowed in Type=%s" % (key, type))
+
+    def checkOnlyShowIn(self, value):
+        values = self.getList(value)
+        valid = ["GNOME", "KDE", "ROX", "XFCE", "Old", "LXDE"]
+        for item in values:
+            if item not in valid and item[0:2] != "X-":
+                self.errors.append("'%s' is not a registered OnlyShowIn value" % item);
+
+    def checkCategorie(self, value):
+        values = self.getList(value)
+
+        main = ["AudioVideo", "Audio", "Video", "Development", "Education", "Game", "Graphics", "Network", "Office", "Settings", "System", "Utility"]
+        hasmain = False
+        for item in values:
+            if item in main:
+                hasmain = True
+        if hasmain == False:
+            self.errors.append("Missing main category")
+
+        additional = ["Building", "Debugger", "IDE", "GUIDesigner", "Profiling", "RevisionControl", "Translation", "Calendar", "ContactManagement", "Database", "Dictionary", "Chart", "Email", "Finance", "FlowChart", "PDA", "ProjectManagement", "Presentation", "Spreadsheet", "WordProcessor", "2DGraphics", "VectorGraphics", "3DGraphics", "RasterGraphics", "Scanning", "OCR", "Photography", "Publishing", "Viewer", "TextTools", "DesktopSettings", "HardwareSettings", "Printing", "PackageManager", "Dialup", "InstantMessaging", "Chat", "IRCClient", "FileTransfer", "HamRadio", "News", "P2P", "RemoteAccess", "Telephony", "TelephonyTools", "VideoConference", "WebBrowser", "WebDevelopment", "Midi", "Mixer", "Sequencer", "Tuner", "TV", "AudioVideoEditing", "Player", "Recorder", "DiscBurning", "ActionGame", "AdventureGame", "ArcadeGame", "BoardGame", "BlocksGame", "CardGame", "KidsGame", "LogicGame", "RolePlaying", "Simulation", "SportsGame", "StrategyGame", "Art", "Construction", "Music", "Languages", "Science", "ArtificialIntelligence", "Astronomy", "Biology", "Chemistry", "ComputerScience", "DataVisualization", "Economy", "Electricity", "Geography", "Geology", "Geoscience", "History", "ImageProcessing", "Literature", "Math", "NumericalAnalysis", "MedicalSoftware", "Physics", "Robotics", "Sports", "ParallelComputing", "Amusement", "Archiving", "Compression", "Electronics", "Emulator", "Engineering", "FileTools", "FileManager", "TerminalEmulator", "Filesystem", "Monitor", "Security", "Accessibility", "Calculator", "Clock", "TextEditor", "Documentation", "Core", "KDE", "GNOME", "GTK", "Qt", "Motif", "Java", "ConsoleOnly", "Screensaver", "TrayIcon", "Applet", "Shell"]
+        
+        for item in values:
+            if item not in additional + main and item[0:2] != "X-":
+                self.errors.append("'%s' is not a registered Category" % item);
+
diff --git a/drlaunch/src/xdg/Exceptions.py b/drlaunch/src/xdg/Exceptions.py
new file mode 100644 (file)
index 0000000..f7d08be
--- /dev/null
@@ -0,0 +1,51 @@
+"""
+Exception Classes for the xdg package
+"""
+
+debug = False
+
+class Error(Exception):
+    def __init__(self, msg):
+        self.msg = msg
+        Exception.__init__(self, msg)
+    def __str__(self):
+        return self.msg
+
+class ValidationError(Error):
+    def __init__(self, msg, file):
+        self.msg = msg
+        self.file = file
+        Error.__init__(self, "ValidationError in file '%s': %s " % (file, msg))
+
+class ParsingError(Error):
+    def __init__(self, msg, file):
+        self.msg = msg
+        self.file = file
+        Error.__init__(self, "ParsingError in file '%s', %s" % (file, msg))
+
+class NoKeyError(Error):
+    def __init__(self, key, group, file):
+        Error.__init__(self, "No key '%s' in group %s of file %s" % (key, group, file))
+        self.key = key
+        self.group = group
+
+class DuplicateKeyError(Error):
+    def __init__(self, key, group, file):
+        Error.__init__(self, "Duplicate key '%s' in group %s of file %s" % (key, group, file))
+        self.key = key
+        self.group = group
+
+class NoGroupError(Error):
+    def __init__(self, group, file):
+        Error.__init__(self, "No group: %s in file %s" % (group, file))
+        self.group = group
+
+class DuplicateGroupError(Error):
+    def __init__(self, group, file):
+        Error.__init__(self, "Duplicate group: %s in file %s" % (group, file))
+        self.group = group
+
+class NoThemeError(Error):
+    def __init__(self, theme):
+        Error.__init__(self, "No such icon-theme: %s" % theme)
+        self.theme = theme
diff --git a/drlaunch/src/xdg/IconTheme.py b/drlaunch/src/xdg/IconTheme.py
new file mode 100644 (file)
index 0000000..1f1fa18
--- /dev/null
@@ -0,0 +1,391 @@
+"""
+Complete implementation of the XDG Icon Spec Version 0.8
+http://standards.freedesktop.org/icon-theme-spec/
+"""
+
+import os, sys, time
+
+from xdg.IniFile import *
+from xdg.BaseDirectory import *
+from xdg.Exceptions import *
+
+import xdg.Config
+
+class IconTheme(IniFile):
+    "Class to parse and validate IconThemes"
+    def __init__(self):
+        IniFile.__init__(self)
+
+    def __repr__(self):
+        return self.name
+
+    def parse(self, file):
+        IniFile.parse(self, file, ["Icon Theme", "KDE Icon Theme"])
+        self.dir = os.path.dirname(file)
+        (nil, self.name) = os.path.split(self.dir)
+
+    def getDir(self):
+        return self.dir
+
+    # Standard Keys
+    def getName(self):
+        return self.get('Name', locale=True)
+    def getComment(self):
+        return self.get('Comment', locale=True)
+    def getInherits(self):
+        return self.get('Inherits', list=True)
+    def getDirectories(self):
+        return self.get('Directories', list=True)
+    def getHidden(self):
+        return self.get('Hidden', type="boolean")
+    def getExample(self):
+        return self.get('Example')
+
+    # Per Directory Keys
+    def getSize(self, directory):
+        return self.get('Size', type="integer", group=directory)
+    def getContext(self, directory):
+        return self.get('Context', group=directory)
+    def getType(self, directory):
+        value = self.get('Type', group=directory)
+        if value:
+            return value
+        else:
+            return "Threshold"
+    def getMaxSize(self, directory):
+        value = self.get('MaxSize', type="integer", group=directory)
+        if value or value == 0:
+            return value
+        else:
+            return self.getSize(directory)
+    def getMinSize(self, directory):
+        value = self.get('MinSize', type="integer", group=directory)
+        if value or value == 0:
+            return value
+        else:
+            return self.getSize(directory)
+    def getThreshold(self, directory):
+        value = self.get('Threshold', type="integer", group=directory)
+        if value or value == 0:
+            return value
+        else:
+            return 2
+
+    # validation stuff
+    def checkExtras(self):
+        # header
+        if self.defaultGroup == "KDE Icon Theme":
+            self.warnings.append('[KDE Icon Theme]-Header is deprecated')
+
+        # file extension
+        if self.fileExtension == ".theme":
+            pass
+        elif self.fileExtension == ".desktop":
+            self.warnings.append('.desktop fileExtension is deprecated')
+        else:
+            self.warnings.append('Unknown File extension')
+
+        # Check required keys
+        # Name
+        try:
+            self.name = self.content[self.defaultGroup]["Name"]
+        except KeyError:
+            self.errors.append("Key 'Name' is missing")
+
+        # Comment
+        try:
+            self.comment = self.content[self.defaultGroup]["Comment"]
+        except KeyError:
+            self.errors.append("Key 'Comment' is missing")
+
+        # Directories
+        try:
+            self.directories = self.content[self.defaultGroup]["Directories"]
+        except KeyError:
+            self.errors.append("Key 'Directories' is missing")
+
+    def checkGroup(self, group):
+        # check if group header is valid
+        if group == self.defaultGroup:
+            pass
+        elif group in self.getDirectories():
+            try:
+                self.type = self.content[group]["Type"]
+            except KeyError:
+                self.type = "Threshold"
+            try:
+                self.name = self.content[group]["Name"]
+            except KeyError:
+                self.errors.append("Key 'Name' in Group '%s' is missing" % group)
+        elif not (re.match("^\[X-", group) and group.decode("utf-8", "ignore").encode("ascii", 'ignore') == group):
+            self.errors.append("Invalid Group name: %s" % group)
+
+    def checkKey(self, key, value, group):
+        # standard keys     
+        if group == self.defaultGroup:
+            if re.match("^Name"+xdg.Locale.regex+"$", key):
+                pass
+            elif re.match("^Comment"+xdg.Locale.regex+"$", key):
+                pass
+            elif key == "Inherits":
+                self.checkValue(key, value, list=True)
+            elif key == "Directories":
+                self.checkValue(key, value, list=True)
+            elif key == "Hidden":
+                self.checkValue(key, value, type="boolean")
+            elif key == "Example":
+                self.checkValue(key, value)
+            elif re.match("^X-[a-zA-Z0-9-]+", key):
+                pass
+            else:
+                self.errors.append("Invalid key: %s" % key)
+        elif group in self.getDirectories():
+            if key == "Size":
+                self.checkValue(key, value, type="integer")
+            elif key == "Context":
+                self.checkValue(key, value)
+            elif key == "Type":
+                self.checkValue(key, value)
+                if value not in ["Fixed", "Scalable", "Threshold"]:
+                    self.errors.append("Key 'Type' must be one out of 'Fixed','Scalable','Threshold', but is %s" % value)
+            elif key == "MaxSize":
+                self.checkValue(key, value, type="integer")
+                if self.type != "Scalable":
+                    self.errors.append("Key 'MaxSize' give, but Type is %s" % self.type)
+            elif key == "MinSize":
+                self.checkValue(key, value, type="integer")
+                if self.type != "Scalable":
+                    self.errors.append("Key 'MinSize' give, but Type is %s" % self.type)
+            elif key == "Threshold":
+                self.checkValue(key, value, type="integer")
+                if self.type != "Threshold":
+                    self.errors.append("Key 'Threshold' give, but Type is %s" % self.type)
+            elif re.match("^X-[a-zA-Z0-9-]+", key):
+                pass
+            else:
+                self.errors.append("Invalid key: %s" % key)
+
+
+class IconData(IniFile):
+    "Class to parse and validate IconData Files"
+    def __init__(self):
+        IniFile.__init__(self)
+
+    def __repr__(self):
+        return self.getDisplayName()
+
+    def parse(self, file):
+        IniFile.parse(self, file, ["Icon Data"])
+
+    # Standard Keys
+    def getDisplayName(self):
+        return self.get('DisplayName', locale=True)
+    def getEmbeddedTextRectangle(self):
+        return self.get('EmbeddedTextRectangle', list=True)
+    def getAttachPoints(self):
+        return self.get('AttachPoints', type="point", list=True)
+
+    # validation stuff
+    def checkExtras(self):
+        # file extension
+        if self.fileExtension != ".icon":
+            self.warnings.append('Unknown File extension')
+
+    def checkGroup(self, group):
+        # check if group header is valid
+        if not (group == self.defaultGroup \
+        or (re.match("^\[X-", group) and group.encode("ascii", 'ignore') == group)):
+            self.errors.append("Invalid Group name: %s" % group.encode("ascii", "replace"))
+
+    def checkKey(self, key, value, group):
+        # standard keys     
+        if re.match("^DisplayName"+xdg.Locale.regex+"$", key):
+            pass
+        elif key == "EmbeddedTextRectangle":
+            self.checkValue(key, value, type="integer", list=True)
+        elif key == "AttachPoints":
+            self.checkValue(key, value, type="point", list=True)
+        elif re.match("^X-[a-zA-Z0-9-]+", key):
+            pass
+        else:
+            self.errors.append("Invalid key: %s" % key)
+
+
+
+icondirs = []
+for basedir in xdg_data_dirs:
+    icondirs.append(os.path.join(basedir, "icons"))
+    icondirs.append(os.path.join(basedir, "pixmaps"))
+icondirs.append(os.path.expanduser("~/.icons"))
+
+# just cache variables, they give a 10x speed improvement
+themes = []
+cache = dict()
+dache = dict()
+eache = dict()
+
+def getIconPath(iconname, size = None, theme = None, extensions = ["png", "svg", "xpm"]):
+    global themes
+
+    if size == None:
+        size = xdg.Config.icon_size
+    if theme == None:
+        theme = xdg.Config.icon_theme
+
+    # if we have an absolute path, just return it
+    if os.path.isabs(iconname):
+        return iconname
+
+    # check if it has an extension and strip it
+    if os.path.splitext(iconname)[1][1:] in extensions:
+        iconname = os.path.splitext(iconname)[0]
+
+    # parse theme files
+    try:
+        if themes[0].name != theme:
+            themes = []
+            __addTheme(theme)
+    except IndexError:
+        __addTheme(theme)
+
+    # more caching (icon looked up in the last 5 seconds?)
+    tmp = "".join([iconname, str(size), theme, "".join(extensions)])
+    if eache.has_key(tmp):
+        if int(time.time() - eache[tmp][0]) >= xdg.Config.cache_time:
+            del eache[tmp]
+        else:
+            return eache[tmp][1]
+
+    for thme in themes:
+        icon = LookupIcon(iconname, size, thme, extensions)
+        if icon:
+            eache[tmp] = [time.time(), icon]
+            return icon
+
+    # cache stuff again (directories lookuped up in the last 5 seconds?)
+    for directory in icondirs:
+        if (not dache.has_key(directory) \
+            or (int(time.time() - dache[directory][1]) >= xdg.Config.cache_time \
+            and dache[directory][2] < os.path.getmtime(directory))) \
+            and os.path.isdir(directory):
+            dache[directory] = [os.listdir(directory), time.time(), os.path.getmtime(directory)]
+
+    for dir, values in dache.items():
+        for extension in extensions:
+            try:
+                if iconname + "." + extension in values[0]:
+                    icon = os.path.join(dir, iconname + "." + extension)
+                    eache[tmp] = [time.time(), icon]
+                    return icon
+            except UnicodeDecodeError, e:
+                if debug:
+                    raise e
+                else:
+                    pass
+
+    # we haven't found anything? "hicolor" is our fallback
+    if theme != "hicolor":
+        icon = getIconPath(iconname, size, "hicolor")
+        eache[tmp] = [time.time(), icon]
+        return icon
+
+def getIconData(path):
+    if os.path.isfile(path):
+        dirname = os.path.dirname(path)
+        basename = os.path.basename(path)
+        if os.path.isfile(os.path.join(dirname, basename + ".icon")):
+            data = IconData()
+            data.parse(os.path.join(dirname, basename + ".icon"))
+            return data
+
+def __addTheme(theme):
+    for dir in icondirs:
+        if os.path.isfile(os.path.join(dir, theme, "index.theme")):
+            __parseTheme(os.path.join(dir,theme, "index.theme"))
+            break
+        elif os.path.isfile(os.path.join(dir, theme, "index.desktop")):
+            __parseTheme(os.path.join(dir,theme, "index.desktop"))
+            break
+    else:
+        if debug:
+            raise NoThemeError(theme)
+
+def __parseTheme(file):
+    theme = IconTheme()
+    theme.parse(file)
+    themes.append(theme)
+    for subtheme in theme.getInherits():
+        __addTheme(subtheme)
+
+def LookupIcon(iconname, size, theme, extensions):
+    # look for the cache
+    if not cache.has_key(theme.name):
+        cache[theme.name] = []
+        cache[theme.name].append(time.time() - (xdg.Config.cache_time + 1)) # [0] last time of lookup
+        cache[theme.name].append(0)               # [1] mtime
+        cache[theme.name].append(dict())          # [2] dir: [subdir, [items]]
+
+    # cache stuff (directory lookuped up the in the last 5 seconds?)
+    if int(time.time() - cache[theme.name][0]) >= xdg.Config.cache_time:
+        cache[theme.name][0] = time.time()
+        for subdir in theme.getDirectories():
+            for directory in icondirs:
+                dir = os.path.join(directory,theme.name,subdir)
+                if (not cache[theme.name][2].has_key(dir) \
+                or cache[theme.name][1] < os.path.getmtime(os.path.join(directory,theme.name))) \
+                and subdir != "" \
+                and os.path.isdir(dir):
+                    cache[theme.name][2][dir] = [subdir, os.listdir(dir)]
+                    cache[theme.name][1] = os.path.getmtime(os.path.join(directory,theme.name))
+
+    for dir, values in cache[theme.name][2].items():
+        if DirectoryMatchesSize(values[0], size, theme):
+            for extension in extensions:
+                if iconname + "." + extension in values[1]:
+                    return os.path.join(dir, iconname + "." + extension)
+
+    minimal_size = sys.maxint
+    closest_filename = ""
+    for dir, values in cache[theme.name][2].items():
+        distance = DirectorySizeDistance(values[0], size, theme)
+        if distance < minimal_size:
+            for extension in extensions:
+                if iconname + "." + extension in values[1]:
+                    closest_filename = os.path.join(dir, iconname + "." + extension)
+                    minimal_size = distance
+
+    return closest_filename
+
+def DirectoryMatchesSize(subdir, iconsize, theme):
+    Type = theme.getType(subdir)
+    Size = theme.getSize(subdir)
+    Threshold = theme.getThreshold(subdir)
+    MinSize = theme.getMinSize(subdir)
+    MaxSize = theme.getMaxSize(subdir)
+    if Type == "Fixed":
+        return Size == iconsize
+    elif Type == "Scaleable":
+        return MinSize <= iconsize <= MaxSize
+    elif Type == "Threshold":
+        return Size - Threshold <= iconsize <= Size + Threshold
+
+def DirectorySizeDistance(subdir, iconsize, theme):
+    Type = theme.getType(subdir)
+    Size = theme.getSize(subdir)
+    Threshold = theme.getThreshold(subdir)
+    MinSize = theme.getMinSize(subdir)
+    MaxSize = theme.getMaxSize(subdir)
+    if Type == "Fixed":
+        return abs(Size - iconsize)
+    elif Type == "Scalable":
+        if iconsize < MinSize:
+            return MinSize - iconsize
+        elif iconsize > MaxSize:
+            return MaxSize - iconsize
+        return 0
+    elif Type == "Threshold":
+        if iconsize < Size - Threshold:
+            return MinSize - iconsize
+        elif iconsize > Size + Threshold:
+            return iconsize - MaxSize
+        return 0
diff --git a/drlaunch/src/xdg/IniFile.py b/drlaunch/src/xdg/IniFile.py
new file mode 100644 (file)
index 0000000..f3f08c7
--- /dev/null
@@ -0,0 +1,406 @@
+"""
+Base Class for DesktopEntry, IconTheme and IconData
+"""
+
+import re, os, stat, codecs
+from Exceptions import *
+import xdg.Locale
+
+class IniFile:
+    defaultGroup = ''
+    fileExtension = ''
+
+    filename = ''
+
+    tainted = False
+
+    def __init__(self, filename=None):
+        self.content = dict()
+        if filename:
+            self.parse(filename)
+
+    def __cmp__(self, other):
+        return cmp(self.content, other.content)
+
+    def parse(self, filename, headers=None):
+        # for performance reasons
+        content = self.content
+
+        if not os.path.isfile(filename):
+            raise ParsingError("File not found", filename)
+
+        try:
+            fd = file(filename, 'r')
+        except IOError, e:
+            if debug:
+                raise e
+            else:
+                return
+
+        # parse file
+        for line in fd:
+            line = line.strip()
+            # empty line
+            if not line:
+                continue
+            # comment
+            elif line[0] == '#':
+                continue
+            # new group
+            elif line[0] == '[':
+                currentGroup = line.lstrip("[").rstrip("]")
+                if debug and self.hasGroup(currentGroup):
+                    raise DuplicateGroupError(currentGroup, filename)
+                else:
+                    content[currentGroup] = {}
+            # key
+            else:
+                index = line.find("=")
+                key = line[0:index].strip()
+                value = line[index+1:].strip()
+                try:
+                    if debug and self.hasKey(key, currentGroup):
+                        raise DuplicateKeyError(key, currentGroup, filename)
+                    else:
+                        content[currentGroup][key] = value
+                except (IndexError, UnboundLocalError):
+                    raise ParsingError("Parsing error on key, group missing", filename)
+
+        fd.close()
+
+        self.filename = filename
+        self.tainted = False
+
+        # check header
+        if headers:
+            for header in headers:
+                if content.has_key(header):
+                    self.defaultGroup = header
+                    break
+            else:
+                raise ParsingError("[%s]-Header missing" % headers[0], filename)
+
+    # start stuff to access the keys
+    def get(self, key, group=None, locale=False, type="string", list=False):
+        # set default group
+        if not group:
+            group = self.defaultGroup
+
+        # return key (with locale)
+        if self.content.has_key(group) and self.content[group].has_key(key):
+            if locale:
+                value = self.content[group][self.__addLocale(key, group)]
+            else:
+                value = self.content[group][key]
+        else:
+            if debug:
+                if not self.content.has_key(group):
+                    raise NoGroupError(group, self.filename)
+                elif not self.content[group].has_key(key):
+                    raise NoKeyError(key, group, self.filename)
+            else:
+                value = ""
+
+        if list == True:
+            values = self.getList(value)
+            result = []
+        else:
+            values = [value]
+
+        for value in values:
+            if type == "string" and locale == True:
+                value = value.decode("utf-8", "ignore")
+            elif type == "boolean":
+                value = self.__getBoolean(value)
+            elif type == "integer":
+                try:
+                    value = int(value)
+                except ValueError:
+                    value = 0
+            elif type == "numeric":
+                try:
+                    value = float(value)
+                except ValueError:
+                    value = 0.0
+            elif type == "regex":
+                value = re.compile(value)
+            elif type == "point":
+                value = value.split(",")
+
+            if list == True:
+                result.append(value)
+            else:
+                result = value
+
+        return result
+    # end stuff to access the keys
+
+    # start subget
+    def getList(self, string):
+        if re.search(r"(?<!\\)\;", string):
+            list = re.split(r"(?<!\\);", string)
+        elif re.search(r"(?<!\\)\|", string):
+            list = re.split(r"(?<!\\)\|", string)
+        elif re.search(r"(?<!\\),", string):
+            list = re.split(r"(?<!\\),", string)
+        else:
+            list = [string]
+        if list[-1] == "":
+            list.pop()
+        return list
+
+    def __getBoolean(self, boolean):
+        if boolean == 1 or boolean == "true" or boolean == "True":
+            return True
+        elif boolean == 0 or boolean == "false" or boolean == "False":
+            return False
+        return False
+    # end subget
+
+    def __addLocale(self, key, group=None):
+        "add locale to key according the current lc_messages"
+        # set default group
+        if not group:
+            group = self.defaultGroup
+
+        for lang in xdg.Locale.langs:
+            if self.content[group].has_key(key+'['+lang+']'):
+                return key+'['+lang+']'
+
+        return key
+
+    # start validation stuff
+    def validate(self, report="All"):
+        "validate ... report = All / Warnings / Errors"
+
+        self.warnings = []
+        self.errors = []
+
+        # get file extension
+        self.fileExtension = os.path.splitext(self.filename)[1]
+
+        # overwrite this for own checkings
+        self.checkExtras()
+
+        # check all keys
+        for group in self.content:
+            self.checkGroup(group)
+            for key in self.content[group]:
+                self.checkKey(key, self.content[group][key], group)
+                # check if value is empty
+                if self.content[group][key] == "":
+                    self.warnings.append("Value of Key '%s' is empty" % key)
+
+        # raise Warnings / Errors
+        msg = ""
+
+        if report == "All" or report == "Warnings":
+            for line in self.warnings:
+                msg += "\n- " + line
+
+        if report == "All" or report == "Errors":
+            for line in self.errors:
+                msg += "\n- " + line
+
+        if msg:
+            raise ValidationError(msg, self.filename)
+
+    # check if group header is valid
+    def checkGroup(self, group):
+        pass
+
+    # check if key is valid
+    def checkKey(self, key, value, group):
+        pass
+
+    # check random stuff
+    def checkValue(self, key, value, type="string", list=False):
+        if list == True:
+            values = self.getList(value)
+        else:
+            values = [value]
+
+        for value in values:
+            if type == "string":
+                code = self.checkString(value)
+            elif type == "boolean":
+                code = self.checkBoolean(value)
+            elif type == "numeric":
+                code = self.checkNumber(value)
+            elif type == "integer":
+                code = self.checkInteger(value)
+            elif type == "regex":
+                code = self.checkRegex(value)
+            elif type == "point":
+                code = self.checkPoint(value)
+            if code == 1:
+                self.errors.append("'%s' is not a valid %s" % (value, type))
+            elif code == 2:
+                self.warnings.append("Value of key '%s' is deprecated" % key)
+
+    def checkExtras(self):
+        pass
+
+    def checkBoolean(self, value):
+        # 1 or 0 : deprecated
+        if (value == "1" or value == "0"):
+            return 2
+        # true or false: ok
+        elif not (value == "true" or value == "false"):
+            return 1
+
+    def checkNumber(self, value):
+        # float() ValueError
+        try:
+            float(value)
+        except:
+            return 1
+
+    def checkInteger(self, value):
+        # int() ValueError
+        try:
+            int(value)
+        except:
+            return 1
+
+    def checkPoint(self, value):
+        if not re.match("^[0-9]+,[0-9]+$", value):
+            return 1
+
+    def checkString(self, value):
+        # convert to ascii
+        if not value.decode("utf-8", "ignore").encode("ascii", 'ignore') == value:
+            return 1
+
+    def checkRegex(self, value):
+        try:
+            re.compile(value)
+        except:
+            return 1
+
+    # write support
+    def write(self, filename=None, trusted=False):
+        if not filename and not self.filename:
+            raise ParsingError("File not found", "")
+
+        if filename:
+            self.filename = filename
+        else:
+            filename = self.filename
+
+        if os.path.dirname(filename) and not os.path.isdir(os.path.dirname(filename)):
+            os.makedirs(os.path.dirname(filename))
+
+        fp = codecs.open(filename, 'w')
+
+        # An executable bit signifies that the desktop file is
+        # trusted, but then the file can be executed. Add hashbang to
+        # make sure that the file is opened by something that
+        # understands desktop files.
+        if trusted:
+            fp.write("#!/usr/bin/env xdg-open\n")
+
+        if self.defaultGroup:
+            fp.write("[%s]\n" % self.defaultGroup)
+            for (key, value) in self.content[self.defaultGroup].items():
+                fp.write("%s=%s\n" % (key, value))
+            fp.write("\n")
+        for (name, group) in self.content.items():
+            if name != self.defaultGroup:
+                fp.write("[%s]\n" % name)
+                for (key, value) in group.items():
+                    fp.write("%s=%s\n" % (key, value))
+                fp.write("\n")
+
+        # Add executable bits to the file to show that it's trusted.
+        if trusted:
+            oldmode = os.stat(filename).st_mode
+            mode = oldmode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH
+            os.chmod(filename, mode)
+
+        self.tainted = False
+
+    def set(self, key, value, group=None, locale=False):
+        # set default group
+        if not group:
+            group = self.defaultGroup
+
+        if locale == True and len(xdg.Locale.langs) > 0:
+            key = key + "[" + xdg.Locale.langs[0] + "]"
+
+        try:
+            if isinstance(value, unicode):
+                self.content[group][key] = value.encode("utf-8", "ignore")
+            else:
+                self.content[group][key] = value
+        except KeyError:
+            raise NoGroupError(group, self.filename)
+            
+        self.tainted = (value == self.get(key, group))
+
+    def addGroup(self, group):
+        if self.hasGroup(group):
+            if debug:
+                raise DuplicateGroupError(group, self.filename)
+            else:
+                pass
+        else:
+            self.content[group] = {}
+            self.tainted = True
+
+    def removeGroup(self, group):
+        existed = group in self.content
+        if existed:
+            del self.content[group]
+            self.tainted = True
+        else:
+            if debug:
+                raise NoGroupError(group, self.filename)
+        return existed
+
+    def removeKey(self, key, group=None, locales=True):
+        # set default group
+        if not group:
+            group = self.defaultGroup
+
+        try:
+            if locales:
+                for (name, value) in self.content[group].items():
+                    if re.match("^" + key + xdg.Locale.regex + "$", name) and name != key:
+                        value = self.content[group][name]
+                        del self.content[group][name]
+            value = self.content[group][key]
+            del self.content[group][key]
+            self.tainted = True
+            return value
+        except KeyError, e:
+            if debug:
+                if e == group:
+                    raise NoGroupError(group, self.filename)
+                else:
+                    raise NoKeyError(key, group, self.filename)
+            else:
+                return ""
+
+    # misc
+    def groups(self):
+        return self.content.keys()
+
+    def hasGroup(self, group):
+        if self.content.has_key(group):
+            return True
+        else:
+            return False
+
+    def hasKey(self, key, group=None):
+        # set default group
+        if not group:
+            group = self.defaultGroup
+
+        if self.content[group].has_key(key):
+            return True
+        else:
+            return False
+
+    def getFileName(self):
+        return self.filename
diff --git a/drlaunch/src/xdg/Locale.py b/drlaunch/src/xdg/Locale.py
new file mode 100644 (file)
index 0000000..d30d91a
--- /dev/null
@@ -0,0 +1,79 @@
+"""
+Helper Module for Locale settings
+
+This module is based on a ROX module (LGPL):
+
+http://cvs.sourceforge.net/viewcvs.py/rox/ROX-Lib2/python/rox/i18n.py?rev=1.3&view=log
+"""
+
+import os
+from locale import normalize
+
+regex = "(\[([a-zA-Z]+)(_[a-zA-Z]+)?(\.[a-zA-Z\-0-9]+)?(@[a-zA-Z]+)?\])?"
+
+def _expand_lang(locale):
+    locale = normalize(locale)
+    COMPONENT_CODESET   = 1 << 0
+    COMPONENT_MODIFIER  = 1 << 1
+    COMPONENT_TERRITORY = 1 << 2
+    # split up the locale into its base components
+    mask = 0
+    pos = locale.find('@')
+    if pos >= 0:
+        modifier = locale[pos:]
+        locale = locale[:pos]
+        mask |= COMPONENT_MODIFIER
+    else:
+        modifier = ''
+    pos = locale.find('.')
+    codeset = ''
+    if pos >= 0:
+        locale = locale[:pos]
+    pos = locale.find('_')
+    if pos >= 0:
+        territory = locale[pos:]
+        locale = locale[:pos]
+        mask |= COMPONENT_TERRITORY
+    else:
+        territory = ''
+    language = locale
+    ret = []
+    for i in range(mask+1):
+        if not (i & ~mask):  # if all components for this combo exist ...
+            val = language
+            if i & COMPONENT_TERRITORY: val += territory
+            if i & COMPONENT_CODESET:   val += codeset
+            if i & COMPONENT_MODIFIER:  val += modifier
+            ret.append(val)
+    ret.reverse()
+    return ret
+
+def expand_languages(languages=None):
+    # Get some reasonable defaults for arguments that were not supplied
+    if languages is None:
+        languages = []
+        for envar in ('LANGUAGE', 'LC_ALL', 'LC_MESSAGES', 'LANG'):
+            val = os.environ.get(envar)
+            if val:
+                languages = val.split(':')
+                break
+    #if 'C' not in languages:
+    #   languages.append('C')
+
+    # now normalize and expand the languages
+    nelangs = []
+    for lang in languages:
+        for nelang in _expand_lang(lang):
+            if nelang not in nelangs:
+                nelangs.append(nelang)
+    return nelangs
+
+def update(language=None):
+    global langs
+    if language:
+        langs = expand_languages([language])
+    else:
+        langs = expand_languages()
+
+langs = []
+update()
diff --git a/drlaunch/src/xdg/Menu.py b/drlaunch/src/xdg/Menu.py
new file mode 100644 (file)
index 0000000..d437ee4
--- /dev/null
@@ -0,0 +1,1074 @@
+"""
+Implementation of the XDG Menu Specification Version 1.0.draft-1
+http://standards.freedesktop.org/menu-spec/
+"""
+
+from __future__ import generators
+import locale, os, xml.dom.minidom
+
+from xdg.BaseDirectory import *
+from xdg.DesktopEntry import *
+from xdg.Exceptions import *
+
+import xdg.Locale
+import xdg.Config
+
+ELEMENT_NODE = xml.dom.Node.ELEMENT_NODE
+
+# for python <= 2.3
+try:
+    reversed = reversed
+except NameError:
+    def reversed(x):
+        return x[::-1]
+
+class Menu:
+    def __init__(self):
+        # Public stuff
+        self.Name = ""
+        self.Directory = None
+        self.Entries = []
+        self.Doc = ""
+        self.Filename = ""
+        self.Depth = 0
+        self.Parent = None
+        self.NotInXml = False
+
+        # Can be one of Deleted/NoDisplay/Hidden/Empty/NotShowIn or True
+        self.Show = True
+        self.Visible = 0
+
+        # Private stuff, only needed for parsing
+        self.AppDirs = []
+        self.DefaultLayout = None
+        self.Deleted = "notset"
+        self.Directories = []
+        self.DirectoryDirs = []
+        self.Layout = None
+        self.MenuEntries = []
+        self.Moves = []
+        self.OnlyUnallocated = "notset"
+        self.Rules = []
+        self.Submenus = []
+
+    def __str__(self):
+        return self.Name
+
+    def __add__(self, other):
+        for dir in other.AppDirs:
+            self.AppDirs.append(dir)
+
+        for dir in other.DirectoryDirs:
+            self.DirectoryDirs.append(dir)
+
+        for directory in other.Directories:
+            self.Directories.append(directory)
+
+        if other.Deleted != "notset":
+            self.Deleted = other.Deleted
+
+        if other.OnlyUnallocated != "notset":
+            self.OnlyUnallocated = other.OnlyUnallocated
+
+        if other.Layout:
+            self.Layout = other.Layout
+
+        if other.DefaultLayout:
+            self.DefaultLayout = other.DefaultLayout
+
+        for rule in other.Rules:
+            self.Rules.append(rule)
+
+        for move in other.Moves:
+            self.Moves.append(move)
+
+        for submenu in other.Submenus:
+            self.addSubmenu(submenu)
+
+        return self
+
+    # FIXME: Performance: cache getName()
+    def __cmp__(self, other):
+        return locale.strcoll(self.getName(), other.getName())
+
+    def __eq__(self, other):
+        if self.Name == str(other):
+            return True
+        else:
+            return False
+
+    """ PUBLIC STUFF """
+    def getEntries(self, hidden=False):
+        for entry in self.Entries:
+            if hidden == True:
+                yield entry
+            elif entry.Show == True:
+                yield entry
+
+    # FIXME: Add searchEntry/seaqrchMenu function
+    # search for name/comment/genericname/desktopfileide
+    # return multiple items
+
+    def getMenuEntry(self, desktopfileid, deep = False):
+        for menuentry in self.MenuEntries:
+            if menuentry.DesktopFileID == desktopfileid:
+                return menuentry
+        if deep == True:
+            for submenu in self.Submenus:
+                submenu.getMenuEntry(desktopfileid, deep)
+
+    def getMenu(self, path):
+        array = path.split("/", 1)
+        for submenu in self.Submenus:
+            if submenu.Name == array[0]:
+                if len(array) > 1:
+                    return submenu.getMenu(array[1])
+                else:
+                    return submenu
+
+    def getPath(self, org=False, toplevel=False):
+        parent = self
+        names=[]
+        while 1:
+            if org:
+                names.append(parent.Name)
+            else:
+                names.append(parent.getName())
+            if parent.Depth > 0:
+                parent = parent.Parent
+            else:
+                break
+        names.reverse()
+        path = ""
+        if toplevel == False:
+            names.pop(0)
+        for name in names:
+            path = os.path.join(path, name)
+        return path
+
+    def getName(self):
+        try:
+            return self.Directory.DesktopEntry.getName()
+        except AttributeError:
+            return self.Name
+
+    def getGenericName(self):
+        try:
+            return self.Directory.DesktopEntry.getGenericName()
+        except AttributeError:
+            return ""
+
+    def getComment(self):
+        try:
+            return self.Directory.DesktopEntry.getComment()
+        except AttributeError:
+            return ""
+
+    def getIcon(self):
+        try:
+            return self.Directory.DesktopEntry.getIcon()
+        except AttributeError:
+            return ""
+
+    """ PRIVATE STUFF """
+    def addSubmenu(self, newmenu):
+        for submenu in self.Submenus:
+            if submenu == newmenu:
+                submenu += newmenu
+                break
+        else:
+            self.Submenus.append(newmenu)
+            newmenu.Parent = self
+            newmenu.Depth = self.Depth + 1
+
+class Move:
+    "A move operation"
+    def __init__(self, node=None):
+        if node:
+            self.parseNode(node)
+        else:
+            self.Old = ""
+            self.New = ""
+
+    def __cmp__(self, other):
+        return cmp(self.Old, other.Old)
+
+    def parseNode(self, node):
+        for child in node.childNodes:
+            if child.nodeType == ELEMENT_NODE:
+                if child.tagName == "Old":
+                    try:
+                        self.parseOld(child.childNodes[0].nodeValue)
+                    except IndexError:
+                        raise ValidationError('Old cannot be empty', '??')                                            
+                elif child.tagName == "New":
+                    try:
+                        self.parseNew(child.childNodes[0].nodeValue)
+                    except IndexError:
+                        raise ValidationError('New cannot be empty', '??')                                            
+
+    def parseOld(self, value):
+        self.Old = value
+    def parseNew(self, value):
+        self.New = value
+
+
+class Layout:
+    "Menu Layout class"
+    def __init__(self, node=None):
+        self.order = []
+        if node:
+            self.show_empty = node.getAttribute("show_empty") or "false"
+            self.inline = node.getAttribute("inline") or "false"
+            self.inline_limit = node.getAttribute("inline_limit") or 4
+            self.inline_header = node.getAttribute("inline_header") or "true"
+            self.inline_alias = node.getAttribute("inline_alias") or "false"
+            self.inline_limit = int(self.inline_limit)
+            self.parseNode(node)
+        else:
+            self.show_empty = "false"
+            self.inline = "false"
+            self.inline_limit = 4
+            self.inline_header = "true"
+            self.inline_alias = "false"
+            self.order.append(["Merge", "menus"])
+            self.order.append(["Merge", "files"])
+
+    def parseNode(self, node):
+        for child in node.childNodes:
+            if child.nodeType == ELEMENT_NODE:
+                if child.tagName == "Menuname":
+                    try:
+                        self.parseMenuname(
+                            child.childNodes[0].nodeValue,
+                            child.getAttribute("show_empty") or "false",
+                            child.getAttribute("inline") or "false",
+                            child.getAttribute("inline_limit") or 4,
+                            child.getAttribute("inline_header") or "true",
+                            child.getAttribute("inline_alias") or "false" )
+                    except IndexError:
+                        raise ValidationError('Menuname cannot be empty', "")
+                elif child.tagName == "Separator":
+                    self.parseSeparator()
+                elif child.tagName == "Filename":
+                    try:
+                        self.parseFilename(child.childNodes[0].nodeValue)
+                    except IndexError:
+                        raise ValidationError('Filename cannot be empty', "")
+                elif child.tagName == "Merge":
+                    self.parseMerge(child.getAttribute("type") or "all")
+
+    def parseMenuname(self, value, empty="false", inline="false", inline_limit=4, inline_header="true", inline_alias="false"):
+        self.order.append(["Menuname", value, empty, inline, inline_limit, inline_header, inline_alias])
+        self.order[-1][4] = int(self.order[-1][4])
+
+    def parseSeparator(self):
+        self.order.append(["Separator"])
+
+    def parseFilename(self, value):
+        self.order.append(["Filename", value])
+
+    def parseMerge(self, type="all"):
+        self.order.append(["Merge", type])
+
+
+class Rule:
+    "Inlcude / Exclude Rules Class"
+    def __init__(self, type, node=None):
+        # Type is Include or Exclude
+        self.Type = type
+        # Rule is a python expression
+        self.Rule = ""
+
+        # Private attributes, only needed for parsing
+        self.Depth = 0
+        self.Expr = [ "or" ]
+        self.New = True
+
+        # Begin parsing
+        if node:
+            self.parseNode(node)
+            self.compile()
+
+    def __str__(self):
+        return self.Rule
+
+    def compile(self):
+        exec("""
+def do(menuentries, type, run):
+    for menuentry in menuentries:
+        if run == 2 and ( menuentry.MatchedInclude == True \
+        or menuentry.Allocated == True ):
+            continue
+        elif %s:
+            if type == "Include":
+                menuentry.Add = True
+                menuentry.MatchedInclude = True
+            else:
+                menuentry.Add = False
+    return menuentries
+""" % self.Rule) in self.__dict__
+
+    def parseNode(self, node):
+        for child in node.childNodes:
+            if child.nodeType == ELEMENT_NODE:
+                if child.tagName == 'Filename':
+                    try:
+                        self.parseFilename(child.childNodes[0].nodeValue)
+                    except IndexError:
+                        raise ValidationError('Filename cannot be empty', "???")
+                elif child.tagName == 'Category':
+                    try:
+                        self.parseCategory(child.childNodes[0].nodeValue)
+                    except IndexError:
+                        raise ValidationError('Category cannot be empty', "???")
+                elif child.tagName == 'All':
+                    self.parseAll()
+                elif child.tagName == 'And':
+                    self.parseAnd(child)
+                elif child.tagName == 'Or':
+                    self.parseOr(child)
+                elif child.tagName == 'Not':
+                    self.parseNot(child)
+
+    def parseNew(self, set=True):
+        if not self.New:
+            self.Rule += " " + self.Expr[self.Depth] + " "
+        if not set:
+            self.New = True
+        elif set:
+            self.New = False
+
+    def parseFilename(self, value):
+        self.parseNew()
+        self.Rule += "menuentry.DesktopFileID == '%s'" % value.strip().replace("\\", r"\\").replace("'", r"\'")
+
+    def parseCategory(self, value):
+        self.parseNew()
+        self.Rule += "'%s' in menuentry.Categories" % value.strip()
+
+    def parseAll(self):
+        self.parseNew()
+        self.Rule += "True"
+
+    def parseAnd(self, node):
+        self.parseNew(False)
+        self.Rule += "("
+        self.Depth += 1
+        self.Expr.append("and")
+        self.parseNode(node)
+        self.Depth -= 1
+        self.Expr.pop()
+        self.Rule += ")"
+
+    def parseOr(self, node):
+        self.parseNew(False)
+        self.Rule += "("
+        self.Depth += 1
+        self.Expr.append("or")
+        self.parseNode(node)
+        self.Depth -= 1
+        self.Expr.pop()
+        self.Rule += ")"
+
+    def parseNot(self, node):
+        self.parseNew(False)
+        self.Rule += "not ("
+        self.Depth += 1
+        self.Expr.append("or")
+        self.parseNode(node)
+        self.Depth -= 1
+        self.Expr.pop()
+        self.Rule += ")"
+
+
+class MenuEntry:
+    "Wrapper for 'Menu Style' Desktop Entries"
+    def __init__(self, filename, dir="", prefix=""):
+        # Create entry
+        self.DesktopEntry = DesktopEntry(os.path.join(dir,filename))
+        self.setAttributes(filename, dir, prefix)
+
+        # Can be one of Deleted/Hidden/Empty/NotShowIn/NoExec or True
+        self.Show = True
+
+        # Semi-Private
+        self.Original = None
+        self.Parents = []
+
+        # Private Stuff
+        self.Allocated = False
+        self.Add = False
+        self.MatchedInclude = False
+
+        # Caching
+        self.Categories = self.DesktopEntry.getCategories()
+
+    def save(self):
+        if self.DesktopEntry.tainted == True:
+            self.DesktopEntry.write()
+
+    def getDir(self):
+        return self.DesktopEntry.filename.replace(self.Filename, '')
+
+    def getType(self):
+        # Can be one of System/User/Both
+        if xdg.Config.root_mode == False:
+            if self.Original:
+                return "Both"
+            elif xdg_data_dirs[0] in self.DesktopEntry.filename:
+                return "User"
+            else:
+                return "System"
+        else:
+            return "User"
+
+    def setAttributes(self, filename, dir="", prefix=""):
+        self.Filename = filename
+        self.Prefix = prefix
+        self.DesktopFileID = os.path.join(prefix,filename).replace("/", "-")
+
+        if not os.path.isabs(self.DesktopEntry.filename):
+            self.__setFilename()
+
+    def updateAttributes(self):
+        if self.getType() == "System":
+            self.Original = MenuEntry(self.Filename, self.getDir(), self.Prefix)
+            self.__setFilename()
+
+    def __setFilename(self):
+        if xdg.Config.root_mode == False:
+            path = xdg_data_dirs[0]
+        else:
+            path= xdg_data_dirs[1]
+
+        if self.DesktopEntry.getType() == "Application":
+            dir = os.path.join(path, "applications")
+        else:
+            dir = os.path.join(path, "desktop-directories")
+
+        self.DesktopEntry.filename = os.path.join(dir, self.Filename)
+
+    def __cmp__(self, other):
+        return locale.strcoll(self.DesktopEntry.getName(), other.DesktopEntry.getName())
+
+    def __eq__(self, other):
+        if self.DesktopFileID == str(other):
+            return True
+        else:
+            return False
+
+    def __repr__(self):
+        return self.DesktopFileID
+
+
+class Separator:
+    "Just a dummy class for Separators"
+    def __init__(self, parent):
+        self.Parent = parent
+        self.Show = True
+
+
+class Header:
+    "Class for Inline Headers"
+    def __init__(self, name, generic_name, comment):
+        self.Name = name
+        self.GenericName = generic_name
+        self.Comment = comment
+
+    def __str__(self):
+        return self.Name
+
+
+tmp = {}
+
+def __getFileName(filename):
+    dirs = xdg_config_dirs[:]
+    if xdg.Config.root_mode == True:
+        dirs.pop(0)
+
+    for dir in dirs:
+        menuname = os.path.join (dir, "menus" , filename)
+        if os.path.isdir(dir) and os.path.isfile(menuname):
+            return menuname
+
+def parse(filename=None):
+    # conver to absolute path
+    if filename and not os.path.isabs(filename):
+        filename = __getFileName(filename)
+
+    # use default if no filename given
+    if not filename: 
+        candidate = os.environ.get('XDG_MENU_PREFIX', '') + "applications.menu"
+        filename = __getFileName(candidate)
+        
+    if not filename:
+        raise ParsingError('File not found', "/etc/xdg/menus/%s" % candidate)
+
+    # check if it is a .menu file
+    if not os.path.splitext(filename)[1] == ".menu":
+        raise ParsingError('Not a .menu file', filename)
+
+    # create xml parser
+    try:
+        doc = xml.dom.minidom.parse(filename)
+    except xml.parsers.expat.ExpatError:
+        raise ParsingError('Not a valid .menu file', filename)
+
+    # parse menufile
+    tmp["Root"] = ""
+    tmp["mergeFiles"] = []
+    tmp["DirectoryDirs"] = []
+    tmp["cache"] = MenuEntryCache()
+
+    __parse(doc, filename, tmp["Root"])
+    __parsemove(tmp["Root"])
+    __postparse(tmp["Root"])
+
+    tmp["Root"].Doc = doc
+    tmp["Root"].Filename = filename
+
+    # generate the menu
+    __genmenuNotOnlyAllocated(tmp["Root"])
+    __genmenuOnlyAllocated(tmp["Root"])
+
+    # and finally sort
+    sort(tmp["Root"])
+
+    return tmp["Root"]
+
+
+def __parse(node, filename, parent=None):
+    for child in node.childNodes:
+        if child.nodeType == ELEMENT_NODE:
+            if child.tagName == 'Menu':
+                __parseMenu(child, filename, parent)
+            elif child.tagName == 'AppDir':
+                try:
+                    __parseAppDir(child.childNodes[0].nodeValue, filename, parent)
+                except IndexError:
+                    raise ValidationError('AppDir cannot be empty', filename)
+            elif child.tagName == 'DefaultAppDirs':
+                __parseDefaultAppDir(filename, parent)
+            elif child.tagName == 'DirectoryDir':
+                try:
+                    __parseDirectoryDir(child.childNodes[0].nodeValue, filename, parent)
+                except IndexError:
+                    raise ValidationError('DirectoryDir cannot be empty', filename)
+            elif child.tagName == 'DefaultDirectoryDirs':
+                __parseDefaultDirectoryDir(filename, parent)
+            elif child.tagName == 'Name' :
+                try:
+                    parent.Name = child.childNodes[0].nodeValue
+                except IndexError:
+                    raise ValidationError('Name cannot be empty', filename)
+            elif child.tagName == 'Directory' :
+                try:
+                    parent.Directories.append(child.childNodes[0].nodeValue)
+                except IndexError:
+                    raise ValidationError('Directory cannot be empty', filename)
+            elif child.tagName == 'OnlyUnallocated':
+                parent.OnlyUnallocated = True
+            elif child.tagName == 'NotOnlyUnallocated':
+                parent.OnlyUnallocated = False
+            elif child.tagName == 'Deleted':
+                parent.Deleted = True
+            elif child.tagName == 'NotDeleted':
+                parent.Deleted = False
+            elif child.tagName == 'Include' or child.tagName == 'Exclude':
+                parent.Rules.append(Rule(child.tagName, child))
+            elif child.tagName == 'MergeFile':
+                try:
+                    if child.getAttribute("type") == "parent":
+                        __parseMergeFile("applications.menu", child, filename, parent)
+                    else:
+                        __parseMergeFile(child.childNodes[0].nodeValue, child, filename, parent)
+                except IndexError:
+                    raise ValidationError('MergeFile cannot be empty', filename)
+            elif child.tagName == 'MergeDir':
+                try:
+                    __parseMergeDir(child.childNodes[0].nodeValue, child, filename, parent)
+                except IndexError:
+                    raise ValidationError('MergeDir cannot be empty', filename)
+            elif child.tagName == 'DefaultMergeDirs':
+                __parseDefaultMergeDirs(child, filename, parent)
+            elif child.tagName == 'Move':
+                parent.Moves.append(Move(child))
+            elif child.tagName == 'Layout':
+                if len(child.childNodes) > 1:
+                    parent.Layout = Layout(child)
+            elif child.tagName == 'DefaultLayout':
+                if len(child.childNodes) > 1:
+                    parent.DefaultLayout = Layout(child)
+            elif child.tagName == 'LegacyDir':
+                try:
+                    __parseLegacyDir(child.childNodes[0].nodeValue, child.getAttribute("prefix"), filename, parent)
+                except IndexError:
+                    raise ValidationError('LegacyDir cannot be empty', filename)
+            elif child.tagName == 'KDELegacyDirs':
+                __parseKDELegacyDirs(filename, parent)
+
+def __parsemove(menu):
+    for submenu in menu.Submenus:
+        __parsemove(submenu)
+
+    # parse move operations
+    for move in menu.Moves:
+        move_from_menu = menu.getMenu(move.Old)
+        if move_from_menu:
+            move_to_menu = menu.getMenu(move.New)
+
+            menus = move.New.split("/")
+            oldparent = None
+            while len(menus) > 0:
+                if not oldparent:
+                    oldparent = menu
+                newmenu = oldparent.getMenu(menus[0])
+                if not newmenu:
+                    newmenu = Menu()
+                    newmenu.Name = menus[0]
+                    if len(menus) > 1:
+                        newmenu.NotInXml = True
+                    oldparent.addSubmenu(newmenu)
+                oldparent = newmenu
+                menus.pop(0)
+
+            newmenu += move_from_menu
+            move_from_menu.Parent.Submenus.remove(move_from_menu)
+
+def __postparse(menu):
+    # unallocated / deleted
+    if menu.Deleted == "notset":
+        menu.Deleted = False
+    if menu.OnlyUnallocated == "notset":
+        menu.OnlyUnallocated = False
+
+    # Layout Tags
+    if not menu.Layout or not menu.DefaultLayout:
+        if menu.DefaultLayout:
+            menu.Layout = menu.DefaultLayout
+        elif menu.Layout:
+            if menu.Depth > 0:
+                menu.DefaultLayout = menu.Parent.DefaultLayout
+            else:
+                menu.DefaultLayout = Layout()
+        else:
+            if menu.Depth > 0:
+                menu.Layout = menu.Parent.DefaultLayout
+                menu.DefaultLayout = menu.Parent.DefaultLayout
+            else:
+                menu.Layout = Layout()
+                menu.DefaultLayout = Layout()
+
+    # add parent's app/directory dirs
+    if menu.Depth > 0:
+        menu.AppDirs = menu.Parent.AppDirs + menu.AppDirs
+        menu.DirectoryDirs = menu.Parent.DirectoryDirs + menu.DirectoryDirs
+
+    # remove duplicates
+    menu.Directories = __removeDuplicates(menu.Directories)
+    menu.DirectoryDirs = __removeDuplicates(menu.DirectoryDirs)
+    menu.AppDirs = __removeDuplicates(menu.AppDirs)
+
+    # go recursive through all menus
+    for submenu in menu.Submenus:
+        __postparse(submenu)
+
+    # reverse so handling is easier
+    menu.Directories.reverse()
+    menu.DirectoryDirs.reverse()
+    menu.AppDirs.reverse()
+
+    # get the valid .directory file out of the list
+    for directory in menu.Directories:
+        for dir in menu.DirectoryDirs:
+            if os.path.isfile(os.path.join(dir, directory)):
+                menuentry = MenuEntry(directory, dir)
+                if not menu.Directory:
+                    menu.Directory = menuentry
+                elif menuentry.getType() == "System":
+                    if menu.Directory.getType() == "User":
+                        menu.Directory.Original = menuentry
+        if menu.Directory:
+            break
+
+
+# Menu parsing stuff
+def __parseMenu(child, filename, parent):
+    m = Menu()
+    __parse(child, filename, m)
+    if parent:
+        parent.addSubmenu(m)
+    else:
+        tmp["Root"] = m
+
+# helper function
+def __check(value, filename, type):
+    path = os.path.dirname(filename)
+
+    if not os.path.isabs(value):
+        value = os.path.join(path, value)
+
+    value = os.path.abspath(value)
+
+    if type == "dir" and os.path.exists(value) and os.path.isdir(value):
+        return value
+    elif type == "file" and os.path.exists(value) and os.path.isfile(value):
+        return value
+    else:
+        return False
+
+# App/Directory Dir Stuff
+def __parseAppDir(value, filename, parent):
+    value = __check(value, filename, "dir")
+    if value:
+        parent.AppDirs.append(value)
+
+def __parseDefaultAppDir(filename, parent):
+    for dir in reversed(xdg_data_dirs):
+        __parseAppDir(os.path.join(dir, "applications"), filename, parent)
+
+def __parseDirectoryDir(value, filename, parent):
+    value = __check(value, filename, "dir")
+    if value:
+        parent.DirectoryDirs.append(value)
+
+def __parseDefaultDirectoryDir(filename, parent):
+    for dir in reversed(xdg_data_dirs):
+        __parseDirectoryDir(os.path.join(dir, "desktop-directories"), filename, parent)
+
+# Merge Stuff
+def __parseMergeFile(value, child, filename, parent):
+    if child.getAttribute("type") == "parent":
+        for dir in xdg_config_dirs:
+            rel_file = filename.replace(dir, "").strip("/")
+            if rel_file != filename:
+                for p in xdg_config_dirs:
+                    if dir == p:
+                        continue
+                    if os.path.isfile(os.path.join(p,rel_file)):
+                        __mergeFile(os.path.join(p,rel_file),child,parent)
+                        break
+    else:
+        value = __check(value, filename, "file")
+        if value:
+            __mergeFile(value, child, parent)
+
+def __parseMergeDir(value, child, filename, parent):
+    value = __check(value, filename, "dir")
+    if value:
+        for item in os.listdir(value):
+            try:
+                if os.path.splitext(item)[1] == ".menu":
+                    __mergeFile(os.path.join(value, item), child, parent)
+            except UnicodeDecodeError:
+                continue
+
+def __parseDefaultMergeDirs(child, filename, parent):
+    basename = os.path.splitext(os.path.basename(filename))[0]
+    for dir in reversed(xdg_config_dirs):
+        __parseMergeDir(os.path.join(dir, "menus", basename + "-merged"), child, filename, parent)
+
+def __mergeFile(filename, child, parent):
+    # check for infinite loops
+    if filename in tmp["mergeFiles"]:
+        if debug:
+            raise ParsingError('Infinite MergeFile loop detected', filename)
+        else:
+            return
+
+    tmp["mergeFiles"].append(filename)
+
+    # load file
+    try:
+        doc = xml.dom.minidom.parse(filename)
+    except IOError:
+        if debug:
+            raise ParsingError('File not found', filename)
+        else:
+            return
+    except xml.parsers.expat.ExpatError:
+        if debug:
+            raise ParsingError('Not a valid .menu file', filename)
+        else:
+            return
+
+    # append file
+    for child in doc.childNodes:
+        if child.nodeType == ELEMENT_NODE:
+            __parse(child,filename,parent)
+            break
+
+# Legacy Dir Stuff
+def __parseLegacyDir(dir, prefix, filename, parent):
+    m = __mergeLegacyDir(dir,prefix,filename,parent)
+    if m:
+        parent += m
+
+def __mergeLegacyDir(dir, prefix, filename, parent):
+    dir = __check(dir,filename,"dir")
+    if dir and dir not in tmp["DirectoryDirs"]:
+        tmp["DirectoryDirs"].append(dir)
+
+        m = Menu()
+        m.AppDirs.append(dir)
+        m.DirectoryDirs.append(dir)
+        m.Name = os.path.basename(dir)
+        m.NotInXml = True
+
+        for item in os.listdir(dir):
+            try:
+                if item == ".directory":
+                    m.Directories.append(item)
+                elif os.path.isdir(os.path.join(dir,item)):
+                    m.addSubmenu(__mergeLegacyDir(os.path.join(dir,item), prefix, filename, parent))
+            except UnicodeDecodeError:
+                continue
+
+        tmp["cache"].addMenuEntries([dir],prefix, True)
+        menuentries = tmp["cache"].getMenuEntries([dir], False)
+
+        for menuentry in menuentries:
+            categories = menuentry.Categories
+            if len(categories) == 0:
+                r = Rule("Include")
+                r.parseFilename(menuentry.DesktopFileID)
+                r.compile()
+                m.Rules.append(r)
+            if not dir in parent.AppDirs:
+                categories.append("Legacy")
+                menuentry.Categories = categories
+
+        return m
+
+def __parseKDELegacyDirs(filename, parent):
+    f=os.popen3("kde-config --path apps")
+    output = f[1].readlines()
+    try:
+        for dir in output[0].split(":"):
+            __parseLegacyDir(dir,"kde", filename, parent)
+    except IndexError:
+        pass
+
+# remove duplicate entries from a list
+def __removeDuplicates(list):
+    set = {}
+    list.reverse()
+    list = [set.setdefault(e,e) for e in list if e not in set]
+    list.reverse()
+    return list
+
+# Finally generate the menu
+def __genmenuNotOnlyAllocated(menu):
+    for submenu in menu.Submenus:
+        __genmenuNotOnlyAllocated(submenu)
+
+    if menu.OnlyUnallocated == False:
+        tmp["cache"].addMenuEntries(menu.AppDirs)
+        menuentries = []
+        for rule in menu.Rules:
+            menuentries = rule.do(tmp["cache"].getMenuEntries(menu.AppDirs), rule.Type, 1)
+        for menuentry in menuentries:
+            if menuentry.Add == True:
+                menuentry.Parents.append(menu)
+                menuentry.Add = False
+                menuentry.Allocated = True
+                menu.MenuEntries.append(menuentry)
+
+def __genmenuOnlyAllocated(menu):
+    for submenu in menu.Submenus:
+        __genmenuOnlyAllocated(submenu)
+
+    if menu.OnlyUnallocated == True:
+        tmp["cache"].addMenuEntries(menu.AppDirs)
+        menuentries = []
+        for rule in menu.Rules:
+            menuentries = rule.do(tmp["cache"].getMenuEntries(menu.AppDirs), rule.Type, 2)
+        for menuentry in menuentries:
+            if menuentry.Add == True:
+                menuentry.Parents.append(menu)
+            #   menuentry.Add = False
+            #   menuentry.Allocated = True
+                menu.MenuEntries.append(menuentry)
+
+# And sorting ...
+def sort(menu):
+    menu.Entries = []
+    menu.Visible = 0
+
+    for submenu in menu.Submenus:
+        sort(submenu)
+
+    tmp_s = []
+    tmp_e = []
+
+    for order in menu.Layout.order:
+        if order[0] == "Filename":
+            tmp_e.append(order[1])
+        elif order[0] == "Menuname":
+            tmp_s.append(order[1])
+    
+    for order in menu.Layout.order:
+        if order[0] == "Separator":
+            separator = Separator(menu)
+            if len(menu.Entries) > 0 and isinstance(menu.Entries[-1], Separator):
+                separator.Show = False
+            menu.Entries.append(separator)
+        elif order[0] == "Filename":
+            menuentry = menu.getMenuEntry(order[1])
+            if menuentry:
+                menu.Entries.append(menuentry)
+        elif order[0] == "Menuname":
+            submenu = menu.getMenu(order[1])
+            if submenu:
+                __parse_inline(submenu, menu)
+        elif order[0] == "Merge":
+            if order[1] == "files" or order[1] == "all":
+                menu.MenuEntries.sort()
+                for menuentry in menu.MenuEntries:
+                    if menuentry not in tmp_e:
+                        menu.Entries.append(menuentry)
+            elif order[1] == "menus" or order[1] == "all":
+                menu.Submenus.sort()
+                for submenu in menu.Submenus:
+                    if submenu.Name not in tmp_s:
+                        __parse_inline(submenu, menu)
+
+    # getHidden / NoDisplay / OnlyShowIn / NotOnlyShowIn / Deleted / NoExec
+    for entry in menu.Entries:
+        entry.Show = True
+        menu.Visible += 1
+        if isinstance(entry, Menu):
+            if entry.Deleted == True:
+                entry.Show = "Deleted"
+                menu.Visible -= 1
+            elif isinstance(entry.Directory, MenuEntry):
+                if entry.Directory.DesktopEntry.getNoDisplay() == True:
+                    entry.Show = "NoDisplay"
+                    menu.Visible -= 1
+                elif entry.Directory.DesktopEntry.getHidden() == True:
+                    entry.Show = "Hidden"
+                    menu.Visible -= 1
+        elif isinstance(entry, MenuEntry):
+            if entry.DesktopEntry.getNoDisplay() == True:
+                entry.Show = "NoDisplay"
+                menu.Visible -= 1
+            elif entry.DesktopEntry.getHidden() == True:
+                entry.Show = "Hidden"
+                menu.Visible -= 1
+            elif entry.DesktopEntry.getTryExec() and not __try_exec(entry.DesktopEntry.getTryExec()):
+                entry.Show = "NoExec"
+                menu.Visible -= 1
+            elif xdg.Config.windowmanager:
+                if ( entry.DesktopEntry.getOnlyShowIn() != [] and xdg.Config.windowmanager not in entry.DesktopEntry.getOnlyShowIn() ) \
+                or xdg.Config.windowmanager in entry.DesktopEntry.getNotShowIn():
+                    entry.Show = "NotShowIn"
+                    menu.Visible -= 1
+        elif isinstance(entry,Separator):
+            menu.Visible -= 1
+
+    # remove separators at the beginning and at the end
+    if len(menu.Entries) > 0:
+        if isinstance(menu.Entries[0], Separator):
+            menu.Entries[0].Show = False
+    if len(menu.Entries) > 1:
+        if isinstance(menu.Entries[-1], Separator):
+            menu.Entries[-1].Show = False
+
+    # show_empty tag
+    for entry in menu.Entries:
+        if isinstance(entry,Menu) and entry.Layout.show_empty == "false" and entry.Visible == 0:
+            entry.Show = "Empty"
+            menu.Visible -= 1
+            if entry.NotInXml == True:
+                menu.Entries.remove(entry)
+
+def __try_exec(executable):
+    paths = os.environ['PATH'].split(os.pathsep)
+    if not os.path.isfile(executable):
+        for p in paths:
+            f = os.path.join(p, executable)
+            if os.path.isfile(f):
+                if os.access(f, os.X_OK):
+                    return True
+    else:
+        if os.access(executable, os.X_OK):
+            return True
+    return False
+
+# inline tags
+def __parse_inline(submenu, menu):
+    if submenu.Layout.inline == "true":
+        if len(submenu.Entries) == 1 and submenu.Layout.inline_alias == "true":
+            menuentry = submenu.Entries[0]
+            menuentry.DesktopEntry.set("Name", submenu.getName(), locale = True)
+            menuentry.DesktopEntry.set("GenericName", submenu.getGenericName(), locale = True)
+            menuentry.DesktopEntry.set("Comment", submenu.getComment(), locale = True)
+            menu.Entries.append(menuentry)
+        elif len(submenu.Entries) <= submenu.Layout.inline_limit or submenu.Layout.inline_limit == 0:
+            if submenu.Layout.inline_header == "true":
+                header = Header(submenu.getName(), submenu.getGenericName(), submenu.getComment())
+                menu.Entries.append(header)
+            for entry in submenu.Entries:
+                menu.Entries.append(entry)
+        else:
+            menu.Entries.append(submenu)
+    else:
+        menu.Entries.append(submenu)
+
+class MenuEntryCache:
+    "Class to cache Desktop Entries"
+    def __init__(self):
+        self.cacheEntries = {}
+        self.cacheEntries['legacy'] = []
+        self.cache = {}
+
+    def addMenuEntries(self, dirs, prefix="", legacy=False):
+        for dir in dirs:
+            if not self.cacheEntries.has_key(dir):
+                self.cacheEntries[dir] = []
+                self.__addFiles(dir, "", prefix, legacy)
+
+    def __addFiles(self, dir, subdir, prefix, legacy):
+        for item in os.listdir(os.path.join(dir,subdir)):
+            if os.path.splitext(item)[1] == ".desktop":
+                try:
+                    menuentry = MenuEntry(os.path.join(subdir,item), dir, prefix)
+                except ParsingError:
+                    continue
+
+                self.cacheEntries[dir].append(menuentry)
+                if legacy == True:
+                    self.cacheEntries['legacy'].append(menuentry)
+            elif os.path.isdir(os.path.join(dir,subdir,item)) and legacy == False:
+                self.__addFiles(dir, os.path.join(subdir,item), prefix, legacy)
+
+    def getMenuEntries(self, dirs, legacy=True):
+        list = []
+        ids = []
+        # handle legacy items
+        appdirs = dirs[:]
+        if legacy == True:
+            appdirs.append("legacy")
+        # cache the results again
+        key = "".join(appdirs)
+        try:
+            return self.cache[key]
+        except KeyError:
+            pass
+        for dir in appdirs:
+            for menuentry in self.cacheEntries[dir]:
+                try:
+                    if menuentry.DesktopFileID not in ids:
+                        ids.append(menuentry.DesktopFileID)
+                        list.append(menuentry)
+                    elif menuentry.getType() == "System":
+                    # FIXME: This is only 99% correct, but still...
+                        i = list.index(menuentry)
+                        e = list[i]
+                        if e.getType() == "User":
+                            e.Original = menuentry
+                except UnicodeDecodeError:
+                    continue
+        self.cache[key] = list
+        return list
diff --git a/drlaunch/src/xdg/MenuEditor.py b/drlaunch/src/xdg/MenuEditor.py
new file mode 100644 (file)
index 0000000..cc5ce54
--- /dev/null
@@ -0,0 +1,511 @@
+""" CLass to edit XDG Menus """
+
+from xdg.Menu import *
+from xdg.BaseDirectory import *
+from xdg.Exceptions import *
+from xdg.DesktopEntry import *
+from xdg.Config import *
+
+import xml.dom.minidom
+import os
+import re
+
+# XML-Cleanups: Move / Exclude
+# FIXME: proper reverte/delete
+# FIXME: pass AppDirs/DirectoryDirs around in the edit/move functions
+# FIXME: catch Exceptions
+# FIXME: copy functions
+# FIXME: More Layout stuff
+# FIXME: unod/redo function / remove menu...
+# FIXME: Advanced MenuEditing Stuff: LegacyDir/MergeFile
+#        Complex Rules/Deleted/OnlyAllocated/AppDirs/DirectoryDirs
+
+class MenuEditor:
+    def __init__(self, menu=None, filename=None, root=False):
+        self.menu = None
+        self.filename = None
+        self.doc = None
+        self.parse(menu, filename, root)
+
+        # fix for creating two menus with the same name on the fly
+        self.filenames = []
+
+    def parse(self, menu=None, filename=None, root=False):
+        if root == True:
+            setRootMode(True)
+
+        if isinstance(menu, Menu):
+            self.menu = menu
+        elif menu:
+            self.menu = parse(menu)
+        else:
+            self.menu = parse()
+
+        if root == True:
+            self.filename = self.menu.Filename
+        elif filename:
+            self.filename = filename
+        else:
+            self.filename = os.path.join(xdg_config_dirs[0], "menus", os.path.split(self.menu.Filename)[1])
+
+        try:
+            self.doc = xml.dom.minidom.parse(self.filename)
+        except IOError:
+            self.doc = xml.dom.minidom.parseString('<!DOCTYPE Menu PUBLIC "-//freedesktop//DTD Menu 1.0//EN" "http://standards.freedesktop.org/menu-spec/menu-1.0.dtd"><Menu><Name>Applications</Name><MergeFile type="parent">'+self.menu.Filename+'</MergeFile></Menu>')
+        except xml.parsers.expat.ExpatError:
+            raise ParsingError('Not a valid .menu file', self.filename)
+
+        self.__remove_whilespace_nodes(self.doc)
+
+    def save(self):
+        self.__saveEntries(self.menu)
+        self.__saveMenu()
+
+    def createMenuEntry(self, parent, name, command=None, genericname=None, comment=None, icon=None, terminal=None, after=None, before=None):
+        menuentry = MenuEntry(self.__getFileName(name, ".desktop"))
+        menuentry = self.editMenuEntry(menuentry, name, genericname, comment, command, icon, terminal)
+
+        self.__addEntry(parent, menuentry, after, before)
+
+        sort(self.menu)
+
+        return menuentry
+
+    def createMenu(self, parent, name, genericname=None, comment=None, icon=None, after=None, before=None):
+        menu = Menu()
+
+        menu.Parent = parent
+        menu.Depth = parent.Depth + 1
+        menu.Layout = parent.DefaultLayout
+        menu.DefaultLayout = parent.DefaultLayout
+
+        menu = self.editMenu(menu, name, genericname, comment, icon)
+
+        self.__addEntry(parent, menu, after, before)
+
+        sort(self.menu)
+
+        return menu
+
+    def createSeparator(self, parent, after=None, before=None):
+        separator = Separator(parent)
+
+        self.__addEntry(parent, separator, after, before)
+
+        sort(self.menu)
+
+        return separator
+
+    def moveMenuEntry(self, menuentry, oldparent, newparent, after=None, before=None):
+        self.__deleteEntry(oldparent, menuentry, after, before)
+        self.__addEntry(newparent, menuentry, after, before)
+
+        sort(self.menu)
+
+        return menuentry
+
+    def moveMenu(self, menu, oldparent, newparent, after=None, before=None):
+        self.__deleteEntry(oldparent, menu, after, before)
+        self.__addEntry(newparent, menu, after, before)
+
+        root_menu = self.__getXmlMenu(self.menu.Name)
+        if oldparent.getPath(True) != newparent.getPath(True):
+            self.__addXmlMove(root_menu, os.path.join(oldparent.getPath(True), menu.Name), os.path.join(newparent.getPath(True), menu.Name))
+
+        sort(self.menu)
+
+        return menu
+
+    def moveSeparator(self, separator, parent, after=None, before=None):
+        self.__deleteEntry(parent, separator, after, before)
+        self.__addEntry(parent, separator, after, before)
+
+        sort(self.menu)
+
+        return separator
+
+    def copyMenuEntry(self, menuentry, oldparent, newparent, after=None, before=None):
+        self.__addEntry(newparent, menuentry, after, before)
+
+        sort(self.menu)
+
+        return menuentry
+
+    def editMenuEntry(self, menuentry, name=None, genericname=None, comment=None, command=None, icon=None, terminal=None, nodisplay=None, hidden=None):
+        deskentry = menuentry.DesktopEntry
+
+        if name:
+            if not deskentry.hasKey("Name"):
+                deskentry.set("Name", name)
+            deskentry.set("Name", name, locale = True)
+        if comment:
+            if not deskentry.hasKey("Comment"):
+                deskentry.set("Comment", comment)
+            deskentry.set("Comment", comment, locale = True)
+        if genericname:
+            if not deskentry.hasKey("GnericNe"):
+                deskentry.set("GenericName", genericname)
+            deskentry.set("GenericName", genericname, locale = True)
+        if command:
+            deskentry.set("Exec", command)
+        if icon:
+            deskentry.set("Icon", icon)
+
+        if terminal == True:
+            deskentry.set("Terminal", "true")
+        elif terminal == False:
+            deskentry.set("Terminal", "false")
+
+        if nodisplay == True:
+            deskentry.set("NoDisplay", "true")
+        elif nodisplay == False:
+            deskentry.set("NoDisplay", "false")
+
+        if hidden == True:
+            deskentry.set("Hidden", "true")
+        elif hidden == False:
+            deskentry.set("Hidden", "false")
+
+        menuentry.updateAttributes()
+
+        if len(menuentry.Parents) > 0:
+            sort(self.menu)
+
+        return menuentry
+
+    def editMenu(self, menu, name=None, genericname=None, comment=None, icon=None, nodisplay=None, hidden=None):
+        # Hack for legacy dirs
+        if isinstance(menu.Directory, MenuEntry) and menu.Directory.Filename == ".directory":
+            xml_menu = self.__getXmlMenu(menu.getPath(True, True))
+            self.__addXmlTextElement(xml_menu, 'Directory', menu.Name + ".directory")
+            menu.Directory.setAttributes(menu.Name + ".directory")
+        # Hack for New Entries
+        elif not isinstance(menu.Directory, MenuEntry):
+            if not name:
+                name = menu.Name
+            filename = self.__getFileName(name, ".directory").replace("/", "")
+            if not menu.Name:
+                menu.Name = filename.replace(".directory", "")
+            xml_menu = self.__getXmlMenu(menu.getPath(True, True))
+            self.__addXmlTextElement(xml_menu, 'Directory', filename)
+            menu.Directory = MenuEntry(filename)
+
+        deskentry = menu.Directory.DesktopEntry
+
+        if name:
+            if not deskentry.hasKey("Name"):
+                deskentry.set("Name", name)
+            deskentry.set("Name", name, locale = True)
+        if genericname:
+            if not deskentry.hasKey("GenericName"):
+                deskentry.set("GenericName", genericname)
+            deskentry.set("GenericName", genericname, locale = True)
+        if comment:
+            if not deskentry.hasKey("Comment"):
+                deskentry.set("Comment", comment)
+            deskentry.set("Comment", comment, locale = True)
+        if icon:
+            deskentry.set("Icon", icon)
+
+        if nodisplay == True:
+            deskentry.set("NoDisplay", "true")
+        elif nodisplay == False:
+            deskentry.set("NoDisplay", "false")
+
+        if hidden == True:
+            deskentry.set("Hidden", "true")
+        elif hidden == False:
+            deskentry.set("Hidden", "false")
+
+        menu.Directory.updateAttributes()
+
+        if isinstance(menu.Parent, Menu):
+            sort(self.menu)
+
+        return menu
+
+    def hideMenuEntry(self, menuentry):
+        self.editMenuEntry(menuentry, nodisplay = True)
+
+    def unhideMenuEntry(self, menuentry):
+        self.editMenuEntry(menuentry, nodisplay = False, hidden = False)
+
+    def hideMenu(self, menu):
+        self.editMenu(menu, nodisplay = True)
+
+    def unhideMenu(self, menu):
+        self.editMenu(menu, nodisplay = False, hidden = False)
+        xml_menu = self.__getXmlMenu(menu.getPath(True,True), False)
+        for node in self.__getXmlNodesByName(["Deleted", "NotDeleted"], xml_menu):
+            node.parentNode.removeChild(node)
+
+    def deleteMenuEntry(self, menuentry):
+        if self.getAction(menuentry) == "delete":
+            self.__deleteFile(menuentry.DesktopEntry.filename)
+            for parent in menuentry.Parents:
+                self.__deleteEntry(parent, menuentry)
+            sort(self.menu)
+        return menuentry
+
+    def revertMenuEntry(self, menuentry):
+        if self.getAction(menuentry) == "revert":
+            self.__deleteFile(menuentry.DesktopEntry.filename)
+            menuentry.Original.Parents = []
+            for parent in menuentry.Parents:
+                index = parent.Entries.index(menuentry)
+                parent.Entries[index] = menuentry.Original
+                index = parent.MenuEntries.index(menuentry)
+                parent.MenuEntries[index] = menuentry.Original
+                menuentry.Original.Parents.append(parent)
+            sort(self.menu)
+        return menuentry
+
+    def deleteMenu(self, menu):
+        if self.getAction(menu) == "delete":
+            self.__deleteFile(menu.Directory.DesktopEntry.filename)
+            self.__deleteEntry(menu.Parent, menu)
+            xml_menu = self.__getXmlMenu(menu.getPath(True, True))
+            xml_menu.parentNode.removeChild(xml_menu)
+            sort(self.menu)
+        return menu
+
+    def revertMenu(self, menu):
+        if self.getAction(menu) == "revert":
+            self.__deleteFile(menu.Directory.DesktopEntry.filename)
+            menu.Directory = menu.Directory.Original
+            sort(self.menu)
+        return menu
+
+    def deleteSeparator(self, separator):
+        self.__deleteEntry(separator.Parent, separator, after=True)
+
+        sort(self.menu)
+
+        return separator
+
+    """ Private Stuff """
+    def getAction(self, entry):
+        if isinstance(entry, Menu):
+            if not isinstance(entry.Directory, MenuEntry):
+                return "none"
+            elif entry.Directory.getType() == "Both":
+                return "revert"
+            elif entry.Directory.getType() == "User" \
+            and (len(entry.Submenus) + len(entry.MenuEntries)) == 0:
+                return "delete"
+
+        elif isinstance(entry, MenuEntry):
+            if entry.getType() == "Both":
+                return "revert"
+            elif entry.getType() == "User":
+                return "delete"
+            else:
+                return "none"
+
+        return "none"
+
+    def __saveEntries(self, menu):
+        if not menu:
+            menu = self.menu
+        if isinstance(menu.Directory, MenuEntry):
+            menu.Directory.save()
+        for entry in menu.getEntries(hidden=True):
+            if isinstance(entry, MenuEntry):
+                entry.save()
+            elif isinstance(entry, Menu):
+                self.__saveEntries(entry)
+
+    def __saveMenu(self):
+        if not os.path.isdir(os.path.dirname(self.filename)):
+            os.makedirs(os.path.dirname(self.filename))
+        fd = open(self.filename, 'w')
+        fd.write(re.sub("\n[\s]*([^\n<]*)\n[\s]*</", "\\1</", self.doc.toprettyxml().replace('<?xml version="1.0" ?>\n', '')))
+        fd.close()
+
+    def __getFileName(self, name, extension):
+        postfix = 0
+        while 1:
+            if postfix == 0:
+                filename = name + extension
+            else:
+                filename = name + "-" + str(postfix) + extension
+            if extension == ".desktop":
+                dir = "applications"
+            elif extension == ".directory":
+                dir = "desktop-directories"
+            if not filename in self.filenames and not \
+                os.path.isfile(os.path.join(xdg_data_dirs[0], dir, filename)):
+                self.filenames.append(filename)
+                break
+            else:
+                postfix += 1
+
+        return filename
+
+    def __getXmlMenu(self, path, create=True, element=None):
+        if not element:
+            element = self.doc
+
+        if "/" in path:
+            (name, path) = path.split("/", 1)
+        else:
+            name = path
+            path = ""
+
+        found = None
+        for node in self.__getXmlNodesByName("Menu", element):
+            for child in self.__getXmlNodesByName("Name", node):
+                if child.childNodes[0].nodeValue == name:
+                    if path:
+                        found = self.__getXmlMenu(path, create, node)
+                    else:
+                        found = node
+                    break
+            if found:
+                break
+        if not found and create == True:
+            node = self.__addXmlMenuElement(element, name)
+            if path:
+                found = self.__getXmlMenu(path, create, node)
+            else:
+                found = node
+
+        return found
+
+    def __addXmlMenuElement(self, element, name):
+        node = self.doc.createElement('Menu')
+        self.__addXmlTextElement(node, 'Name', name)
+        return element.appendChild(node)
+
+    def __addXmlTextElement(self, element, name, text):
+        node = self.doc.createElement(name)
+        text = self.doc.createTextNode(text)
+        node.appendChild(text)
+        return element.appendChild(node)
+
+    def __addXmlFilename(self, element, filename, type = "Include"):
+        # remove old filenames
+        for node in self.__getXmlNodesByName(["Include", "Exclude"], element):
+            if node.childNodes[0].nodeName == "Filename" and node.childNodes[0].childNodes[0].nodeValue == filename:
+                element.removeChild(node)
+
+        # add new filename
+        node = self.doc.createElement(type)
+        node.appendChild(self.__addXmlTextElement(node, 'Filename', filename))
+        return element.appendChild(node)
+
+    def __addXmlMove(self, element, old, new):
+        node = self.doc.createElement("Move")
+        node.appendChild(self.__addXmlTextElement(node, 'Old', old))
+        node.appendChild(self.__addXmlTextElement(node, 'New', new))
+        return element.appendChild(node)
+
+    def __addXmlLayout(self, element, layout):
+        # remove old layout
+        for node in self.__getXmlNodesByName("Layout", element):
+            element.removeChild(node)
+
+        # add new layout
+        node = self.doc.createElement("Layout")
+        for order in layout.order:
+            if order[0] == "Separator":
+                child = self.doc.createElement("Separator")
+                node.appendChild(child)
+            elif order[0] == "Filename":
+                child = self.__addXmlTextElement(node, "Filename", order[1])
+            elif order[0] == "Menuname":
+                child = self.__addXmlTextElement(node, "Menuname", order[1])
+            elif order[0] == "Merge":
+                child = self.doc.createElement("Merge")
+                child.setAttribute("type", order[1])
+                node.appendChild(child)
+        return element.appendChild(node)
+
+    def __getXmlNodesByName(self, name, element):
+        for child in element.childNodes:
+            if child.nodeType == xml.dom.Node.ELEMENT_NODE and child.nodeName in name:
+                yield child
+
+    def __addLayout(self, parent):
+        layout = Layout()
+        layout.order = []
+        layout.show_empty = parent.Layout.show_empty
+        layout.inline = parent.Layout.inline
+        layout.inline_header = parent.Layout.inline_header
+        layout.inline_alias = parent.Layout.inline_alias
+        layout.inline_limit = parent.Layout.inline_limit
+
+        layout.order.append(["Merge", "menus"])
+        for entry in parent.Entries:
+            if isinstance(entry, Menu):
+                layout.parseMenuname(entry.Name)
+            elif isinstance(entry, MenuEntry):
+                layout.parseFilename(entry.DesktopFileID)
+            elif isinstance(entry, Separator):
+                layout.parseSeparator()
+        layout.order.append(["Merge", "files"])
+
+        parent.Layout = layout
+
+        return layout
+
+    def __addEntry(self, parent, entry, after=None, before=None):
+        if after or before:
+            if after:
+                index = parent.Entries.index(after) + 1
+            elif before:
+                index = parent.Entries.index(before)
+            parent.Entries.insert(index, entry)
+        else:
+            parent.Entries.append(entry)
+
+        xml_parent = self.__getXmlMenu(parent.getPath(True, True))
+
+        if isinstance(entry, MenuEntry):
+            parent.MenuEntries.append(entry)
+            entry.Parents.append(parent)
+            self.__addXmlFilename(xml_parent, entry.DesktopFileID, "Include")
+        elif isinstance(entry, Menu):
+            parent.addSubmenu(entry)
+
+        if after or before:
+            self.__addLayout(parent)
+            self.__addXmlLayout(xml_parent, parent.Layout)
+
+    def __deleteEntry(self, parent, entry, after=None, before=None):
+        parent.Entries.remove(entry)
+
+        xml_parent = self.__getXmlMenu(parent.getPath(True, True))
+
+        if isinstance(entry, MenuEntry):
+            entry.Parents.remove(parent)
+            parent.MenuEntries.remove(entry)
+            self.__addXmlFilename(xml_parent, entry.DesktopFileID, "Exclude")
+        elif isinstance(entry, Menu):
+            parent.Submenus.remove(entry)
+
+        if after or before:
+            self.__addLayout(parent)
+            self.__addXmlLayout(xml_parent, parent.Layout)
+
+    def __deleteFile(self, filename):
+        try:
+            os.remove(filename)
+        except OSError:
+            pass
+        try:
+            self.filenames.remove(filename)
+        except ValueError:
+            pass
+
+    def __remove_whilespace_nodes(self, node):
+        remove_list = []
+        for child in node.childNodes:
+            if child.nodeType == xml.dom.minidom.Node.TEXT_NODE:
+                child.data = child.data.strip()
+                if not child.data.strip():
+                    remove_list.append(child)
+            elif child.hasChildNodes():
+                self.__remove_whilespace_nodes(child)
+        for node in remove_list:
+            node.parentNode.removeChild(node)
diff --git a/drlaunch/src/xdg/Mime.py b/drlaunch/src/xdg/Mime.py
new file mode 100644 (file)
index 0000000..04fe0d2
--- /dev/null
@@ -0,0 +1,474 @@
+"""
+This module is based on a rox module (LGPL):
+
+http://cvs.sourceforge.net/viewcvs.py/rox/ROX-Lib2/python/rox/mime.py?rev=1.21&view=log
+
+This module provides access to the shared MIME database.
+
+types is a dictionary of all known MIME types, indexed by the type name, e.g.
+types['application/x-python']
+
+Applications can install information about MIME types by storing an
+XML file as <MIME>/packages/<application>.xml and running the
+update-mime-database command, which is provided by the freedesktop.org
+shared mime database package.
+
+See http://www.freedesktop.org/standards/shared-mime-info-spec/ for
+information about the format of these files.
+
+(based on version 0.13)
+"""
+
+import os
+import stat
+import fnmatch
+
+import xdg.BaseDirectory
+import xdg.Locale
+
+from xml.dom import Node, minidom, XML_NAMESPACE
+
+FREE_NS = 'http://www.freedesktop.org/standards/shared-mime-info'
+
+types = {}      # Maps MIME names to type objects
+
+exts = None     # Maps extensions to types
+globs = None    # List of (glob, type) pairs
+literals = None # Maps liternal names to types
+magic = None
+
+def _get_node_data(node):
+    """Get text of XML node"""
+    return ''.join([n.nodeValue for n in node.childNodes]).strip()
+
+def lookup(media, subtype = None):
+    "Get the MIMEtype object for this type, creating a new one if needed."
+    if subtype is None and '/' in media:
+        media, subtype = media.split('/', 1)
+    if (media, subtype) not in types:
+        types[(media, subtype)] = MIMEtype(media, subtype)
+    return types[(media, subtype)]
+
+class MIMEtype:
+    """Type holding data about a MIME type"""
+    def __init__(self, media, subtype):
+        "Don't use this constructor directly; use mime.lookup() instead."
+        assert media and '/' not in media
+        assert subtype and '/' not in subtype
+        assert (media, subtype) not in types
+
+        self.media = media
+        self.subtype = subtype
+        self._comment = None
+
+    def _load(self):
+        "Loads comment for current language. Use get_comment() instead."
+        resource = os.path.join('mime', self.media, self.subtype + '.xml')
+        for path in xdg.BaseDirectory.load_data_paths(resource):
+            doc = minidom.parse(path)
+            if doc is None:
+                continue
+            for comment in doc.documentElement.getElementsByTagNameNS(FREE_NS, 'comment'):
+                lang = comment.getAttributeNS(XML_NAMESPACE, 'lang') or 'en'
+                goodness = 1 + (lang in xdg.Locale.langs)
+                if goodness > self._comment[0]:
+                    self._comment = (goodness, _get_node_data(comment))
+                if goodness == 2: return
+
+    # FIXME: add get_icon method
+    def get_comment(self):
+        """Returns comment for current language, loading it if needed."""
+        # Should we ever reload?
+        if self._comment is None:
+            self._comment = (0, str(self))
+            self._load()
+        return self._comment[1]
+
+    def __str__(self):
+        return self.media + '/' + self.subtype
+
+    def __repr__(self):
+        return '[%s: %s]' % (self, self._comment or '(comment not loaded)')
+
+class MagicRule:
+    def __init__(self, f):
+        self.next=None
+        self.prev=None
+
+        #print line
+        ind=''
+        while True:
+            c=f.read(1)
+            if c=='>':
+                break
+            ind+=c
+        if not ind:
+            self.nest=0
+        else:
+            self.nest=int(ind)
+
+        start=''
+        while True:
+            c=f.read(1)
+            if c=='=':
+                break
+            start+=c
+        self.start=int(start)
+        
+        hb=f.read(1)
+        lb=f.read(1)
+        self.lenvalue=ord(lb)+(ord(hb)<<8)
+
+        self.value=f.read(self.lenvalue)
+
+        c=f.read(1)
+        if c=='&':
+            self.mask=f.read(self.lenvalue)
+            c=f.read(1)
+        else:
+            self.mask=None
+
+        if c=='~':
+            w=''
+            while c!='+' and c!='\n':
+                c=f.read(1)
+                if c=='+' or c=='\n':
+                    break
+                w+=c
+            
+            self.word=int(w)
+        else:
+            self.word=1
+
+        if c=='+':
+            r=''
+            while c!='\n':
+                c=f.read(1)
+                if c=='\n':
+                    break
+                r+=c
+            #print r
+            self.range=int(r)
+        else:
+            self.range=1
+
+        if c!='\n':
+            raise 'Malformed MIME magic line'
+
+    def getLength(self):
+        return self.start+self.lenvalue+self.range
+
+    def appendRule(self, rule):
+        if self.nest<rule.nest:
+            self.next=rule
+            rule.prev=self
+
+        elif self.prev:
+            self.prev.appendRule(rule)
+        
+    def match(self, buffer):
+        if self.match0(buffer):
+            if self.next:
+                return self.next.match(buffer)
+            return True
+
+    def match0(self, buffer):
+        l=len(buffer)
+        for o in range(self.range):
+            s=self.start+o
+            e=s+self.lenvalue
+            if l<e:
+                return False
+            if self.mask:
+                test=''
+                for i in range(self.lenvalue):
+                    c=ord(buffer[s+i]) & ord(self.mask[i])
+                    test+=chr(c)
+            else:
+                test=buffer[s:e]
+
+            if test==self.value:
+                return True
+
+    def __repr__(self):
+        return '<MagicRule %d>%d=[%d]%s&%s~%d+%d>' % (self.nest,
+                                  self.start,
+                                  self.lenvalue,
+                                  `self.value`,
+                                  `self.mask`,
+                                  self.word,
+                                  self.range)
+
+class MagicType:
+    def __init__(self, mtype):
+        self.mtype=mtype
+        self.top_rules=[]
+        self.last_rule=None
+
+    def getLine(self, f):
+        nrule=MagicRule(f)
+
+        if nrule.nest and self.last_rule:
+            self.last_rule.appendRule(nrule)
+        else:
+            self.top_rules.append(nrule)
+
+        self.last_rule=nrule
+
+        return nrule
+
+    def match(self, buffer):
+        for rule in self.top_rules:
+            if rule.match(buffer):
+                return self.mtype
+
+    def __repr__(self):
+        return '<MagicType %s>' % self.mtype
+    
+class MagicDB:
+    def __init__(self):
+        self.types={}   # Indexed by priority, each entry is a list of type rules
+        self.maxlen=0
+
+    def mergeFile(self, fname):
+        f=file(fname, 'r')
+        line=f.readline()
+        if line!='MIME-Magic\0\n':
+            raise 'Not a MIME magic file'
+
+        while True:
+            shead=f.readline()
+            #print shead
+            if not shead:
+                break
+            if shead[0]!='[' or shead[-2:]!=']\n':
+                raise 'Malformed section heading'
+            pri, tname=shead[1:-2].split(':')
+            #print shead[1:-2]
+            pri=int(pri)
+            mtype=lookup(tname)
+
+            try:
+                ents=self.types[pri]
+            except:
+                ents=[]
+                self.types[pri]=ents
+
+            magictype=MagicType(mtype)
+            #print tname
+
+            #rline=f.readline()
+            c=f.read(1)
+            f.seek(-1, 1)
+            while c and c!='[':
+                rule=magictype.getLine(f)
+                #print rule
+                if rule and rule.getLength()>self.maxlen:
+                    self.maxlen=rule.getLength()
+
+                c=f.read(1)
+                f.seek(-1, 1)
+
+            ents.append(magictype)
+            #self.types[pri]=ents
+            if not c:
+                break
+
+    def match_data(self, data, max_pri=100, min_pri=0):
+        pris=self.types.keys()
+        pris.sort(lambda a, b: -cmp(a, b))
+        for pri in pris:
+            #print pri, max_pri, min_pri
+            if pri>max_pri:
+                continue
+            if pri<min_pri:
+                break
+            for type in self.types[pri]:
+                m=type.match(data)
+                if m:
+                    return m
+        
+
+    def match(self, path, max_pri=100, min_pri=0):
+        try:
+            buf=file(path, 'r').read(self.maxlen)
+            return self.match_data(buf, max_pri, min_pri)
+        except:
+            pass
+
+        return None
+    
+    def __repr__(self):
+        return '<MagicDB %s>' % self.types
+            
+
+# Some well-known types
+text = lookup('text', 'plain')
+inode_block = lookup('inode', 'blockdevice')
+inode_char = lookup('inode', 'chardevice')
+inode_dir = lookup('inode', 'directory')
+inode_fifo = lookup('inode', 'fifo')
+inode_socket = lookup('inode', 'socket')
+inode_symlink = lookup('inode', 'symlink')
+inode_door = lookup('inode', 'door')
+app_exe = lookup('application', 'executable')
+
+_cache_uptodate = False
+
+def _cache_database():
+    global exts, globs, literals, magic, _cache_uptodate
+
+    _cache_uptodate = True
+
+    exts = {}       # Maps extensions to types
+    globs = []      # List of (glob, type) pairs
+    literals = {}   # Maps liternal names to types
+    magic = MagicDB()
+
+    def _import_glob_file(path):
+        """Loads name matching information from a MIME directory."""
+        for line in file(path):
+            if line.startswith('#'): continue
+            line = line[:-1]
+
+            type_name, pattern = line.split(':', 1)
+            mtype = lookup(type_name)
+
+            if pattern.startswith('*.'):
+                rest = pattern[2:]
+                if not ('*' in rest or '[' in rest or '?' in rest):
+                    exts[rest] = mtype
+                    continue
+            if '*' in pattern or '[' in pattern or '?' in pattern:
+                globs.append((pattern, mtype))
+            else:
+                literals[pattern] = mtype
+
+    for path in xdg.BaseDirectory.load_data_paths(os.path.join('mime', 'globs')):
+        _import_glob_file(path)
+    for path in xdg.BaseDirectory.load_data_paths(os.path.join('mime', 'magic')):
+        magic.mergeFile(path)
+
+    # Sort globs by length
+    globs.sort(lambda a, b: cmp(len(b[0]), len(a[0])))
+
+def get_type_by_name(path):
+    """Returns type of file by its name, or None if not known"""
+    if not _cache_uptodate:
+        _cache_database()
+
+    leaf = os.path.basename(path)
+    if leaf in literals:
+        return literals[leaf]
+
+    lleaf = leaf.lower()
+    if lleaf in literals:
+        return literals[lleaf]
+
+    ext = leaf
+    while 1:
+        p = ext.find('.')
+        if p < 0: break
+        ext = ext[p + 1:]
+        if ext in exts:
+            return exts[ext]
+    ext = lleaf
+    while 1:
+        p = ext.find('.')
+        if p < 0: break
+        ext = ext[p+1:]
+        if ext in exts:
+            return exts[ext]
+    for (glob, mime_type) in globs:
+        if fnmatch.fnmatch(leaf, glob):
+            return mime_type
+        if fnmatch.fnmatch(lleaf, glob):
+            return mime_type
+    return None
+
+def get_type_by_contents(path, max_pri=100, min_pri=0):
+    """Returns type of file by its contents, or None if not known"""
+    if not _cache_uptodate:
+        _cache_database()
+
+    return magic.match(path, max_pri, min_pri)
+
+def get_type_by_data(data, max_pri=100, min_pri=0):
+    """Returns type of the data"""
+    if not _cache_uptodate:
+        _cache_database()
+
+    return magic.match_data(data, max_pri, min_pri)
+
+def get_type(path, follow=1, name_pri=100):
+    """Returns type of file indicated by path.
+    path     - pathname to check (need not exist)
+    follow   - when reading file, follow symbolic links
+    name_pri - Priority to do name matches.  100=override magic"""
+    if not _cache_uptodate:
+        _cache_database()
+    
+    try:
+        if follow:
+            st = os.stat(path)
+        else:
+            st = os.lstat(path)
+    except:
+        t = get_type_by_name(path)
+        return t or text
+
+    if stat.S_ISREG(st.st_mode):
+        t = get_type_by_contents(path, min_pri=name_pri)
+        if not t: t = get_type_by_name(path)
+        if not t: t = get_type_by_contents(path, max_pri=name_pri)
+        if t is None:
+            if stat.S_IMODE(st.st_mode) & 0111:
+                return app_exe
+            else:
+                return text
+        return t
+    elif stat.S_ISDIR(st.st_mode): return inode_dir
+    elif stat.S_ISCHR(st.st_mode): return inode_char
+    elif stat.S_ISBLK(st.st_mode): return inode_block
+    elif stat.S_ISFIFO(st.st_mode): return inode_fifo
+    elif stat.S_ISLNK(st.st_mode): return inode_symlink
+    elif stat.S_ISSOCK(st.st_mode): return inode_socket
+    return inode_door
+
+def install_mime_info(application, package_file):
+    """Copy 'package_file' as ~/.local/share/mime/packages/<application>.xml.
+    If package_file is None, install <app_dir>/<application>.xml.
+    If already installed, does nothing. May overwrite an existing
+    file with the same name (if the contents are different)"""
+    application += '.xml'
+
+    new_data = file(package_file).read()
+
+    # See if the file is already installed
+    package_dir = os.path.join('mime', 'packages')
+    resource = os.path.join(package_dir, application)
+    for x in xdg.BaseDirectory.load_data_paths(resource):
+        try:
+            old_data = file(x).read()
+        except:
+            continue
+        if old_data == new_data:
+            return  # Already installed
+
+    global _cache_uptodate
+    _cache_uptodate = False
+
+    # Not already installed; add a new copy
+    # Create the directory structure...
+    new_file = os.path.join(xdg.BaseDirectory.save_data_path(package_dir), application)
+
+    # Write the file...
+    file(new_file, 'w').write(new_data)
+
+    # Update the database...
+    command = 'update-mime-database'
+    if os.spawnlp(os.P_WAIT, command, command, xdg.BaseDirectory.save_data_path('mime')):
+        os.unlink(new_file)
+        raise Exception("The '%s' command returned an error code!\n" \
+                  "Make sure you have the freedesktop.org shared MIME package:\n" \
+                  "http://standards.freedesktop.org/shared-mime-info/") % command
diff --git a/drlaunch/src/xdg/RecentFiles.py b/drlaunch/src/xdg/RecentFiles.py
new file mode 100644 (file)
index 0000000..6c2cd85
--- /dev/null
@@ -0,0 +1,159 @@
+"""
+Implementation of the XDG Recent File Storage Specification Version 0.2
+http://standards.freedesktop.org/recent-file-spec
+"""
+
+import xml.dom.minidom, xml.sax.saxutils
+import os, time, fcntl
+from xdg.Exceptions import *
+
+class RecentFiles:
+    def __init__(self):
+        self.RecentFiles = []
+        self.filename = ""
+
+    def parse(self, filename=None):
+        if not filename:
+            filename = os.path.join(os.getenv("HOME"), ".recently-used")
+
+        try:
+            doc = xml.dom.minidom.parse(filename)
+        except IOError:
+            raise ParsingError('File not found', filename)
+        except xml.parsers.expat.ExpatError:
+            raise ParsingError('Not a valid .menu file', filename)
+
+        self.filename = filename
+
+        for child in doc.childNodes:
+            if child.nodeType == xml.dom.Node.ELEMENT_NODE:
+                if child.tagName == "RecentFiles":
+                    for recent in child.childNodes:
+                        if recent.nodeType == xml.dom.Node.ELEMENT_NODE:    
+                            if recent.tagName == "RecentItem":
+                                self.__parseRecentItem(recent)
+
+        self.sort()
+
+    def __parseRecentItem(self, item):
+        recent = RecentFile()
+        self.RecentFiles.append(recent)
+
+        for attribute in item.childNodes:
+            if attribute.nodeType == xml.dom.Node.ELEMENT_NODE:
+                if attribute.tagName == "URI":
+                    recent.URI = attribute.childNodes[0].nodeValue
+                elif attribute.tagName == "Mime-Type":
+                    recent.MimeType = a